diff --git a/Dockerfile b/Dockerfile index 1e8ef556..a93b5d60 100644 --- a/Dockerfile +++ b/Dockerfile @@ -165,7 +165,7 @@ ENV VPN_SERVICE_PROVIDER=pia \ FIREWALL_VPN_INPUT_PORTS= \ FIREWALL_INPUT_PORTS= \ FIREWALL_OUTBOUND_SUBNETS= \ - FIREWALL_IMPLEMENTATION=iptables \ + FIREWALL_IMPLEMENTATION=auto \ FIREWALL_DEBUG=off \ # Logging LOG_LEVEL=info \ diff --git a/internal/configuration/settings/firewall.go b/internal/configuration/settings/firewall.go index c609a694..f420fff5 100644 --- a/internal/configuration/settings/firewall.go +++ b/internal/configuration/settings/firewall.go @@ -4,9 +4,9 @@ import ( "fmt" "net/netip" - "github.com/qdm12/gluetun/internal/configuration/settings/helpers" "github.com/qdm12/gosettings" "github.com/qdm12/gosettings/reader" + "github.com/qdm12/gosettings/validate" "github.com/qdm12/gotree" ) @@ -35,8 +35,9 @@ func (f Firewall) validate() (err error) { } } - if !helpers.IsOneOf(f.Implementation, "iptables", "nftables") { - return fmt.Errorf("firewall implementation %q must be either 'iptables' or 'nftables'", f.Implementation) + err = validate.IsOneOf(f.Implementation, "auto", "iptables", "nftables") + if err != nil { + return fmt.Errorf("firewall implementation: %w", err) } return nil diff --git a/internal/firewall/firewall.go b/internal/firewall/firewall.go index 50348f8d..bb7b0b0b 100644 --- a/internal/firewall/firewall.go +++ b/internal/firewall/firewall.go @@ -41,8 +41,9 @@ func NewConfig(ctx context.Context, implementation string, logger Logger, ) (config *Config, err error) { var impl firewallImpl var customRulesPath string + // TODO after v3.42 release, use nftables if [nftables.IsSupported] is true. switch implementation { - case "iptables": + case "auto", "iptables": impl, err = iptables.New(ctx, runner, logger) if err != nil { return nil, fmt.Errorf("creating iptables firewall: %w", err) diff --git a/internal/firewall/nftables/output.go b/internal/firewall/nftables/output.go index 0b33dabd..2fa9c24c 100644 --- a/internal/firewall/nftables/output.go +++ b/internal/firewall/nftables/output.go @@ -10,6 +10,22 @@ import ( "github.com/qdm12/gluetun/internal/models" ) +// cidrMask returns the binary mask for a CIDR prefix length. +// +//nolint:mnd +func cidrMask(bits, addrLen int) []byte { + result := make([]byte, addrLen) + fullBytes := bits / 8 + remainingBits := bits % 8 + for i := range fullBytes { + result[i] = 0xff + } + if remainingBits > 0 { + result[fullBytes] = byte((0xff << (8 - remainingBits)) & 0xff) + } + return result +} + func (f *Firewall) AcceptIpv6MulticastOutput(_ context.Context, intf string) error { f.mutex.Lock() defer f.mutex.Unlock() @@ -35,17 +51,17 @@ func (f *Firewall) AcceptIpv6MulticastOutput(_ context.Context, intf string) err mask := []byte{ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, - } //nolint:mnd + } addr := []byte{ 0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0x00, 0x00, 0x00, - } //nolint:mnd + } exprs = append(exprs, &expr.Payload{ DestRegister: 1, Base: expr.PayloadBaseNetworkHeader, - Offset: 24, // IPv6 Destination Address offset //nolint:mnd + Offset: 24, //nolint:mnd // IPv6 Destination Address offset Len: 16, //nolint:mnd }, &expr.Bitwise{ @@ -53,7 +69,7 @@ func (f *Firewall) AcceptIpv6MulticastOutput(_ context.Context, intf string) err DestRegister: 1, Len: 16, //nolint:mnd Mask: mask, - Xor: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, //nolint:mnd + Xor: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, }, &expr.Cmp{ Op: expr.CmpOpEq, @@ -79,8 +95,9 @@ func (f *Firewall) AcceptIpv6MulticastOutput(_ context.Context, intf string) err return nil } -func (f *Firewall) AcceptOutputTrafficToVPN(_ context.Context, defaultInterface string, - connection models.Connection, remove bool) error { +func (f *Firewall) AcceptOutputTrafficToVPN(_ context.Context, defaultInterface string, + connection models.Connection, remove bool, +) error { f.mutex.Lock() defer f.mutex.Unlock() @@ -109,8 +126,8 @@ func (f *Firewall) AcceptOutputTrafficToVPN(_ context.Context, defaultInterface &expr.Payload{ DestRegister: 1, Base: expr.PayloadBaseNetworkHeader, - Offset: 16, // IPv4 destination address offset - Len: 4, //nolint:mnd + Offset: 16, //nolint:mnd // IPv4 destination address offset + Len: 4, //nolint:mnd }, &expr.Cmp{ Op: expr.CmpOpEq, @@ -123,7 +140,7 @@ func (f *Firewall) AcceptOutputTrafficToVPN(_ context.Context, defaultInterface &expr.Payload{ DestRegister: 1, Base: expr.PayloadBaseNetworkHeader, - Offset: 24, // IPv6 destination address offset + Offset: 24, //nolint:mnd// IPv6 destination address offset Len: 16, //nolint:mnd }, &expr.Cmp{ @@ -136,21 +153,17 @@ func (f *Firewall) AcceptOutputTrafficToVPN(_ context.Context, defaultInterface // Protocol (tcp or udp) var protocolByte uint8 - if connection.Protocol == "tcp" || connection.Protocol == "tcp-client" { + switch connection.Protocol { + case "tcp", "tcp-client": protocolByte = 6 // TCP - } else if connection.Protocol == "udp" { + case "udp": protocolByte = 17 // UDP - } else { + default: return fmt.Errorf("unsupported protocol: %s", connection.Protocol) } exprs = append(exprs, - &expr.Payload{ - DestRegister: 1, - Base: expr.PayloadBaseTransportHeader, - Offset: 3, // Protocol byte offset in IP header - Len: 1, - }, + &expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1}, &expr.Cmp{ Op: expr.CmpOpEq, Register: 1, @@ -159,130 +172,12 @@ func (f *Firewall) AcceptOutputTrafficToVPN(_ context.Context, defaultInterface ) // Destination port - portBytes := []byte{byte(connection.Port >> 8), byte(connection.Port)} //nolint:mnd + portBytes := []byte{byte(connection.Port >> 8), byte(connection.Port)} //nolint:mnd,gosec exprs = append(exprs, &expr.Payload{ DestRegister: 1, Base: expr.PayloadBaseTransportHeader, - Offset: 2, // destination port offset - Len: 2, //nolint:mnd - }, - &expr.Cmp{ - Op: expr.CmpOpEq, - Register: 1, - Data: portBytes, - }, - &expr.Verdict{Kind: expr.VerdictAccept}, - ) - - rule := &nftables.Rule{ - Table: table, - Chain: outputChain, - Exprs: exprs, - } - - if !remove { - conn.AddRule(rule) - f.rules = append(f.rules, rule) - } else { - err = f.deleteRule(conn, rule) - if err != nil { - return fmt.Errorf("deleting rule: %w", err) - } - } - - err = conn.Flush() - if err != nil { - if !remove { - f.rules = f.rules[:len(f.rules)-1] - } - return fmt.Errorf("flushing: %w", err) - } - - return nil -} - -func (f *Firewall) AcceptOutput(_ context.Context, protocol, intf string, ip netip.Addr, port uint16, remove bool) error { - f.mutex.Lock() - defer f.mutex.Unlock() - - conn, err := nftables.New() - if err != nil { - return fmt.Errorf("creating nftables connection: %w", err) - } - - table, _, _, outputChain := setupFilterWithBaseChains(conn) - - const maxExprsLen = 7 - exprs := make([]expr.Any, 0, maxExprsLen) - - if intf != "" && intf != "*" { - exprs = append(exprs, - &expr.Meta{Key: expr.MetaKeyOIFNAME, Register: 1}, - &expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: []byte(intf + "\x00")}, - ) - } - - if ip.Is4() { - exprs = append(exprs, - &expr.Payload{ - DestRegister: 1, - Base: expr.PayloadBaseNetworkHeader, - Offset: 16, //nolint:mnd - Len: 4, //nolint:mnd - }, - &expr.Cmp{ - Op: expr.CmpOpEq, - Register: 1, - Data: ip.AsSlice(), - }, - ) - } else { - exprs = append(exprs, - &expr.Payload{ - DestRegister: 1, - Base: expr.PayloadBaseNetworkHeader, - Offset: 24, //nolint:mnd - Len: 16, //nolint:mnd - }, - &expr.Cmp{ - Op: expr.CmpOpEq, - Register: 1, - Data: ip.AsSlice(), - }, - ) - } - - var protocolByte uint8 - switch protocol { - case "tcp": - protocolByte = 6 //nolint:mnd - case "udp": - protocolByte = 17 //nolint:mnd - default: - return fmt.Errorf("unsupported protocol: %s", protocol) - } - - exprs = append(exprs, - &expr.Payload{ - DestRegister: 1, - Base: expr.PayloadBaseTransportHeader, - Offset: 3, //nolint:mnd - Len: 1, - }, - &expr.Cmp{ - Op: expr.CmpOpEq, - Register: 1, - Data: []byte{protocolByte}, - }, - ) - - portBytes := []byte{byte(port >> 8), byte(port)} //nolint:mnd - exprs = append(exprs, - &expr.Payload{ - DestRegister: 1, - Base: expr.PayloadBaseTransportHeader, - Offset: 2, //nolint:mnd + Offset: 2, //nolint:mnd// destination port offset Len: 2, //nolint:mnd }, &expr.Cmp{ @@ -349,7 +244,7 @@ func (f *Firewall) AcceptOutputFromIPPortToIPPort(_ context.Context, protocol, i DestRegister: 1, Base: expr.PayloadBaseNetworkHeader, Offset: 12, //nolint:mnd - Len: 4, //nolint:mnd + Len: 4, //nolint:mnd }, &expr.Cmp{ Op: expr.CmpOpEq, @@ -362,7 +257,7 @@ func (f *Firewall) AcceptOutputFromIPPortToIPPort(_ context.Context, protocol, i &expr.Payload{ DestRegister: 1, Base: expr.PayloadBaseNetworkHeader, - Offset: 8, //nolint:mnd + Offset: 8, //nolint:mnd Len: 16, //nolint:mnd }, &expr.Cmp{ @@ -379,7 +274,7 @@ func (f *Firewall) AcceptOutputFromIPPortToIPPort(_ context.Context, protocol, i DestRegister: 1, Base: expr.PayloadBaseNetworkHeader, Offset: 16, //nolint:mnd - Len: 4, //nolint:mnd + Len: 4, //nolint:mnd }, &expr.Cmp{ Op: expr.CmpOpEq, @@ -410,16 +305,11 @@ func (f *Firewall) AcceptOutputFromIPPortToIPPort(_ context.Context, protocol, i case "udp": protocolByte = 17 //nolint:mnd default: - return fmt.Errorf("unsupported protocol: %s", protocol) + return fmt.Errorf("unsupported protocol: %s", protocol) //nolint:err113 } exprs = append(exprs, - &expr.Payload{ - DestRegister: 1, - Base: expr.PayloadBaseTransportHeader, - Offset: 3, //nolint:mnd - Len: 1, - }, + &expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1}, &expr.Cmp{ Op: expr.CmpOpEq, Register: 1, @@ -427,7 +317,7 @@ func (f *Firewall) AcceptOutputFromIPPortToIPPort(_ context.Context, protocol, i }, ) - sourcePortBytes := []byte{byte(source.Port() >> 8), byte(source.Port())} //nolint:mnd + sourcePortBytes := []byte{byte(source.Port() >> 8), byte(source.Port())} //nolint:mnd destinationPortBytes := []byte{byte(destination.Port() >> 8), byte(destination.Port())} //nolint:mnd exprs = append(exprs, &expr.Payload{ @@ -511,7 +401,7 @@ func (f *Firewall) AcceptOutputFromIPToSubnet(_ context.Context, intf string, as DestRegister: 1, Base: expr.PayloadBaseNetworkHeader, Offset: 12, //nolint:mnd - Len: 4, //nolint:mnd + Len: 4, //nolint:mnd }, &expr.Cmp{ Op: expr.CmpOpEq, @@ -524,7 +414,7 @@ func (f *Firewall) AcceptOutputFromIPToSubnet(_ context.Context, intf string, as &expr.Payload{ DestRegister: 1, Base: expr.PayloadBaseNetworkHeader, - Offset: 8, //nolint:mnd + Offset: 8, //nolint:mnd Len: 16, //nolint:mnd }, &expr.Cmp{ @@ -536,22 +426,31 @@ func (f *Firewall) AcceptOutputFromIPToSubnet(_ context.Context, intf string, as } if subnet.Addr().Is4() { + mask := cidrMask(subnet.Bits(), 4) //nolint:mnd + networkAddr := subnet.Masked().Addr().AsSlice() exprs = append(exprs, &expr.Payload{ DestRegister: 1, Base: expr.PayloadBaseNetworkHeader, Offset: 16, //nolint:mnd - Len: 4, //nolint:mnd + Len: 4, //nolint:mnd }, &expr.Bitwise{ SourceRegister: 1, DestRegister: 1, Len: 4, //nolint:mnd - Mask: subnet.Masked().Addr().AsSlice(), + Mask: mask, Xor: []byte{0, 0, 0, 0}, //nolint:mnd }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: networkAddr, + }, ) } else { + mask := cidrMask(subnet.Bits(), 16) //nolint:mnd + networkAddr := subnet.Masked().Addr().AsSlice() exprs = append(exprs, &expr.Payload{ DestRegister: 1, @@ -563,9 +462,14 @@ func (f *Firewall) AcceptOutputFromIPToSubnet(_ context.Context, intf string, as SourceRegister: 1, DestRegister: 1, Len: 16, //nolint:mnd - Mask: subnet.Masked().Addr().AsSlice(), + Mask: mask, Xor: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, //nolint:mnd }, + &expr.Cmp{ + Op: expr.CmpOpEq, + Register: 1, + Data: networkAddr, + }, ) } diff --git a/internal/firewall/nftables/redirect.go b/internal/firewall/nftables/redirect.go index 6da44746..cd79fdeb 100644 --- a/internal/firewall/nftables/redirect.go +++ b/internal/firewall/nftables/redirect.go @@ -80,7 +80,7 @@ func (f *Firewall) RedirectPort(_ context.Context, intf string, err = conn.Flush() if err != nil && !isTableDoesNotExist(err) { if !remove { - removeFailedRules(f.rules, rulesToDelete) + f.rules = removeFailedRules(f.rules, rulesToDelete) } return fmt.Errorf("redirecting source port %d to destination port %d on interface %s: %w", sourcePort, destinationPort, intf, err) @@ -157,10 +157,12 @@ func isTableDoesNotExist(err error) bool { return strings.Contains(err.Error(), "Table does not exist") } -func removeFailedRules(rules []*nftables.Rule, failed []*nftables.Rule) { - for i := len(rules) - 1; i >= 0; i-- { - if slices.Contains(failed, rules[i]) { - rules = append(rules[:i], rules[i+1:]...) +func removeFailedRules(rules []*nftables.Rule, failed []*nftables.Rule) (succeeded []*nftables.Rule) { + succeeded = make([]*nftables.Rule, 0, len(rules)-len(failed)) + for _, rule := range rules { + if !slices.Contains(failed, rule) { + succeeded = append(succeeded, rule) } } + return succeeded } diff --git a/internal/firewall/nftables/support.go b/internal/firewall/nftables/support.go index 11f0c5f8..ac80616b 100644 --- a/internal/firewall/nftables/support.go +++ b/internal/firewall/nftables/support.go @@ -12,6 +12,7 @@ import ( "github.com/google/nftables" ) +// IsSupported returns true if nftables is supported on the system, false otherwise. func IsSupported() bool { conn, err := nftables.New() if err != nil {