mirror of
https://github.com/qdm12/gluetun.git
synced 2026-06-27 14:27:29 +02:00
Compare commits
12 Commits
52a41cb891
...
conntrack
| Author | SHA1 | Date | |
|---|---|---|---|
| 27b8e83aa5 | |||
| eb9f1b4e36 | |||
| a62220d7b6 | |||
| 594b1db98b | |||
| bfc8136bc9 | |||
| 1fd4cc511a | |||
| af0bc3e224 | |||
| 302f1f11f7 | |||
| f654dece66 | |||
| a37354426b | |||
| dfac2b2f1a | |||
| 6467f3b4ad |
+1
-5
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
type Config struct {
|
||||
runner CmdRunner
|
||||
netlinker Netlinker
|
||||
logger Logger
|
||||
defaultRoutes []routing.DefaultRoute
|
||||
localNetworks []routing.LocalNetwork
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -33,6 +33,9 @@ type chainRule struct {
|
||||
ctstate []string // for example ["RELATED","ESTABLISHED"]. Can be empty.
|
||||
tcpFlags tcpFlags
|
||||
mark mark
|
||||
connMark mark
|
||||
setMark uint
|
||||
rejectWith string // for example "tcp-reset", only used for REJECT targets
|
||||
}
|
||||
|
||||
type mark struct {
|
||||
@@ -219,10 +222,6 @@ func parseChainRuleField(fieldIndex int, field string, rule *chainRule) (err err
|
||||
return fmt.Errorf("parsing bytes: %w", err)
|
||||
}
|
||||
case targetIndex:
|
||||
err = checkTarget(field)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking target: %w", err)
|
||||
}
|
||||
rule.target = field
|
||||
case protocolIndex:
|
||||
rule.protocol, err = parseProtocol(field)
|
||||
@@ -293,6 +292,33 @@ 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:])
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing connmark: %w", err)
|
||||
}
|
||||
rule.connMark = connMark
|
||||
i += consumed
|
||||
case "CONNMARK":
|
||||
i++
|
||||
switch optionalFields[i] {
|
||||
case "set":
|
||||
i++
|
||||
value, err := parseAny32bNumber(optionalFields[i])
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing CONNMARK set value: %w", err)
|
||||
}
|
||||
rule.setMark = value
|
||||
i++
|
||||
default:
|
||||
return fmt.Errorf("%w: unexpected %q after CONNMARK",
|
||||
ErrChainRuleMalformed, optionalFields[i])
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("%w: unexpected optional field: %s",
|
||||
ErrChainRuleMalformed, optionalFields[i])
|
||||
@@ -422,8 +448,6 @@ func parsePortsCSV(s string) (ports []uint16, err error) {
|
||||
return ports, nil
|
||||
}
|
||||
|
||||
var errMarkValueMalformed = errors.New("mark value is malformed")
|
||||
|
||||
func parseMark(optionalFields []string) (m mark, consumed int, err error) {
|
||||
switch optionalFields[consumed] {
|
||||
case "match":
|
||||
@@ -433,13 +457,11 @@ func parseMark(optionalFields []string) (m mark, consumed int, err error) {
|
||||
consumed++
|
||||
}
|
||||
|
||||
const base = 0 // auto-detect
|
||||
const bits = 32
|
||||
value, err := strconv.ParseUint(optionalFields[consumed], base, bits)
|
||||
value, err := parseAny32bNumber(optionalFields[consumed])
|
||||
if err != nil {
|
||||
return mark{}, 0, fmt.Errorf("%w: %s", errMarkValueMalformed, optionalFields[consumed])
|
||||
return mark{}, 0, fmt.Errorf("value malformed: %w", err)
|
||||
}
|
||||
m.value = uint(value)
|
||||
m.value = value
|
||||
consumed++
|
||||
default:
|
||||
return mark{}, 0, fmt.Errorf("%w: unexpected mark mode field: %s",
|
||||
|
||||
@@ -9,9 +9,19 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type operation uint8
|
||||
|
||||
const (
|
||||
opNone operation = iota
|
||||
opAppend
|
||||
opDelete
|
||||
opInsert
|
||||
opReplace
|
||||
)
|
||||
|
||||
type iptablesInstruction struct {
|
||||
table string // defaults to "filter", and can be "nat" for example.
|
||||
append bool
|
||||
operation operation
|
||||
chain string // for example INPUT, PREROUTING. Cannot be empty.
|
||||
target string // for example ACCEPT. Can be empty.
|
||||
protocol string // "tcp" or "udp" or "" for all protocols.
|
||||
@@ -25,6 +35,9 @@ type iptablesInstruction struct {
|
||||
ctstate []string // if empty, there is no ctstate
|
||||
tcpFlags tcpFlags
|
||||
mark mark
|
||||
connMark mark
|
||||
setMark uint // only used for jump CONNMARK --set-mark
|
||||
rejectWith string // only used for REJECT targets
|
||||
}
|
||||
|
||||
func (i *iptablesInstruction) setDefaults() {
|
||||
@@ -65,6 +78,12 @@ func (i *iptablesInstruction) equalToRule(table, chain string, rule chainRule) (
|
||||
return false
|
||||
case i.mark != rule.mark:
|
||||
return false
|
||||
case i.connMark != rule.connMark:
|
||||
return false
|
||||
case i.setMark != rule.setMark:
|
||||
return false
|
||||
case i.rejectWith != rule.rejectWith:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
@@ -113,13 +132,20 @@ func parseInstructionFlag(fields []string, instruction *iptablesInstruction) (co
|
||||
case "-t", "--table":
|
||||
instruction.table = value
|
||||
case "-D", "--delete":
|
||||
instruction.append = false
|
||||
instruction.operation = opDelete
|
||||
instruction.chain = value
|
||||
case "-A", "--append":
|
||||
instruction.append = true
|
||||
instruction.operation = opAppend
|
||||
instruction.chain = value
|
||||
case "-I", "--insert":
|
||||
instruction.operation = opInsert
|
||||
instruction.chain = value
|
||||
case "-j", "--jump":
|
||||
instruction.target = value
|
||||
subConsumed, err := parseJumpFlag(fields[1:], instruction)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("parsing jump flag: %w", err)
|
||||
}
|
||||
consumed += subConsumed
|
||||
case "-p", "--protocol":
|
||||
instruction.protocol = value
|
||||
case "-m", "--match":
|
||||
@@ -128,13 +154,11 @@ func parseInstructionFlag(fields []string, instruction *iptablesInstruction) (co
|
||||
return 0, fmt.Errorf("parsing match module: %w", err)
|
||||
}
|
||||
case "--mark":
|
||||
const base = 0 // auto-detect
|
||||
const bits = 32
|
||||
value, err := strconv.ParseUint(value, base, bits)
|
||||
n, err := parseAny32bNumber(value)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("parsing mark value %q: %w", fields[2], err)
|
||||
return 0, fmt.Errorf("parsing mark value %q: %w", value, err)
|
||||
}
|
||||
instruction.mark.value = uint(value)
|
||||
instruction.mark.value = n
|
||||
case "-i", "--in-interface":
|
||||
instruction.inputInterface = value
|
||||
case "-o", "--out-interface":
|
||||
@@ -172,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)
|
||||
}
|
||||
@@ -182,7 +208,7 @@ func preCheckInstructionFields(fields []string) (consumed int, err error) {
|
||||
flag := fields[0]
|
||||
// All flags use one value after the flag, except the following:
|
||||
switch flag {
|
||||
case "--tcp-flags": // -m can have 1 or 2 values
|
||||
case "--tcp-flags":
|
||||
const expected = 3
|
||||
if len(fields) < expected {
|
||||
return 0, fmt.Errorf("%w: flag %q requires at least 2 values, but got %s",
|
||||
@@ -199,6 +225,34 @@ func preCheckInstructionFields(fields []string) (consumed int, err error) {
|
||||
}
|
||||
}
|
||||
|
||||
func parseJumpFlag(fields []string, instruction *iptablesInstruction) (consumed int, err error) {
|
||||
instruction.target = fields[0]
|
||||
// consumed in the caller already takes fields[0] into account
|
||||
if instruction.target != "CONNMARK" {
|
||||
return consumed, nil
|
||||
}
|
||||
// consumed already accounts for the "CONNMARK" value
|
||||
const expectedFields = 3
|
||||
if len(fields) < expectedFields {
|
||||
return 0, fmt.Errorf("%w: jump CONNMARK requires at least two additional values",
|
||||
ErrIptablesCommandMalformed)
|
||||
}
|
||||
switch fields[1] {
|
||||
case "--set-mark":
|
||||
n, err := parseAny32bNumber(fields[2])
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("parsing connmark mark value %q: %w", fields[2], err)
|
||||
}
|
||||
consumed++
|
||||
instruction.setMark = n
|
||||
default:
|
||||
return consumed, fmt.Errorf("%w: unsupported jump CONNMARK with value: %s",
|
||||
ErrIptablesCommandMalformed, fields[1])
|
||||
}
|
||||
consumed++
|
||||
return consumed, nil
|
||||
}
|
||||
|
||||
func parseIPPrefix(value string) (prefix netip.Prefix, err error) {
|
||||
slashIndex := strings.Index(value, "/")
|
||||
if slashIndex >= 0 {
|
||||
@@ -221,6 +275,13 @@ func parsePort(value string) (port uint16, err error) {
|
||||
return uint16(portValue), nil
|
||||
}
|
||||
|
||||
func parseAny32bNumber(mark string) (value uint, err error) {
|
||||
const base = 0 // auto-detect
|
||||
const bits = 32
|
||||
n, err := strconv.ParseUint(mark, base, bits)
|
||||
return uint(n), err
|
||||
}
|
||||
|
||||
func parseMatchModule(fields []string, instruction *iptablesInstruction) (
|
||||
consumed int, err error,
|
||||
) {
|
||||
@@ -234,14 +295,30 @@ func parseMatchModule(fields []string, instruction *iptablesInstruction) (
|
||||
// parse it twice.
|
||||
case "mark":
|
||||
consumed++
|
||||
switch fields[consumed] {
|
||||
case "!":
|
||||
switch {
|
||||
case len(fields[consumed:]) == 0 || strings.HasPrefix(fields[consumed], "-"):
|
||||
// end or another flag
|
||||
return consumed, nil
|
||||
case fields[consumed] == "!":
|
||||
consumed++
|
||||
instruction.mark.invert = true
|
||||
default:
|
||||
return consumed, fmt.Errorf("%w: unsupported match mark with value: %s",
|
||||
ErrIptablesCommandMalformed, fields[2])
|
||||
}
|
||||
case "connmark":
|
||||
consumed++
|
||||
switch {
|
||||
case len(fields[consumed:]) == 0 || strings.HasPrefix(fields[consumed], "-"):
|
||||
// end or another flag
|
||||
return consumed, nil
|
||||
case fields[consumed] == "!":
|
||||
consumed++
|
||||
instruction.connMark.invert = true
|
||||
default:
|
||||
return consumed, fmt.Errorf("%w: unsupported match connmark with value: %s",
|
||||
ErrIptablesCommandMalformed, fields[2])
|
||||
}
|
||||
default:
|
||||
return 0, fmt.Errorf("%w: unknown match value: %s",
|
||||
ErrIptablesCommandMalformed, fields[consumed])
|
||||
|
||||
@@ -33,9 +33,9 @@ func Test_parseIptablesInstruction(t *testing.T) {
|
||||
"one_pair": {
|
||||
s: "-A INPUT",
|
||||
instruction: iptablesInstruction{
|
||||
table: "filter",
|
||||
chain: "INPUT",
|
||||
append: true,
|
||||
table: "filter",
|
||||
chain: "INPUT",
|
||||
operation: opAppend,
|
||||
},
|
||||
},
|
||||
"instruction_A": {
|
||||
@@ -43,7 +43,7 @@ func Test_parseIptablesInstruction(t *testing.T) {
|
||||
instruction: iptablesInstruction{
|
||||
table: "filter",
|
||||
chain: "INPUT",
|
||||
append: true,
|
||||
operation: opAppend,
|
||||
inputInterface: "tun0",
|
||||
protocol: "tcp",
|
||||
source: netip.MustParsePrefix("1.2.3.4/32"),
|
||||
@@ -57,7 +57,7 @@ func Test_parseIptablesInstruction(t *testing.T) {
|
||||
instruction: iptablesInstruction{
|
||||
table: "nat",
|
||||
chain: "PREROUTING",
|
||||
append: false,
|
||||
operation: opDelete,
|
||||
inputInterface: "tun0",
|
||||
protocol: "tcp",
|
||||
destinationPort: 43716,
|
||||
|
||||
@@ -64,7 +64,7 @@ func parseTCPFlag(s string) (tcpFlag, error) {
|
||||
return 0, fmt.Errorf("%w: %s", errTCPFlagUnknown, s)
|
||||
}
|
||||
|
||||
var ErrMarkMatchModuleMissing = errors.New("kernel is missing the mark module libxt_mark.so")
|
||||
var ErrMarkMatchModuleMissing = errors.New("libxt_mark.so module is missing")
|
||||
|
||||
// TempDropOutputTCPRST temporarily drops outgoing TCP RST packets to the specified address and port,
|
||||
// for any TCP packets not marked with the excludeMark given.
|
||||
|
||||
@@ -1,38 +1,44 @@
|
||||
package netlink
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/mdlayher/netlink"
|
||||
"github.com/ti-mo/netfilter"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
var ErrConntrackNetlinkNotSupported = errors.New("nf_conntrack_netlink is not supported by the kernel")
|
||||
|
||||
func (n *NetLink) FlushConntrack() error {
|
||||
conn, err := netfilter.Dial(nil)
|
||||
if err != nil {
|
||||
if !n.conntrackNetlink {
|
||||
err = fmt.Errorf("%w: %w", err, ErrConntrackNetlinkNotSupported)
|
||||
}
|
||||
return fmt.Errorf("dialing netfilter: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
families := [...]netfilter.ProtoFamily{netfilter.ProtoIPv4, netfilter.ProtoIPv6}
|
||||
for _, family := range families {
|
||||
const IPCtnlMsgCtDelete = 2
|
||||
request, err := netfilter.MarshalNetlink(
|
||||
netfilter.Header{
|
||||
SubsystemID: netfilter.NFSubsysCTNetlink,
|
||||
MessageType: netfilter.MessageType(IPCtnlMsgCtDelete),
|
||||
Family: family,
|
||||
Flags: netlink.Request | netlink.Acknowledge,
|
||||
},
|
||||
nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encoding netlink request: %w", err)
|
||||
}
|
||||
const ipCtnlMsgCtDelete = netfilter.MessageType(2)
|
||||
header := netfilter.Header{
|
||||
SubsystemID: netfilter.NFSubsysCTNetlink,
|
||||
MessageType: ipCtnlMsgCtDelete,
|
||||
Family: unix.AF_UNSPEC,
|
||||
Flags: netlink.Request | netlink.Acknowledge,
|
||||
}
|
||||
request, err := netfilter.MarshalNetlink(header, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encoding netlink request: %w", err)
|
||||
}
|
||||
|
||||
_, err = conn.Query(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("querying netlink request: %w", err)
|
||||
_, err = conn.Query(request)
|
||||
if err != nil {
|
||||
if !n.conntrackNetlink {
|
||||
err = fmt.Errorf("%w: %w", err, ErrConntrackNetlinkNotSupported)
|
||||
}
|
||||
return fmt.Errorf("querying netlink request: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
package netlink
|
||||
|
||||
import "errors"
|
||||
|
||||
var ErrConntrackNetlinkNotSupported = errors.New("error not implemented")
|
||||
|
||||
func (n *NetLink) FlushConntrack() error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
package netlink
|
||||
|
||||
import "github.com/qdm12/log"
|
||||
import (
|
||||
"github.com/qdm12/gluetun/internal/mod"
|
||||
"github.com/qdm12/log"
|
||||
)
|
||||
|
||||
type NetLink struct {
|
||||
debugLogger DebugLogger
|
||||
|
||||
// Fixed state
|
||||
conntrackNetlink bool
|
||||
}
|
||||
|
||||
func New(debugLogger DebugLogger) *NetLink {
|
||||
conntrackNetlink := mod.Probe("nf_conntrack_netlink") == nil
|
||||
return &NetLink{
|
||||
debugLogger: debugLogger,
|
||||
debugLogger: debugLogger,
|
||||
conntrackNetlink: conntrackNetlink,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ func PathMTUDiscover(ctx context.Context, icmpAddrs []netip.Addr, tcpAddrs []net
|
||||
}
|
||||
mtu, err = tcp.PathMTUDiscover(ctx, tcpAddrs, minMTU, maxPossibleMTU, tryTimeout, fw, logger)
|
||||
if err != nil {
|
||||
if errors.Is(err, iptables.ErrMarkMatchModuleMissing) {
|
||||
if errors.Is(err, iptables.ErrKernelModuleMissing) {
|
||||
logger.Debugf("aborting TCP path MTU discovery: %s", err)
|
||||
if icmpSuccess {
|
||||
return maxPossibleMTU, nil // only rely on ICMP PMTUD results
|
||||
|
||||
@@ -35,7 +35,7 @@ func getFirewall(t *testing.T) *firewall.Config {
|
||||
noopLogger := &noopLogger{}
|
||||
cmder := command.New()
|
||||
var err error
|
||||
testFirewall, err = firewall.NewConfig(t.Context(), noopLogger, cmder, nil, nil)
|
||||
testFirewall, err = firewall.NewConfig(t.Context(), noopLogger, cmder, nil, nil, nil)
|
||||
if errors.Is(err, iptables.ErrNotSupported) {
|
||||
t.Skip("iptables not installed, skipping TCP PMTUD tests")
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ func findHighestMSSDestination(ctx context.Context, familyToFD map[int]fileDescr
|
||||
if result.err != nil {
|
||||
switch {
|
||||
case err != nil: // error already occurred for another findMSS goroutine
|
||||
case errors.Is(result.err, iptables.ErrMarkMatchModuleMissing):
|
||||
case errors.Is(result.err, iptables.ErrKernelModuleMissing):
|
||||
err = fmt.Errorf("finding MSS for %s: %w", result.dst, result.err)
|
||||
case dst.Addr().Is6() && errors.Is(result.err, ip.ErrNetworkUnreachable):
|
||||
// silently discard IPv6 network unreachable errors since they are common
|
||||
|
||||
Reference in New Issue
Block a user