diff --git a/internal/firewall/flush.go b/internal/firewall/flush.go index 010927d1..0ef567ff 100644 --- a/internal/firewall/flush.go +++ b/internal/firewall/flush.go @@ -4,7 +4,9 @@ import ( "context" "errors" "fmt" + "time" + "github.com/qdm12/gluetun/internal/firewall/iptables" "github.com/qdm12/gluetun/internal/netlink" ) @@ -18,6 +20,10 @@ func (c *Config) flushExistingConnections(ctx context.Context) error { 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 @@ -25,3 +31,25 @@ func (c *Config) flushExistingConnections(ctx context.Context) error { return fmt.Errorf("flushing conntrack: %w", err) } } + +func (c *Config) rejectOutputTrafficTemporarily(ctx context.Context) error { + remove := false + err := c.impl.RejectOutputPublicTraffic(ctx, remove) + if err != nil { + return fmt.Errorf("rejecting only new output public traffic: %w", err) + } + timer := time.NewTimer(time.Second) + select { + case <-timer.C: + case <-ctx.Done(): + timer.Stop() + } + 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) + if err != nil { + return fmt.Errorf("reverting rejecting only new output public traffic: %w", err) + } + return nil +} diff --git a/internal/firewall/interfaces.go b/internal/firewall/interfaces.go index 74a2afc3..ed22d658 100644 --- a/internal/firewall/interfaces.go +++ b/internal/firewall/interfaces.go @@ -27,6 +27,7 @@ type Netlinker interface { 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 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 8b873804..cd6c3100 100644 --- a/internal/firewall/iptables/iptables.go +++ b/internal/firewall/iptables/iptables.go @@ -162,31 +162,12 @@ func (c *Config) AcceptOutputPublicOnlyNewTraffic(ctx context.Context) error { return fmt.Errorf("checking kernel modules: %w", err) } - ipv4PrivatePrefixes := []netip.Prefix{ - netip.MustParsePrefix("10.0.0.0/8"), - netip.MustParsePrefix("172.16.0.0/12"), - netip.MustParsePrefix("192.168.0.0/16"), - netip.MustParsePrefix("127.0.0.0/8"), - } - ipv6PrivatePrefixes := []netip.Prefix{ - netip.MustParsePrefix("fc00::/7"), - netip.MustParsePrefix("fe80::/10"), - netip.MustParsePrefix("::1/128"), - } - var ipv4Instructions, ipv6Instructions []string //nolint:prealloc + ipv4Instructions, ipv6Instructions := makeCreatePublicIPChainInstructions() appendToBoth := func(instruction string) { ipv4Instructions = append(ipv4Instructions, instruction) ipv6Instructions = append(ipv6Instructions, instruction) } - appendToBoth("-N PUBLIC_ONLY") - for _, prefix := range ipv4PrivatePrefixes { - ipv4Instructions = append(ipv4Instructions, fmt.Sprintf( - "-A PUBLIC_ONLY -d %s -j RETURN", prefix)) - } - for _, prefix := range ipv6PrivatePrefixes { - ipv6Instructions = append(ipv6Instructions, fmt.Sprintf( - "-A PUBLIC_ONLY -d %s -j RETURN", prefix)) - } + // Mark new connections with mark 0x567 appendToBoth("-A PUBLIC_ONLY -m conntrack --ctstate NEW -j CONNMARK --set-mark 0x567") // Drop related/established connections that made it through; marked connections would @@ -220,6 +201,75 @@ func (c *Config) AcceptOutputPublicOnlyNewTraffic(ctx context.Context) error { return nil } +func (c *Config) RejectOutputPublicTraffic(ctx context.Context, remove bool) error { + removeInstructions := []string{ + "-D OUTPUT -j PUBLIC_ONLY", + "-F PUBLIC_ONLY", + "-X PUBLIC_ONLY", + } + if remove { + return c.runMixedIptablesInstructions(ctx, removeInstructions) + } + + err := checkKernelModulesAreOK(c.modules.nfConntrack, c.modules.nfRejectIPv4, c.modules.xtReject) + if err != nil { + return fmt.Errorf("checking kernel modules: %w", err) + } + + ipv4Instructions, ipv6Instructions := makeCreatePublicIPChainInstructions() + appendToBoth := func(instruction string) { + ipv4Instructions = append(ipv4Instructions, instruction) + 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") + appendToBoth("-I OUTPUT -j PUBLIC_ONLY") + + err = c.runIptablesInstructions(ctx, ipv4Instructions) + if err != nil { + return err + } + err = c.runIP6tablesInstructions(ctx, ipv6Instructions) + if err != nil { + _ = c.runIptablesInstructions(ctx, removeInstructions) + return err + } + return nil +} + +func makeCreatePublicIPChainInstructions() (ipv4Instructions, ipv6Instructions []string) { + ipv4PrivatePrefixes := []netip.Prefix{ + netip.MustParsePrefix("10.0.0.0/8"), + netip.MustParsePrefix("172.16.0.0/12"), + netip.MustParsePrefix("192.168.0.0/16"), + netip.MustParsePrefix("127.0.0.0/8"), + } + ipv6PrivatePrefixes := []netip.Prefix{ + netip.MustParsePrefix("fc00::/7"), + netip.MustParsePrefix("fe80::/10"), + netip.MustParsePrefix("::1/128"), + } + + ipv4Instructions = append(ipv4Instructions, "-N PUBLIC_ONLY") + ipv6Instructions = append(ipv6Instructions, "-N PUBLIC_ONLY") + + for _, prefix := range ipv4PrivatePrefixes { + ipv4Instructions = append(ipv4Instructions, fmt.Sprintf( + "-A PUBLIC_ONLY -d %s -j RETURN", prefix)) + } + + for _, prefix := range ipv6PrivatePrefixes { + ipv6Instructions = append(ipv6Instructions, fmt.Sprintf( + "-A PUBLIC_ONLY -d %s -j RETURN", prefix)) + } + + return ipv4Instructions, ipv6Instructions +} + func (c *Config) AcceptOutputTrafficToVPN(ctx context.Context, defaultInterface string, connection models.Connection, remove bool, ) error { diff --git a/internal/firewall/iptables/kernel.go b/internal/firewall/iptables/kernel.go index 5b506e42..60e08514 100644 --- a/internal/firewall/iptables/kernel.go +++ b/internal/firewall/iptables/kernel.go @@ -8,9 +8,11 @@ import ( ) type kernelModules struct { - nfConntrack kernelModule - xtConnmark kernelModule - xtConntrack kernelModule + nfConntrack kernelModule + nfRejectIPv4 kernelModule + xtConnmark kernelModule + xtConntrack kernelModule + xtReject kernelModule } type kernelModule struct { @@ -22,8 +24,10 @@ func newKernelModules() kernelModules { var m kernelModules nameToFieldPtr := map[string]*kernelModule{ "nf_conntrack_netlink": &m.nfConntrack, + "nf_reject_ipv4": &m.nfRejectIPv4, "xt_connmark": &m.xtConnmark, "xt_conntrack": &m.xtConntrack, + "xt_reject": &m.xtReject, } for name, fieldPtr := range nameToFieldPtr { fieldPtr.name = name diff --git a/internal/firewall/iptables/list.go b/internal/firewall/iptables/list.go index 38d918c5..c6f8168e 100644 --- a/internal/firewall/iptables/list.go +++ b/internal/firewall/iptables/list.go @@ -35,6 +35,7 @@ type chainRule struct { mark mark connMark mark setMark uint + rejectWith string // for example "tcp-reset", only used for REJECT targets } type mark struct { @@ -295,6 +296,10 @@ func parseChainRuleOptionalFields(optionalFields []string, rule *chainRule) (err } rule.mark = mark i += consumed + case "reject-with": + i++ + rule.rejectWith = optionalFields[i] // for example "tcp-reset" + i++ case "connmark": i++ connMark, consumed, err := parseMark(optionalFields[i:]) diff --git a/internal/firewall/iptables/parse.go b/internal/firewall/iptables/parse.go index fda21aaf..eb3a1923 100644 --- a/internal/firewall/iptables/parse.go +++ b/internal/firewall/iptables/parse.go @@ -36,7 +36,8 @@ type iptablesInstruction struct { tcpFlags tcpFlags mark mark connMark mark - setMark uint // only used for jump CONNMARK --set-mark + setMark uint // only used for jump CONNMARK --set-mark + rejectWith string // only used for REJECT targets } func (i *iptablesInstruction) setDefaults() { @@ -81,6 +82,8 @@ func (i *iptablesInstruction) equalToRule(table, chain string, rule chainRule) ( return false case i.setMark != rule.setMark: return false + case i.rejectWith != rule.rejectWith: + return false default: return true } @@ -193,6 +196,8 @@ func parseInstructionFlag(fields []string, instruction *iptablesInstruction) (co if err != nil { return 0, fmt.Errorf("parsing TCP flags: %w", err) } + case "--reject-with": + instruction.rejectWith = value // for example "tcp-reset" default: return 0, fmt.Errorf("%w: unknown key %q", ErrIptablesCommandMalformed, flag) }