mirror of
https://github.com/qdm12/gluetun.git
synced 2026-08-11 14:52:56 +02:00
human review!
This commit is contained in:
+1
-1
@@ -165,7 +165,7 @@ ENV VPN_SERVICE_PROVIDER=pia \
|
|||||||
FIREWALL_VPN_INPUT_PORTS= \
|
FIREWALL_VPN_INPUT_PORTS= \
|
||||||
FIREWALL_INPUT_PORTS= \
|
FIREWALL_INPUT_PORTS= \
|
||||||
FIREWALL_OUTBOUND_SUBNETS= \
|
FIREWALL_OUTBOUND_SUBNETS= \
|
||||||
FIREWALL_IMPLEMENTATION=iptables \
|
FIREWALL_IMPLEMENTATION=auto \
|
||||||
FIREWALL_DEBUG=off \
|
FIREWALL_DEBUG=off \
|
||||||
# Logging
|
# Logging
|
||||||
LOG_LEVEL=info \
|
LOG_LEVEL=info \
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
|
|
||||||
"github.com/qdm12/gosettings"
|
"github.com/qdm12/gosettings"
|
||||||
"github.com/qdm12/gosettings/reader"
|
"github.com/qdm12/gosettings/reader"
|
||||||
|
"github.com/qdm12/gosettings/validate"
|
||||||
"github.com/qdm12/gotree"
|
"github.com/qdm12/gotree"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,8 +35,9 @@ func (f Firewall) validate() (err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !helpers.IsOneOf(f.Implementation, "iptables", "nftables") {
|
err = validate.IsOneOf(f.Implementation, "auto", "iptables", "nftables")
|
||||||
return fmt.Errorf("firewall implementation %q must be either 'iptables' or 'nftables'", f.Implementation)
|
if err != nil {
|
||||||
|
return fmt.Errorf("firewall implementation: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -41,8 +41,9 @@ func NewConfig(ctx context.Context, implementation string, logger Logger,
|
|||||||
) (config *Config, err error) {
|
) (config *Config, err error) {
|
||||||
var impl firewallImpl
|
var impl firewallImpl
|
||||||
var customRulesPath string
|
var customRulesPath string
|
||||||
|
// TODO after v3.42 release, use nftables if [nftables.IsSupported] is true.
|
||||||
switch implementation {
|
switch implementation {
|
||||||
case "iptables":
|
case "auto", "iptables":
|
||||||
impl, err = iptables.New(ctx, runner, logger)
|
impl, err = iptables.New(ctx, runner, logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("creating iptables firewall: %w", err)
|
return nil, fmt.Errorf("creating iptables firewall: %w", err)
|
||||||
|
|||||||
@@ -10,6 +10,22 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/models"
|
"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 {
|
func (f *Firewall) AcceptIpv6MulticastOutput(_ context.Context, intf string) error {
|
||||||
f.mutex.Lock()
|
f.mutex.Lock()
|
||||||
defer f.mutex.Unlock()
|
defer f.mutex.Unlock()
|
||||||
@@ -35,17 +51,17 @@ func (f *Firewall) AcceptIpv6MulticastOutput(_ context.Context, intf string) err
|
|||||||
mask := []byte{
|
mask := []byte{
|
||||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||||
0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00,
|
0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00,
|
||||||
} //nolint:mnd
|
}
|
||||||
addr := []byte{
|
addr := []byte{
|
||||||
0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x01, 0xff, 0x00, 0x00, 0x00,
|
0x00, 0x00, 0x00, 0x01, 0xff, 0x00, 0x00, 0x00,
|
||||||
} //nolint:mnd
|
}
|
||||||
|
|
||||||
exprs = append(exprs,
|
exprs = append(exprs,
|
||||||
&expr.Payload{
|
&expr.Payload{
|
||||||
DestRegister: 1,
|
DestRegister: 1,
|
||||||
Base: expr.PayloadBaseNetworkHeader,
|
Base: expr.PayloadBaseNetworkHeader,
|
||||||
Offset: 24, // IPv6 Destination Address offset //nolint:mnd
|
Offset: 24, //nolint:mnd // IPv6 Destination Address offset
|
||||||
Len: 16, //nolint:mnd
|
Len: 16, //nolint:mnd
|
||||||
},
|
},
|
||||||
&expr.Bitwise{
|
&expr.Bitwise{
|
||||||
@@ -53,7 +69,7 @@ func (f *Firewall) AcceptIpv6MulticastOutput(_ context.Context, intf string) err
|
|||||||
DestRegister: 1,
|
DestRegister: 1,
|
||||||
Len: 16, //nolint:mnd
|
Len: 16, //nolint:mnd
|
||||||
Mask: mask,
|
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{
|
&expr.Cmp{
|
||||||
Op: expr.CmpOpEq,
|
Op: expr.CmpOpEq,
|
||||||
@@ -80,7 +96,8 @@ func (f *Firewall) AcceptIpv6MulticastOutput(_ context.Context, intf string) err
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Firewall) AcceptOutputTrafficToVPN(_ context.Context, defaultInterface string,
|
func (f *Firewall) AcceptOutputTrafficToVPN(_ context.Context, defaultInterface string,
|
||||||
connection models.Connection, remove bool) error {
|
connection models.Connection, remove bool,
|
||||||
|
) error {
|
||||||
f.mutex.Lock()
|
f.mutex.Lock()
|
||||||
defer f.mutex.Unlock()
|
defer f.mutex.Unlock()
|
||||||
|
|
||||||
@@ -109,7 +126,7 @@ func (f *Firewall) AcceptOutputTrafficToVPN(_ context.Context, defaultInterface
|
|||||||
&expr.Payload{
|
&expr.Payload{
|
||||||
DestRegister: 1,
|
DestRegister: 1,
|
||||||
Base: expr.PayloadBaseNetworkHeader,
|
Base: expr.PayloadBaseNetworkHeader,
|
||||||
Offset: 16, // IPv4 destination address offset
|
Offset: 16, //nolint:mnd // IPv4 destination address offset
|
||||||
Len: 4, //nolint:mnd
|
Len: 4, //nolint:mnd
|
||||||
},
|
},
|
||||||
&expr.Cmp{
|
&expr.Cmp{
|
||||||
@@ -123,7 +140,7 @@ func (f *Firewall) AcceptOutputTrafficToVPN(_ context.Context, defaultInterface
|
|||||||
&expr.Payload{
|
&expr.Payload{
|
||||||
DestRegister: 1,
|
DestRegister: 1,
|
||||||
Base: expr.PayloadBaseNetworkHeader,
|
Base: expr.PayloadBaseNetworkHeader,
|
||||||
Offset: 24, // IPv6 destination address offset
|
Offset: 24, //nolint:mnd// IPv6 destination address offset
|
||||||
Len: 16, //nolint:mnd
|
Len: 16, //nolint:mnd
|
||||||
},
|
},
|
||||||
&expr.Cmp{
|
&expr.Cmp{
|
||||||
@@ -136,21 +153,17 @@ func (f *Firewall) AcceptOutputTrafficToVPN(_ context.Context, defaultInterface
|
|||||||
|
|
||||||
// Protocol (tcp or udp)
|
// Protocol (tcp or udp)
|
||||||
var protocolByte uint8
|
var protocolByte uint8
|
||||||
if connection.Protocol == "tcp" || connection.Protocol == "tcp-client" {
|
switch connection.Protocol {
|
||||||
|
case "tcp", "tcp-client":
|
||||||
protocolByte = 6 // TCP
|
protocolByte = 6 // TCP
|
||||||
} else if connection.Protocol == "udp" {
|
case "udp":
|
||||||
protocolByte = 17 // UDP
|
protocolByte = 17 // UDP
|
||||||
} else {
|
default:
|
||||||
return fmt.Errorf("unsupported protocol: %s", connection.Protocol)
|
return fmt.Errorf("unsupported protocol: %s", connection.Protocol)
|
||||||
}
|
}
|
||||||
|
|
||||||
exprs = append(exprs,
|
exprs = append(exprs,
|
||||||
&expr.Payload{
|
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
|
||||||
DestRegister: 1,
|
|
||||||
Base: expr.PayloadBaseTransportHeader,
|
|
||||||
Offset: 3, // Protocol byte offset in IP header
|
|
||||||
Len: 1,
|
|
||||||
},
|
|
||||||
&expr.Cmp{
|
&expr.Cmp{
|
||||||
Op: expr.CmpOpEq,
|
Op: expr.CmpOpEq,
|
||||||
Register: 1,
|
Register: 1,
|
||||||
@@ -159,130 +172,12 @@ func (f *Firewall) AcceptOutputTrafficToVPN(_ context.Context, defaultInterface
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Destination port
|
// 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,
|
exprs = append(exprs,
|
||||||
&expr.Payload{
|
&expr.Payload{
|
||||||
DestRegister: 1,
|
DestRegister: 1,
|
||||||
Base: expr.PayloadBaseTransportHeader,
|
Base: expr.PayloadBaseTransportHeader,
|
||||||
Offset: 2, // destination port offset
|
Offset: 2, //nolint:mnd// 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
|
|
||||||
Len: 2, //nolint:mnd
|
Len: 2, //nolint:mnd
|
||||||
},
|
},
|
||||||
&expr.Cmp{
|
&expr.Cmp{
|
||||||
@@ -410,16 +305,11 @@ func (f *Firewall) AcceptOutputFromIPPortToIPPort(_ context.Context, protocol, i
|
|||||||
case "udp":
|
case "udp":
|
||||||
protocolByte = 17 //nolint:mnd
|
protocolByte = 17 //nolint:mnd
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported protocol: %s", protocol)
|
return fmt.Errorf("unsupported protocol: %s", protocol) //nolint:err113
|
||||||
}
|
}
|
||||||
|
|
||||||
exprs = append(exprs,
|
exprs = append(exprs,
|
||||||
&expr.Payload{
|
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
|
||||||
DestRegister: 1,
|
|
||||||
Base: expr.PayloadBaseTransportHeader,
|
|
||||||
Offset: 3, //nolint:mnd
|
|
||||||
Len: 1,
|
|
||||||
},
|
|
||||||
&expr.Cmp{
|
&expr.Cmp{
|
||||||
Op: expr.CmpOpEq,
|
Op: expr.CmpOpEq,
|
||||||
Register: 1,
|
Register: 1,
|
||||||
@@ -536,6 +426,8 @@ func (f *Firewall) AcceptOutputFromIPToSubnet(_ context.Context, intf string, as
|
|||||||
}
|
}
|
||||||
|
|
||||||
if subnet.Addr().Is4() {
|
if subnet.Addr().Is4() {
|
||||||
|
mask := cidrMask(subnet.Bits(), 4) //nolint:mnd
|
||||||
|
networkAddr := subnet.Masked().Addr().AsSlice()
|
||||||
exprs = append(exprs,
|
exprs = append(exprs,
|
||||||
&expr.Payload{
|
&expr.Payload{
|
||||||
DestRegister: 1,
|
DestRegister: 1,
|
||||||
@@ -547,11 +439,18 @@ func (f *Firewall) AcceptOutputFromIPToSubnet(_ context.Context, intf string, as
|
|||||||
SourceRegister: 1,
|
SourceRegister: 1,
|
||||||
DestRegister: 1,
|
DestRegister: 1,
|
||||||
Len: 4, //nolint:mnd
|
Len: 4, //nolint:mnd
|
||||||
Mask: subnet.Masked().Addr().AsSlice(),
|
Mask: mask,
|
||||||
Xor: []byte{0, 0, 0, 0}, //nolint:mnd
|
Xor: []byte{0, 0, 0, 0}, //nolint:mnd
|
||||||
},
|
},
|
||||||
|
&expr.Cmp{
|
||||||
|
Op: expr.CmpOpEq,
|
||||||
|
Register: 1,
|
||||||
|
Data: networkAddr,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
mask := cidrMask(subnet.Bits(), 16) //nolint:mnd
|
||||||
|
networkAddr := subnet.Masked().Addr().AsSlice()
|
||||||
exprs = append(exprs,
|
exprs = append(exprs,
|
||||||
&expr.Payload{
|
&expr.Payload{
|
||||||
DestRegister: 1,
|
DestRegister: 1,
|
||||||
@@ -563,9 +462,14 @@ func (f *Firewall) AcceptOutputFromIPToSubnet(_ context.Context, intf string, as
|
|||||||
SourceRegister: 1,
|
SourceRegister: 1,
|
||||||
DestRegister: 1,
|
DestRegister: 1,
|
||||||
Len: 16, //nolint:mnd
|
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
|
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,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ func (f *Firewall) RedirectPort(_ context.Context, intf string,
|
|||||||
err = conn.Flush()
|
err = conn.Flush()
|
||||||
if err != nil && !isTableDoesNotExist(err) {
|
if err != nil && !isTableDoesNotExist(err) {
|
||||||
if !remove {
|
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",
|
return fmt.Errorf("redirecting source port %d to destination port %d on interface %s: %w",
|
||||||
sourcePort, destinationPort, intf, err)
|
sourcePort, destinationPort, intf, err)
|
||||||
@@ -157,10 +157,12 @@ func isTableDoesNotExist(err error) bool {
|
|||||||
return strings.Contains(err.Error(), "Table does not exist")
|
return strings.Contains(err.Error(), "Table does not exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeFailedRules(rules []*nftables.Rule, failed []*nftables.Rule) {
|
func removeFailedRules(rules []*nftables.Rule, failed []*nftables.Rule) (succeeded []*nftables.Rule) {
|
||||||
for i := len(rules) - 1; i >= 0; i-- {
|
succeeded = make([]*nftables.Rule, 0, len(rules)-len(failed))
|
||||||
if slices.Contains(failed, rules[i]) {
|
for _, rule := range rules {
|
||||||
rules = append(rules[:i], rules[i+1:]...)
|
if !slices.Contains(failed, rule) {
|
||||||
|
succeeded = append(succeeded, rule)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return succeeded
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/google/nftables"
|
"github.com/google/nftables"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// IsSupported returns true if nftables is supported on the system, false otherwise.
|
||||||
func IsSupported() bool {
|
func IsSupported() bool {
|
||||||
conn, err := nftables.New()
|
conn, err := nftables.New()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user