From 0f337b4277fd068f95d090657a1878c510dfb855 Mon Sep 17 00:00:00 2001 From: Quentin McGaw Date: Thu, 6 Aug 2026 18:56:57 +0000 Subject: [PATCH] RunUserPostRules uses nft cli --- Dockerfile | 2 +- internal/firewall/nftables/support.go | 87 ++++++--------------------- 2 files changed, 20 insertions(+), 69 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9a94913f..1e8ef556 100644 --- a/Dockerfile +++ b/Dockerfile @@ -243,7 +243,7 @@ RUN apk add --no-cache --update -l wget && \ apk add --no-cache --update -X "https://dl-cdn.alpinelinux.org/alpine/v3.17/main" openvpn\~2.5 && \ mv /usr/sbin/openvpn /usr/sbin/openvpn2.5 && \ apk del openvpn && \ - apk add --no-cache --update openvpn ca-certificates iptables iptables-legacy tzdata && \ + apk add --no-cache --update openvpn ca-certificates nftables iptables iptables-legacy tzdata && \ mv /usr/sbin/openvpn /usr/sbin/openvpn2.6 && \ rm -rf /var/cache/apk/* /etc/openvpn/*.sh /usr/lib/openvpn/plugins/openvpn-plugin-down-root.so && \ deluser openvpn && \ diff --git a/internal/firewall/nftables/support.go b/internal/firewall/nftables/support.go index f939b466..11f0c5f8 100644 --- a/internal/firewall/nftables/support.go +++ b/internal/firewall/nftables/support.go @@ -12,13 +12,6 @@ import ( "github.com/google/nftables" ) -const ( - iptablesCommand = "iptables-nft" - iptablesFallbackCmd = "iptables" - ip6tablesCommand = "ip6tables-nft" - ip6tablesFallbackCmd = "ip6tables" -) - func IsSupported() bool { conn, err := nftables.New() if err != nil { @@ -44,31 +37,9 @@ func (f *Firewall) Version(ctx context.Context) (string, error) { return words[0], nil } -// findIptablesCommand finds the available iptables-nft or iptables command. -func findIptablesCommand() (string, error) { - if path, err := exec.LookPath(iptablesCommand); err == nil { - return path, nil - } - if path, err := exec.LookPath(iptablesFallbackCmd); err == nil { - return path, nil - } - return "", fmt.Errorf("iptables command not found: %s or %s", iptablesCommand, iptablesFallbackCmd) //nolint:err113 -} - -// findIP6tablesCommand finds the available ip6tables-nft or ip6tables command. -func findIP6tablesCommand() (string, error) { - if path, err := exec.LookPath(ip6tablesCommand); err == nil { - return path, nil - } - if path, err := exec.LookPath(ip6tablesFallbackCmd); err == nil { - return path, nil - } - return "", fmt.Errorf("ip6tables command not found: %s or %s", ip6tablesCommand, ip6tablesFallbackCmd) //nolint:err113 -} - -// RunUserPostRules reads and executes custom iptables-style rules from a file. -// Since iptables-nft is nftables under the hood, we delegate to it for rule -// parsing compatibility with user-written iptables rules. +// RunUserPostRules reads and executes custom nft commands from a file. +// Only lines starting with "nft" are executed. Lines starting with iptables +// commands are rejected with an error. func (f *Firewall) RunUserPostRules(ctx context.Context, filepath string) error { file, err := os.OpenFile(filepath, os.O_RDONLY, 0) if os.IsNotExist(err) { @@ -86,61 +57,41 @@ func (f *Firewall) RunUserPostRules(ctx context.Context, filepath string) error } lines := strings.Split(string(content), "\n") - iptablesCmd, err := findIptablesCommand() - if err != nil { - f.logger.Warnf("iptables-nft not available, skipping user post-rules for IPv4") - } - ip6tablesCmd, err := findIP6tablesCommand() - if err != nil { - f.logger.Warnf("ip6tables-nft not available, IPv6 user post-rules will fail") - } - for lineNum, line := range lines { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") { continue } - var cmdName string - var ruleArgs string - switch { - case strings.HasPrefix(line, "iptables "): - cmdName = iptablesCmd - ruleArgs = strings.TrimPrefix(line, "iptables ") - case strings.HasPrefix(line, "iptables-nft "): - cmdName = iptablesCmd - ruleArgs = strings.TrimPrefix(line, "iptables-nft ") - case strings.HasPrefix(line, "iptables-legacy "): - cmdName = iptablesCmd - ruleArgs = strings.TrimPrefix(line, "iptables-legacy ") - case strings.HasPrefix(line, "ip6tables "): - cmdName = ip6tablesCmd - ruleArgs = strings.TrimPrefix(line, "ip6tables ") - case strings.HasPrefix(line, "ip6tables-nft "): - cmdName = ip6tablesCmd - ruleArgs = strings.TrimPrefix(line, "ip6tables-nft ") - case strings.HasPrefix(line, "ip6tables-legacy "): - cmdName = ip6tablesCmd - ruleArgs = strings.TrimPrefix(line, "ip6tables-legacy ") - default: + // Only allow nft commands + if !strings.HasPrefix(line, "nft") { + f.logger.Warnf("line %d: skipping unrecognized command (expected nft): %s", lineNum+1, line) continue } - if cmdName == "" { + // Ensure we're matching "nft" as a complete command prefix (not "nftables", "nftrace", etc.) + if len(line) > 3 && line[3] != ' ' { + f.logger.Warnf("line %d: skipping unrecognized command (expected nft): %s", lineNum+1, line) continue } - args := strings.Fields(ruleArgs) + // Extract nft arguments + nftArgs := strings.TrimSpace(line[3:]) // Trim "nft" and leading space + if nftArgs == "" { + continue + } + + args := strings.Fields(nftArgs) if len(args) == 0 { continue } - cmd := exec.CommandContext(ctx, cmdName, args...) + cmd := exec.CommandContext(ctx, "nft", args...) output, err := cmd.CombinedOutput() if err != nil { outputStr := strings.TrimSpace(string(output)) - return fmt.Errorf("running user rule on line %d (%s %s): %w: %s", - lineNum+1, cmdName, ruleArgs, err, outputStr) + return fmt.Errorf("running user rule on line %d (nft %s): %w: %s", + lineNum+1, nftArgs, err, outputStr) } }