diff --git a/cmd/gluetun/main.go b/cmd/gluetun/main.go index c222a6327..49b19cc72 100644 --- a/cmd/gluetun/main.go +++ b/cmd/gluetun/main.go @@ -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 diff --git a/internal/firewall/enable.go b/internal/firewall/enable.go index c0f9aa8d4..71fa67c32 100644 --- a/internal/firewall/enable.go +++ b/internal/firewall/enable.go @@ -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 } diff --git a/internal/firewall/firewall.go b/internal/firewall/firewall.go index 3b9b902a9..aba688317 100644 --- a/internal/firewall/firewall.go +++ b/internal/firewall/firewall.go @@ -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 diff --git a/internal/firewall/flush.go b/internal/firewall/flush.go new file mode 100644 index 000000000..f09bc6a20 --- /dev/null +++ b/internal/firewall/flush.go @@ -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 +} diff --git a/internal/firewall/interfaces.go b/internal/firewall/interfaces.go index a1938f96e..5d995ffd8 100644 --- a/internal/firewall/interfaces.go +++ b/internal/firewall/interfaces.go @@ -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 diff --git a/internal/firewall/iptables/firewall.go b/internal/firewall/iptables/firewall.go index aeedae63b..3cb7c285f 100644 --- a/internal/firewall/iptables/firewall.go +++ b/internal/firewall/iptables/firewall.go @@ -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 diff --git a/internal/firewall/iptables/ip6tables.go b/internal/firewall/iptables/ip6tables.go index 6e0966993..19ef233e1 100644 --- a/internal/firewall/iptables/ip6tables.go +++ b/internal/firewall/iptables/ip6tables.go @@ -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) } diff --git a/internal/firewall/iptables/iptables.go b/internal/firewall/iptables/iptables.go index 486e9fab3..4a42e9a7f 100644 --- a/internal/firewall/iptables/iptables.go +++ b/internal/firewall/iptables/iptables.go @@ -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 { diff --git a/internal/firewall/iptables/list.go b/internal/firewall/iptables/list.go index 49f855fe4..8c9a8b732 100644 --- a/internal/firewall/iptables/list.go +++ b/internal/firewall/iptables/list.go @@ -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", diff --git a/internal/firewall/iptables/parse.go b/internal/firewall/iptables/parse.go index a18d7714b..eb3a1923a 100644 --- a/internal/firewall/iptables/parse.go +++ b/internal/firewall/iptables/parse.go @@ -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]) diff --git a/internal/firewall/iptables/parse_test.go b/internal/firewall/iptables/parse_test.go index 51c5ab7cc..cdf66b836 100644 --- a/internal/firewall/iptables/parse_test.go +++ b/internal/firewall/iptables/parse_test.go @@ -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, diff --git a/internal/firewall/iptables/tcp.go b/internal/firewall/iptables/tcp.go index 77e5c5f28..99dbde364 100644 --- a/internal/firewall/iptables/tcp.go +++ b/internal/firewall/iptables/tcp.go @@ -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. diff --git a/internal/netlink/conntrack_linux.go b/internal/netlink/conntrack_linux.go index 9ece2722b..6b69a2c22 100644 --- a/internal/netlink/conntrack_linux.go +++ b/internal/netlink/conntrack_linux.go @@ -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 } diff --git a/internal/netlink/conntrack_unspecified.go b/internal/netlink/conntrack_unspecified.go index d2652b267..5ed2a24ac 100644 --- a/internal/netlink/conntrack_unspecified.go +++ b/internal/netlink/conntrack_unspecified.go @@ -2,6 +2,10 @@ package netlink +import "errors" + +var ErrConntrackNetlinkNotSupported = errors.New("error not implemented") + func (n *NetLink) FlushConntrack() error { panic("not implemented") } diff --git a/internal/netlink/netlink.go b/internal/netlink/netlink.go index 9a26ab08d..bc2950826 100644 --- a/internal/netlink/netlink.go +++ b/internal/netlink/netlink.go @@ -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, } } diff --git a/internal/pmtud/pmtud.go b/internal/pmtud/pmtud.go index 4505c0e96..63fe6715a 100644 --- a/internal/pmtud/pmtud.go +++ b/internal/pmtud/pmtud.go @@ -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 diff --git a/internal/pmtud/tcp/helpers_test.go b/internal/pmtud/tcp/helpers_test.go index e5a21e059..e0ad925d6 100644 --- a/internal/pmtud/tcp/helpers_test.go +++ b/internal/pmtud/tcp/helpers_test.go @@ -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") } diff --git a/internal/pmtud/tcp/mss.go b/internal/pmtud/tcp/mss.go index bedc2a5d9..f92467aa0 100644 --- a/internal/pmtud/tcp/mss.go +++ b/internal/pmtud/tcp/mss.go @@ -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