Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions cmd/gluetun/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
firewallLogger.Patch(log.SetLevel(log.LevelDebug))
}
firewallConf, err := firewall.NewConfig(ctx, firewallLogger, cmder,
defaultRoutes, localNetworks)
netLinker, defaultRoutes, localNetworks)
if err != nil {
return err
}
Expand All @@ -237,10 +237,6 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
if err != nil {
return err
}
err = netLinker.FlushConntrack()
if err != nil {
logger.Warnf("flushing conntrack failed: %s", err)
}
}

// TODO run this in a loop or in openvpn to reload from file without restarting
Expand Down
5 changes: 5 additions & 0 deletions internal/firewall/enable.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ func (c *Config) enable(ctx context.Context) (err error) {
return err
}

err = c.flushExistingConnections(ctx)
if err != nil {
return fmt.Errorf("flushing existing connections: %w", err)
}

if err = c.impl.AcceptEstablishedRelatedTraffic(ctx); err != nil {
return err
}
Expand Down
6 changes: 4 additions & 2 deletions internal/firewall/firewall.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

type Config struct {
runner CmdRunner
netlinker Netlinker
logger Logger
defaultRoutes []routing.DefaultRoute
localNetworks []routing.LocalNetwork
Expand All @@ -35,8 +36,8 @@ type Config struct {
// NewConfig creates a new Config instance and returns an error
// if no iptables implementation is available.
func NewConfig(ctx context.Context, logger Logger,
runner CmdRunner, defaultRoutes []routing.DefaultRoute,
localNetworks []routing.LocalNetwork,
runner CmdRunner, netlinker Netlinker,
defaultRoutes []routing.DefaultRoute, localNetworks []routing.LocalNetwork,
) (config *Config, err error) {
impl, err := iptables.New(ctx, runner, logger)
if err != nil {
Expand All @@ -45,6 +46,7 @@ func NewConfig(ctx context.Context, logger Logger,

return &Config{
runner: runner,
netlinker: netlinker,
logger: logger,
allowedInputPorts: make(map[uint16]map[string]struct{}),
// Obtained from routing
Expand Down
74 changes: 74 additions & 0 deletions internal/firewall/flush.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package firewall

import (
"context"
"errors"
"fmt"
"time"

"github.com/qdm12/gluetun/internal/firewall/iptables"
"github.com/qdm12/gluetun/internal/netlink"
)

func (c *Config) flushExistingConnections(ctx context.Context) error {
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 := f(ctx, remove)
if err != nil {
return fmt.Errorf("setting up: %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 = f(context.Background(), remove)
if err != nil {
return fmt.Errorf("reverting: %w", err)
}
return nil
}
10 changes: 9 additions & 1 deletion internal/firewall/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,23 @@ type CmdRunner interface {

type Logger interface {
Debug(s string)
Debugf(format string, args ...any)
Info(s string)
Warn(s string)
Error(s string)
}

type Netlinker interface {
FlushConntrack() error
}

type firewallImpl interface { //nolint:interfacebloat
SaveAndRestore(ctx context.Context) (restore func(context.Context), err error)
AcceptEstablishedRelatedTraffic(ctx context.Context) 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
AcceptInputToSubnet(ctx context.Context, intf string, subnet netip.Prefix) error
AcceptIpv6MulticastOutput(ctx context.Context, intf string) error
Expand Down
3 changes: 3 additions & 0 deletions internal/firewall/iptables/firewall.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ package iptables

import (
"context"
"errors"
"sync"
)

var ErrKernelModuleMissing = errors.New("kernel module is missing for this operation")

type Config struct {
runner CmdRunner
logger Logger
Expand Down
3 changes: 3 additions & 0 deletions internal/firewall/iptables/ip6tables.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ func (c *Config) runIP6tablesInstructionNoSave(ctx context.Context, instruction
cmd := exec.CommandContext(ctx, c.ip6Tables, flags...) // #nosec G204
c.logger.Debug(cmd.String())
if output, err := c.runner.Run(cmd); err != nil {
if strings.Contains(output, "missing kernel module") {
err = ErrKernelModuleMissing
}
return fmt.Errorf("command failed: \"%s %s\": %s: %w",
c.ip6Tables, instruction, output, err)
}
Expand Down
133 changes: 133 additions & 0 deletions internal/firewall/iptables/iptables.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ func (c *Config) runIptablesInstructionNoSave(ctx context.Context, instruction s
cmd := exec.CommandContext(ctx, c.ipTables, flags...) // #nosec G204
c.logger.Debug(cmd.String())
if output, err := c.runner.Run(cmd); err != nil {
if strings.Contains(output, "missing kernel module") {
err = ErrKernelModuleMissing
}
return fmt.Errorf("command failed: \"%s %s\": %s: %w",
c.ipTables, instruction, output, err)
}
Expand Down Expand Up @@ -147,6 +150,136 @@ func (c *Config) AcceptEstablishedRelatedTraffic(ctx context.Context) error {
})
}

// AcceptOutputPublicOnlyNewTraffic adds rules to mark new output connections, and to accept
// established or related packets with this mark only. This effectively forces
// previously established or related traffic to be blocked.
// If remove is true, the rules are removed instead of appended.
// If the relevant kernel modules are not available, it returns an error indicating
// which kernel module is missing.
func (c *Config) AcceptOutputPublicOnlyNewTraffic(ctx context.Context) error {
ipv4Instructions, ipv6Instructions := makeCreatePublicIPChainInstructions()
appendToBoth := func(instruction string) {
ipv4Instructions = append(ipv4Instructions, instruction)
ipv6Instructions = append(ipv6Instructions, instruction)
}

// 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
// be directly accepted by the first rule in the OUTPUT chain (see below)
appendToBoth("-A PUBLIC_ONLY -m conntrack --ctstate RELATED,ESTABLISHED -j DROP")
// Set the PUBLIC_ONLY chain as the second rule in the OUTPUT chain, so that it is evaluated
// after the accept rule below, for performance reasons.
appendToBoth("-I OUTPUT -j PUBLIC_ONLY")
appendToBoth("-I OUTPUT -m conntrack --ctstate RELATED,ESTABLISHED -m connmark --mark 0x567 -j ACCEPT")

c.iptablesMutex.Lock()
c.ip6tablesMutex.Lock()
defer c.iptablesMutex.Unlock()
defer c.ip6tablesMutex.Unlock()

restore, err := c.saveAndRestore(ctx)
if err != nil {
return err
}

err = c.runIptablesInstructionsNoSave(ctx, ipv4Instructions)
if err != nil {
restore(ctx)
return err
}
err = c.runIP6tablesInstructionsNoSave(ctx, ipv6Instructions)
if err != nil {
restore(ctx)
return err
}

return nil
}

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",
"-X PUBLIC_ONLY",
}
if remove {
return c.runMixedIptablesInstructions(ctx, removeInstructions)
}

ipv4Instructions, ipv6Instructions := makeCreatePublicIPChainInstructions()
appendToBoth := func(instruction string) {
ipv4Instructions = append(ipv4Instructions, instruction)
ipv6Instructions = append(ipv6Instructions, instruction)
}

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")

err := c.runIptablesInstructions(ctx, ipv4Instructions)
if err != nil {
if strings.Contains(err.Error(), " support") {
return fmt.Errorf("%w: %w", ErrKernelModuleMissing, err)
}
}

err = c.runIP6tablesInstructions(ctx, ipv6Instructions)
if err != nil {
_ = c.runIptablesInstructions(ctx, removeInstructions)
if strings.Contains(err.Error(), " support") {
return fmt.Errorf("%w: %w", ErrKernelModuleMissing, err)
}
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 {
Expand Down
Loading
Loading