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"
"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
}
+1
View File
@@ -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
+71 -21
View File
@@ -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 {
+7 -3
View File
@@ -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
+5
View File
@@ -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:])
+6 -1
View File
@@ -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)
}