Reject output public ip traffic for 1s as another fallback

This commit is contained in:
Quentin McGaw
2026-02-26 18:04:23 +00:00
parent a37354426b
commit f654dece66
6 changed files with 118 additions and 25 deletions
+28
View File
@@ -4,7 +4,9 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"time"
"github.com/qdm12/gluetun/internal/firewall/iptables"
"github.com/qdm12/gluetun/internal/netlink" "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) c.logger.Debugf("falling back to marking and filtering unmarked packets because flush conntrack failed: %s", err)
err = c.impl.AcceptOutputPublicOnlyNewTraffic(ctx) err = c.impl.AcceptOutputPublicOnlyNewTraffic(ctx)
if err != nil { 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 fmt.Errorf("accepting only new output public traffic: %w", err)
} }
return nil return nil
@@ -25,3 +31,25 @@ func (c *Config) flushExistingConnections(ctx context.Context) error {
return fmt.Errorf("flushing conntrack: %w", err) 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
}
+1
View File
@@ -27,6 +27,7 @@ type Netlinker interface {
type firewallImpl interface { //nolint:interfacebloat type firewallImpl interface { //nolint:interfacebloat
SaveAndRestore(ctx context.Context) (restore func(context.Context), err error) SaveAndRestore(ctx context.Context) (restore func(context.Context), err error)
AcceptOutputPublicOnlyNewTraffic(ctx context.Context) error AcceptOutputPublicOnlyNewTraffic(ctx context.Context) error
RejectOutputPublicTraffic(ctx context.Context, remove bool) error
AcceptInputThroughInterface(ctx context.Context, intf string) error AcceptInputThroughInterface(ctx context.Context, intf string) error
AcceptEstablishedRelatedTraffic(ctx context.Context) error AcceptEstablishedRelatedTraffic(ctx context.Context) error
AcceptInputToPort(ctx context.Context, intf string, port uint16, remove bool) error AcceptInputToPort(ctx context.Context, intf string, port uint16, remove bool) error
+71 -21
View File
@@ -162,31 +162,12 @@ func (c *Config) AcceptOutputPublicOnlyNewTraffic(ctx context.Context) error {
return fmt.Errorf("checking kernel modules: %w", err) return fmt.Errorf("checking kernel modules: %w", err)
} }
ipv4PrivatePrefixes := []netip.Prefix{ ipv4Instructions, ipv6Instructions := makeCreatePublicIPChainInstructions()
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
appendToBoth := func(instruction string) { appendToBoth := func(instruction string) {
ipv4Instructions = append(ipv4Instructions, instruction) ipv4Instructions = append(ipv4Instructions, instruction)
ipv6Instructions = append(ipv6Instructions, 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 // Mark new connections with mark 0x567
appendToBoth("-A PUBLIC_ONLY -m conntrack --ctstate NEW -j CONNMARK --set-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 // 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 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, func (c *Config) AcceptOutputTrafficToVPN(ctx context.Context,
defaultInterface string, connection models.Connection, remove bool, defaultInterface string, connection models.Connection, remove bool,
) error { ) error {
+7 -3
View File
@@ -8,9 +8,11 @@ import (
) )
type kernelModules struct { type kernelModules struct {
nfConntrack kernelModule nfConntrack kernelModule
xtConnmark kernelModule nfRejectIPv4 kernelModule
xtConntrack kernelModule xtConnmark kernelModule
xtConntrack kernelModule
xtReject kernelModule
} }
type kernelModule struct { type kernelModule struct {
@@ -22,8 +24,10 @@ func newKernelModules() kernelModules {
var m kernelModules var m kernelModules
nameToFieldPtr := map[string]*kernelModule{ nameToFieldPtr := map[string]*kernelModule{
"nf_conntrack_netlink": &m.nfConntrack, "nf_conntrack_netlink": &m.nfConntrack,
"nf_reject_ipv4": &m.nfRejectIPv4,
"xt_connmark": &m.xtConnmark, "xt_connmark": &m.xtConnmark,
"xt_conntrack": &m.xtConntrack, "xt_conntrack": &m.xtConntrack,
"xt_reject": &m.xtReject,
} }
for name, fieldPtr := range nameToFieldPtr { for name, fieldPtr := range nameToFieldPtr {
fieldPtr.name = name fieldPtr.name = name
+5
View File
@@ -35,6 +35,7 @@ type chainRule struct {
mark mark mark mark
connMark mark connMark mark
setMark uint setMark uint
rejectWith string // for example "tcp-reset", only used for REJECT targets
} }
type mark struct { type mark struct {
@@ -295,6 +296,10 @@ func parseChainRuleOptionalFields(optionalFields []string, rule *chainRule) (err
} }
rule.mark = mark rule.mark = mark
i += consumed i += consumed
case "reject-with":
i++
rule.rejectWith = optionalFields[i] // for example "tcp-reset"
i++
case "connmark": case "connmark":
i++ i++
connMark, consumed, err := parseMark(optionalFields[i:]) connMark, consumed, err := parseMark(optionalFields[i:])
+6 -1
View File
@@ -36,7 +36,8 @@ type iptablesInstruction struct {
tcpFlags tcpFlags tcpFlags tcpFlags
mark mark mark mark
connMark 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() { func (i *iptablesInstruction) setDefaults() {
@@ -81,6 +82,8 @@ func (i *iptablesInstruction) equalToRule(table, chain string, rule chainRule) (
return false return false
case i.setMark != rule.setMark: case i.setMark != rule.setMark:
return false return false
case i.rejectWith != rule.rejectWith:
return false
default: default:
return true return true
} }
@@ -193,6 +196,8 @@ func parseInstructionFlag(fields []string, instruction *iptablesInstruction) (co
if err != nil { if err != nil {
return 0, fmt.Errorf("parsing TCP flags: %w", err) return 0, fmt.Errorf("parsing TCP flags: %w", err)
} }
case "--reject-with":
instruction.rejectWith = value // for example "tcp-reset"
default: default:
return 0, fmt.Errorf("%w: unknown key %q", ErrIptablesCommandMalformed, flag) return 0, fmt.Errorf("%w: unknown key %q", ErrIptablesCommandMalformed, flag)
} }