diff --git a/cmd/gluetun/main.go b/cmd/gluetun/main.go index c01294e8..c76151ec 100644 --- a/cmd/gluetun/main.go +++ b/cmd/gluetun/main.go @@ -205,8 +205,12 @@ func _main(ctx context.Context, buildInfo models.BuildInformation, if *allSettings.Firewall.Debug { // To remove in v4 firewallLogger.PatchLevel(logging.LevelDebug) } - firewallConf := firewall.NewConfig(firewallLogger, cmder, + firewallConf, err := firewall.NewConfig(ctx, firewallLogger, cmder, defaultInterface, defaultGateway, localNetworks, defaultIP) + if err != nil { + return err + } + if *allSettings.Firewall.Enabled { err = firewallConf.SetEnabled(ctx, true) if err != nil { diff --git a/internal/firewall/firewall.go b/internal/firewall/firewall.go index a28cb7a7..45698757 100644 --- a/internal/firewall/firewall.go +++ b/internal/firewall/firewall.go @@ -33,7 +33,8 @@ type Config struct { //nolint:maligned localIP net.IP // Fixed state - ip6Tables bool + ipTables string + ip6Tables string customRulesPath string // State @@ -45,20 +46,28 @@ type Config struct { //nolint:maligned stateMutex sync.Mutex } -// NewConfig creates a new Config instance. -func NewConfig(logger Logger, runner command.Runner, - defaultInterface string, defaultGateway net.IP, - localNetworks []routing.LocalNetwork, localIP net.IP) *Config { +// NewConfig creates a new Config instance and returns an error +// if no iptables implementation is available. +func NewConfig(ctx context.Context, logger Logger, + runner command.Runner, defaultInterface string, + defaultGateway net.IP, localNetworks []routing.LocalNetwork, + localIP net.IP) (config *Config, err error) { + iptables, err := findIptablesSupported(ctx, runner) + if err != nil { + return nil, err + } + return &Config{ runner: runner, logger: logger, allowedInputPorts: make(map[uint16]string), - ip6Tables: ip6tablesSupported(context.Background(), runner), + ipTables: iptables, + ip6Tables: findIP6tablesSupported(ctx, runner), customRulesPath: "/iptables/post-rules.txt", // Obtained from routing defaultInterface: defaultInterface, defaultGateway: defaultGateway, localNetworks: localNetworks, localIP: localIP, - } + }, nil } diff --git a/internal/firewall/ip6tables.go b/internal/firewall/ip6tables.go index 5680b73e..7b261fb3 100644 --- a/internal/firewall/ip6tables.go +++ b/internal/firewall/ip6tables.go @@ -10,16 +10,27 @@ import ( "github.com/qdm12/golibs/command" ) -var ( - ErrIP6NotSupported = errors.New("ip6tables not supported") -) +// findIP6tablesSupported checks for multiple iptables implementations +// and returns the iptables path that is supported. If none work, an +// empty string path is returned. +func findIP6tablesSupported(ctx context.Context, runner command.Runner) ( + ip6tablesPath string) { + binsToTry := []string{"ip6tables", "ip6tables-nft"} -func ip6tablesSupported(ctx context.Context, runner command.Runner) (supported bool) { - cmd := exec.CommandContext(ctx, "ip6tables", "-L") - if _, err := runner.Run(cmd); err != nil { - return false + var err error + for _, ip6tablesPath = range binsToTry { + cmd := exec.CommandContext(ctx, ip6tablesPath, "-L") + _, err = runner.Run(cmd) + if err == nil { + break + } } - return true + + if err != nil { + return "" + } + + return ip6tablesPath } func (c *Config) runIP6tablesInstructions(ctx context.Context, instructions []string) error { @@ -32,18 +43,19 @@ func (c *Config) runIP6tablesInstructions(ctx context.Context, instructions []st } func (c *Config) runIP6tablesInstruction(ctx context.Context, instruction string) error { - if !c.ip6Tables { + if c.ip6Tables == "" { return nil } c.ip6tablesMutex.Lock() // only one ip6tables command at once defer c.ip6tablesMutex.Unlock() - c.logger.Debug("ip6tables " + instruction) + c.logger.Debug(c.ip6Tables + " " + instruction) flags := strings.Fields(instruction) - cmd := exec.CommandContext(ctx, "ip6tables", flags...) + cmd := exec.CommandContext(ctx, c.ip6Tables, flags...) // #nosec G204 if output, err := c.runner.Run(cmd); err != nil { - return fmt.Errorf("command failed: \"ip6tables %s\": %s: %w", instruction, output, err) + return fmt.Errorf("command failed: \"%s %s\": %s: %w", + c.ip6Tables, instruction, output, err) } return nil } diff --git a/internal/firewall/iptables.go b/internal/firewall/iptables.go index 65c395f1..1d4ce9cd 100644 --- a/internal/firewall/iptables.go +++ b/internal/firewall/iptables.go @@ -15,11 +15,31 @@ import ( ) var ( + ErrIPTablesNotSupported = errors.New("no iptables supported found") ErrIPTablesVersionTooShort = errors.New("iptables version string is too short") ErrPolicyUnknown = errors.New("unknown policy") ErrNeedIP6Tables = errors.New("ip6tables is required, please upgrade your kernel to support it") ) +func findIptablesSupported(ctx context.Context, runner command.Runner) (iptablesPath string, err error) { + binsToTry := []string{"iptables", "iptables-nft"} + + for _, iptablesPath = range binsToTry { + cmd := exec.CommandContext(ctx, iptablesPath, "-L") + _, err = runner.Run(cmd) + if err == nil { + break + } + } + + if err != nil { + return "", fmt.Errorf("%w: from %s: last error is %s", + ErrIPTablesNotSupported, strings.Join(binsToTry, ", "), err) + } + + return iptablesPath, nil +} + func appendOrDelete(remove bool) string { if remove { return "--delete" @@ -71,12 +91,13 @@ func (c *Config) runIptablesInstruction(ctx context.Context, instruction string) c.iptablesMutex.Lock() // only one iptables command at once defer c.iptablesMutex.Unlock() - c.logger.Debug("iptables " + instruction) + c.logger.Debug(c.ipTables + " " + instruction) flags := strings.Fields(instruction) - cmd := exec.CommandContext(ctx, "iptables", flags...) + cmd := exec.CommandContext(ctx, c.ipTables, flags...) // #nosec G204 if output, err := c.runner.Run(cmd); err != nil { - return fmt.Errorf("command failed: \"iptables %s\": %s: %w", instruction, output, err) + return fmt.Errorf("command failed: \"%s %s\": %s: %w", + c.ipTables, instruction, output, err) } return nil } @@ -124,7 +145,7 @@ func (c *Config) acceptInputToSubnet(ctx context.Context, intf string, destinati if isIP4Subnet { return c.runIptablesInstruction(ctx, instruction) } - if !c.ip6Tables { + if c.ip6Tables == "" { return fmt.Errorf("accept input to subnet %s: %w", destination, ErrNeedIP6Tables) } return c.runIP6tablesInstruction(ctx, instruction) @@ -151,7 +172,7 @@ func (c *Config) acceptOutputTrafficToVPN(ctx context.Context, isIPv4 := connection.IP.To4() != nil if isIPv4 { return c.runIptablesInstruction(ctx, instruction) - } else if !c.ip6Tables { + } else if c.ip6Tables == "" { return fmt.Errorf("accept output to VPN server: %w", ErrNeedIP6Tables) } return c.runIP6tablesInstruction(ctx, instruction) @@ -172,7 +193,7 @@ func (c *Config) acceptOutputFromIPToSubnet(ctx context.Context, if doIPv4 { return c.runIptablesInstruction(ctx, instruction) - } else if !c.ip6Tables { + } else if c.ip6Tables == "" { return fmt.Errorf("accept output from %s to %s: %w", sourceIP, destinationSubnet, ErrNeedIP6Tables) } return c.runIP6tablesInstruction(ctx, instruction) @@ -223,9 +244,15 @@ func (c *Config) runUserPostRules(ctx context.Context, filepath string, remove b case strings.HasPrefix(line, "iptables "): ipv4 = true rule = strings.TrimPrefix(line, "iptables ") + case strings.HasPrefix(line, "iptables-nft "): + ipv4 = true + rule = strings.TrimPrefix(line, "iptables-nft ") case strings.HasPrefix(line, "ip6tables "): ipv4 = false rule = strings.TrimPrefix(line, "ip6tables ") + case strings.HasPrefix(line, "ip6tables-nft "): + ipv4 = false + rule = strings.TrimPrefix(line, "ip6tables-nft ") default: continue } @@ -237,7 +264,7 @@ func (c *Config) runUserPostRules(ctx context.Context, filepath string, remove b switch { case ipv4: err = c.runIptablesInstruction(ctx, rule) - case !c.ip6Tables: + case c.ip6Tables == "": err = fmt.Errorf("cannot run user ip6tables rule: %w", ErrNeedIP6Tables) default: // ipv6 err = c.runIP6tablesInstruction(ctx, rule)