Files
gluetun/internal/firewall/nftables/tcp_test.go
T
2026-08-07 11:20:23 +00:00

173 lines
4.9 KiB
Go

package nftables
import (
"context"
"fmt"
"net/netip"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_TempDropOutputTCPRST(t *testing.T) {
t.Parallel()
ctx := context.Background()
fw := New(nil)
src := netip.MustParseAddrPort("192.168.1.1:12345")
dst := netip.MustParseAddrPort("10.0.0.1:443")
excludeMark := 0x100
revert, err := fw.TempDropOutputTCPRST(ctx, src, dst, excludeMark)
// May fail if not running as root; just verify no panic and correct error type
if err != nil {
assert.Nil(t, revert)
assert.Contains(t, err.Error(), "creating nftables connection")
} else {
require.NotNil(t, revert)
}
}
func Test_TempDropOutputTCPRST_ipv6(t *testing.T) {
t.Parallel()
ctx := context.Background()
fw := New(nil)
src := netip.MustParseAddrPort("[2001:db8::1]:12345")
dst := netip.MustParseAddrPort("[2001:db8::2]:443")
excludeMark := 0x100
revert, err := fw.TempDropOutputTCPRST(ctx, src, dst, excludeMark)
if err != nil {
assert.Nil(t, revert)
assert.Contains(t, err.Error(), "creating nftables connection")
} else {
require.NotNil(t, revert)
}
}
func Test_TempDropOutputTCPRST_ExpressionCount(t *testing.T) {
t.Parallel()
// Verify that the TCP RST rule has the expected number of expressions
// Source IP (2) + Dest IP (2) + TCP proto (2) + src port (2) + dst port (2) +
// TCP flags (2) + mark exclusion (2) + DROP (1) = 15 for IPv4
// Source IP (2) + Dest IP (2) + TCP proto (2) + src port (2) + dst port (2) +
// TCP flags (2) + mark exclusion (2) + DROP (1) = 15 for IPv6
src := netip.MustParseAddrPort("192.168.1.1:12345")
dst := netip.MustParseAddrPort("10.0.0.1:443")
excludeMark := 0x100
exprs := buildTCPRSTDropExprs(src, dst, excludeMark)
// 2 (src IP) + 2 (dst IP) + 2 (proto) + 2 (src port) + 2 (dst port) + 2 (flags) + 2 (mark) + 1 (drop)
assert.Len(t, exprs, 15)
}
func Test_TempDropOutputTCPRST_TCPFlags(t *testing.T) {
t.Parallel()
// Verify TCP RST flag matching expression
src := netip.MustParseAddrPort("192.168.1.1:12345")
dst := netip.MustParseAddrPort("10.0.0.1:443")
excludeMark := 0x100
exprs := buildTCPRSTDropExprs(src, dst, excludeMark)
// Find the TCP flags expression (should be near the end, before mark)
var flagsCmp *exprCmpFinder
for _, e := range exprs {
if cmp, ok := e.(*exprCmpFinder); ok && cmp.Data != nil && len(cmp.Data) == 1 && cmp.Data[0] == 0x04 {
flagsCmp = cmp
break
}
}
// The TCP flags byte (offset 13) should match exactly 0x04 (RST only)
require.NotNil(t, flagsCmp)
assert.Equal(t, []byte{0x04}, flagsCmp.Data)
}
// Helper types for testing expression structure.
type exprCmpFinder struct {
Op byte
Data []byte
}
func buildTCPRSTDropExprs(src, dst netip.AddrPort, excludeMark int) []any {
exprs := make([]any, 0, 15)
// Source IP
if src.Addr().Is4() {
exprs = append(exprs, "payload_src_ip_v4", &exprCmpFinder{Data: src.Addr().AsSlice()})
} else {
exprs = append(exprs, "payload_src_ip_v6", &exprCmpFinder{Data: src.Addr().AsSlice()})
}
// Dest IP
if dst.Addr().Is4() {
exprs = append(exprs, "payload_dst_ip_v4", &exprCmpFinder{Data: dst.Addr().AsSlice()})
} else {
exprs = append(exprs, "payload_dst_ip_v6", &exprCmpFinder{Data: dst.Addr().AsSlice()})
}
// TCP protocol
exprs = append(exprs, "meta_l4proto", &exprCmpFinder{Data: []byte{6}})
// Source port
srcPort := []byte{byte(src.Port() >> 8), byte(src.Port())} //nolint:gosec // network byte order
exprs = append(exprs, "payload_src_port", &exprCmpFinder{Data: srcPort})
// Dest port
dstPort := []byte{byte(dst.Port() >> 8), byte(dst.Port())} //nolint:gosec // network byte order
exprs = append(exprs, "payload_dst_port", &exprCmpFinder{Data: dstPort})
// TCP flags (RST only = 0x04)
exprs = append(exprs, "payload_flags", &exprCmpFinder{Data: []byte{0x04}})
// Mark exclusion
markData := []byte{ //nolint:gosec // mark is int (32-bit), byte conversions are intentional
byte(excludeMark), byte(excludeMark >> 8), byte(excludeMark >> 16), byte(excludeMark >> 24),
}
exprs = append(exprs, "meta_mark_neq", &exprCmpFinder{Data: markData})
// DROP
exprs = append(exprs, "verdict_drop")
return exprs
}
func Test_TempDropOutputTCPRST_MarkExclusion(t *testing.T) {
t.Parallel()
// Verify the mark exclusion works correctly for different mark values
testCases := []struct {
mark int
expected []byte
}{
{0x100, []byte{0x00, 0x01, 0x00, 0x00}},
{0x0, []byte{0x00, 0x00, 0x00, 0x00}},
{0xFFFFFFFF, []byte{0xFF, 0xFF, 0xFF, 0xFF}},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("mark_%d", tc.mark), func(t *testing.T) {
t.Parallel()
src := netip.MustParseAddrPort("192.168.1.1:12345")
dst := netip.MustParseAddrPort("10.0.0.1:443")
exprs := buildTCPRSTDropExprs(src, dst, tc.mark)
// Find mark exclusion expression (second to last before DROP)
if len(exprs) >= 2 {
markExpr, ok := exprs[len(exprs)-2].(*exprCmpFinder)
require.True(t, ok)
assert.Equal(t, tc.expected, markExpr.Data)
}
})
}
}