human review!

This commit is contained in:
Quentin McGaw
2026-08-06 20:08:38 +00:00
parent 0f337b4277
commit c513b8fbd2
6 changed files with 73 additions and 164 deletions
+1 -1
View File
@@ -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 \
+4 -3
View File
@@ -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
+2 -1
View File
@@ -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)
+49 -145
View File
@@ -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,
@@ -80,7 +96,8 @@ func (f *Firewall) AcceptIpv6MulticastOutput(_ context.Context, intf string) err
}
func (f *Firewall) AcceptOutputTrafficToVPN(_ context.Context, defaultInterface string,
connection models.Connection, remove bool) error {
connection models.Connection, remove bool,
) error {
f.mutex.Lock()
defer f.mutex.Unlock()
@@ -109,7 +126,7 @@ func (f *Firewall) AcceptOutputTrafficToVPN(_ context.Context, defaultInterface
&expr.Payload{
DestRegister: 1,
Base: expr.PayloadBaseNetworkHeader,
Offset: 16, // IPv4 destination address offset
Offset: 16, //nolint:mnd // IPv4 destination address offset
Len: 4, //nolint:mnd
},
&expr.Cmp{
@@ -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{
@@ -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,
@@ -536,6 +426,8 @@ 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,
@@ -547,11 +439,18 @@ func (f *Firewall) AcceptOutputFromIPToSubnet(_ context.Context, intf string, as
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,
},
)
}
+7 -5
View File
@@ -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
}
+1
View File
@@ -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 {