From bfc8136bc9ee45ccd4088aed3936f3b2ea2ecde6 Mon Sep 17 00:00:00 2001 From: Quentin McGaw Date: Fri, 27 Feb 2026 12:17:12 +0000 Subject: [PATCH] Fourth fallback, use DROP temporarily instead of REJECT --- internal/firewall/flush.go | 63 +++++++++++++++++--------- internal/firewall/interfaces.go | 1 + internal/firewall/iptables/iptables.go | 22 +++++++-- 3 files changed, 59 insertions(+), 27 deletions(-) diff --git a/internal/firewall/flush.go b/internal/firewall/flush.go index 0ef567ff..f09bc6a2 100644 --- a/internal/firewall/flush.go +++ b/internal/firewall/flush.go @@ -10,33 +10,52 @@ import ( "github.com/qdm12/gluetun/internal/netlink" ) -// Note remove is a no-op if conntrack netlink is supported by the kernel. func (c *Config) flushExistingConnections(ctx context.Context) error { - err := c.netlinker.FlushConntrack() - switch { - case err == nil: - return nil - case errors.Is(err, netlink.ErrConntrackNetlinkNotSupported): - c.logger.Debugf("falling back to marking and filtering unmarked packets because flush conntrack failed: %s", err) - err = c.impl.AcceptOutputPublicOnlyNewTraffic(ctx) - if err != nil { - if errors.Is(err, iptables.ErrKernelModuleMissing) { - c.logger.Debugf("falling back to killing connections for one second because marking packets failed: %s", err) - return c.rejectOutputTrafficTemporarily(ctx) - } - return fmt.Errorf("accepting only new output public traffic: %w", err) - } - return nil - default: - return fmt.Errorf("flushing conntrack: %w", err) + tries := []struct { + name string + f func(ctx context.Context) error + }{ + {name: "flushing conntrack", f: func(_ context.Context) error { + return c.netlinker.FlushConntrack() + }}, + {name: "marking and filtering unmarked packets", f: c.impl.AcceptOutputPublicOnlyNewTraffic}, + {name: "rejecting connections for one second", f: c.rejectOutputTrafficTemporarily}, + {name: "dropping connections for one second", f: c.dropOutputTrafficTemporarily}, } + errs := make([]error, 0, len(tries)) + for i, try := range tries { + if i > 0 { + c.logger.Debugf("falling back to %s because %s failed: %s", try.name, tries[i-1].name, errs[i-1]) + } + err := try.f(ctx) + if err == nil { + return nil + } + err = fmt.Errorf("%s: %w", try.name, err) + if !errors.Is(err, iptables.ErrKernelModuleMissing) && !errors.Is(err, netlink.ErrConntrackNetlinkNotSupported) { + return err + } + errs = append(errs, err) + } + return fmt.Errorf("all tries failed: %v", errs) //nolint:err113 } func (c *Config) rejectOutputTrafficTemporarily(ctx context.Context) error { + return setupThenRevert(ctx, c.impl.RejectOutputPublicTraffic) +} + +func (c *Config) dropOutputTrafficTemporarily(ctx context.Context) error { + return setupThenRevert(ctx, c.impl.DropOutputPublicTraffic) +} + +// setupThenRevert is a helper function to run a setup function that takes a remove boolean argument, +// and then run the same function with remove set to true after one second or when the context is canceled, +// whichever comes first. +func setupThenRevert(ctx context.Context, f func(ctx context.Context, remove bool) error) error { remove := false - err := c.impl.RejectOutputPublicTraffic(ctx, remove) + err := f(ctx, remove) if err != nil { - return fmt.Errorf("rejecting only new output public traffic: %w", err) + return fmt.Errorf("setting up: %w", err) } timer := time.NewTimer(time.Second) select { @@ -47,9 +66,9 @@ func (c *Config) rejectOutputTrafficTemporarily(ctx context.Context) error { remove = true // Use [context.Background] to make sure this is removed, even if the context // passed to this function is canceled. - err = c.impl.RejectOutputPublicTraffic(context.Background(), remove) + err = f(context.Background(), remove) if err != nil { - return fmt.Errorf("reverting rejecting only new output public traffic: %w", err) + return fmt.Errorf("reverting: %w", err) } return nil } diff --git a/internal/firewall/interfaces.go b/internal/firewall/interfaces.go index ed22d658..5d995ffd 100644 --- a/internal/firewall/interfaces.go +++ b/internal/firewall/interfaces.go @@ -28,6 +28,7 @@ type firewallImpl interface { //nolint:interfacebloat SaveAndRestore(ctx context.Context) (restore func(context.Context), err error) AcceptOutputPublicOnlyNewTraffic(ctx context.Context) error RejectOutputPublicTraffic(ctx context.Context, remove bool) error + DropOutputPublicTraffic(ctx context.Context, remove bool) error AcceptInputThroughInterface(ctx context.Context, intf string) error AcceptEstablishedRelatedTraffic(ctx context.Context) error AcceptInputToPort(ctx context.Context, intf string, port uint16, remove bool) error diff --git a/internal/firewall/iptables/iptables.go b/internal/firewall/iptables/iptables.go index 2d571351..e126acbd 100644 --- a/internal/firewall/iptables/iptables.go +++ b/internal/firewall/iptables/iptables.go @@ -208,6 +208,14 @@ func (c *Config) AcceptOutputPublicOnlyNewTraffic(ctx context.Context) error { } func (c *Config) RejectOutputPublicTraffic(ctx context.Context, remove bool) error { + return c.targetOutputPublicTraffic(ctx, "REJECT", remove) +} + +func (c *Config) DropOutputPublicTraffic(ctx context.Context, remove bool) error { + return c.targetOutputPublicTraffic(ctx, "DROP", remove) +} + +func (c *Config) targetOutputPublicTraffic(ctx context.Context, target string, remove bool) error { removeInstructions := []string{ "-D OUTPUT -j PUBLIC_ONLY", "-F PUBLIC_ONLY", @@ -223,11 +231,15 @@ func (c *Config) RejectOutputPublicTraffic(ctx context.Context, remove bool) err ipv6Instructions = append(ipv6Instructions, instruction) } - // Block UDP and ICMP, sending back ICMP port unreachable. - appendToBoth("-A PUBLIC_ONLY -m conntrack --ctstate RELATED,ESTABLISHED -j REJECT") - // Block TCP by sending back TCP RST packets. - appendToBoth("-A PUBLIC_ONLY -p tcp -m conntrack --ctstate RELATED,ESTABLISHED " + - "-j REJECT --reject-with tcp-reset") + if target == "REJECT" { + // Block TCP by sending back TCP RST packets. + appendToBoth("-A PUBLIC_ONLY -p tcp -m conntrack --ctstate RELATED,ESTABLISHED " + + "-j REJECT --reject-with tcp-reset") + // Block UDP and ICMP, sending back ICMP port unreachable. + appendToBoth("-A PUBLIC_ONLY -m conntrack --ctstate RELATED,ESTABLISHED -j REJECT") + } else { + appendToBoth("-A PUBLIC_ONLY -m conntrack --ctstate RELATED,ESTABLISHED -j " + target) + } appendToBoth("-I OUTPUT -j PUBLIC_ONLY") kernelErr := checkKernelModulesAreOK(c.modules.nfConntrack,