gluetun/internal/firewall/support_test.go
2024-10-11 19:27:29 +00:00

346 lines
11 KiB
Go

package firewall
import (
"context"
"errors"
"testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func newAppendTestRuleMatcher(path string) *cmdMatcher {
return newCmdMatcher(path,
"^-A$", "^OUTPUT$", "^-o$", "^[a-z0-9]{15}$",
"^-j$", "^DROP$")
}
func newDeleteTestRuleMatcher(path string) *cmdMatcher {
return newCmdMatcher(path,
"^-D$", "^OUTPUT$", "^-o$", "^[a-z0-9]{15}$",
"^-j$", "^DROP$")
}
func newListInputRulesMatcher(path string) *cmdMatcher {
return newCmdMatcher(path,
"^-L$", "^INPUT$")
}
func newSetPolicyMatcher(path, inputPolicy string) *cmdMatcher { //nolint:unparam
return newCmdMatcher(path,
"^--policy$", "^INPUT$", "^"+inputPolicy+"$")
}
func Test_checkIptablesSupport(t *testing.T) {
t.Parallel()
ctx := context.Background()
errDummy := errors.New("exit code 4")
const inputPolicy = "ACCEPT"
testCases := map[string]struct {
buildRunner func(ctrl *gomock.Controller) CmdRunner
iptablesPathsToTry []string
iptablesPath string
errSentinel error
errMessage string
}{
"critical error when checking": {
buildRunner: func(ctrl *gomock.Controller) CmdRunner {
runner := NewMockCmdRunner(ctrl)
runner.EXPECT().Run(newAppendTestRuleMatcher("path1")).
Return("", nil)
runner.EXPECT().Run(newDeleteTestRuleMatcher("path1")).
Return("output", errDummy)
return runner
},
iptablesPathsToTry: []string{"path1", "path2"},
errSentinel: ErrTestRuleCleanup,
errMessage: "for path1: failed cleaning up test rule: " +
"output (exit code 4)",
},
"found valid path": {
buildRunner: func(ctrl *gomock.Controller) CmdRunner {
runner := NewMockCmdRunner(ctrl)
runner.EXPECT().Run(newAppendTestRuleMatcher("path1")).
Return("", nil)
runner.EXPECT().Run(newDeleteTestRuleMatcher("path1")).
Return("", nil)
runner.EXPECT().Run(newListInputRulesMatcher("path1")).
Return("Chain INPUT (policy "+inputPolicy+")", nil)
runner.EXPECT().Run(newSetPolicyMatcher("path1", inputPolicy)).
Return("", nil)
return runner
},
iptablesPathsToTry: []string{"path1", "path2"},
iptablesPath: "path1",
},
"all permission denied": {
buildRunner: func(ctrl *gomock.Controller) CmdRunner {
runner := NewMockCmdRunner(ctrl)
runner.EXPECT().Run(newAppendTestRuleMatcher("path1")).
Return("Permission denied (you must be root) more context", errDummy)
runner.EXPECT().Run(newAppendTestRuleMatcher("path2")).
Return("context: Permission denied (you must be root)", errDummy)
return runner
},
iptablesPathsToTry: []string{"path1", "path2"},
errSentinel: ErrNetAdminMissing,
errMessage: "NET_ADMIN capability is missing: " +
"path1: Permission denied (you must be root) more context (exit code 4); " +
"path2: context: Permission denied (you must be root) (exit code 4)",
},
"no valid path": {
buildRunner: func(ctrl *gomock.Controller) CmdRunner {
runner := NewMockCmdRunner(ctrl)
runner.EXPECT().Run(newAppendTestRuleMatcher("path1")).
Return("output 1", errDummy)
runner.EXPECT().Run(newAppendTestRuleMatcher("path2")).
Return("output 2", errDummy)
return runner
},
iptablesPathsToTry: []string{"path1", "path2"},
errSentinel: ErrIPTablesNotSupported,
errMessage: "no iptables supported found: " +
"errors encountered are: " +
"path1: output 1 (exit code 4); " +
"path2: output 2 (exit code 4)",
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
runner := testCase.buildRunner(ctrl)
iptablesPath, err := checkIptablesSupport(ctx, runner, testCase.iptablesPathsToTry...)
require.ErrorIs(t, err, testCase.errSentinel)
if testCase.errSentinel != nil {
assert.EqualError(t, err, testCase.errMessage)
}
assert.Equal(t, testCase.iptablesPath, iptablesPath)
})
}
}
func Test_testIptablesPath(t *testing.T) {
t.Parallel()
ctx := context.Background()
const path = "dummypath"
errDummy := errors.New("exit code 4")
const inputPolicy = "ACCEPT"
testCases := map[string]struct {
buildRunner func(ctrl *gomock.Controller) CmdRunner
ok bool
unsupportedMessage string
criticalErrWrapped error
criticalErrMessage string
}{
"append test rule permission denied": {
buildRunner: func(ctrl *gomock.Controller) CmdRunner {
runner := NewMockCmdRunner(ctrl)
runner.EXPECT().Run(newAppendTestRuleMatcher(path)).
Return("Permission denied (you must be root)", errDummy)
return runner
},
unsupportedMessage: "Permission denied (you must be root) (exit code 4)",
},
"append test rule unsupported": {
buildRunner: func(ctrl *gomock.Controller) CmdRunner {
runner := NewMockCmdRunner(ctrl)
runner.EXPECT().Run(newAppendTestRuleMatcher(path)).
Return("some output", errDummy)
return runner
},
unsupportedMessage: "some output (exit code 4)",
},
"remove test rule error": {
buildRunner: func(ctrl *gomock.Controller) CmdRunner {
runner := NewMockCmdRunner(ctrl)
runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(newDeleteTestRuleMatcher(path)).
Return("some output", errDummy)
return runner
},
criticalErrWrapped: ErrTestRuleCleanup,
criticalErrMessage: "failed cleaning up test rule: some output (exit code 4)",
},
"list input rules permission denied": {
buildRunner: func(ctrl *gomock.Controller) CmdRunner {
runner := NewMockCmdRunner(ctrl)
runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(newDeleteTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(newListInputRulesMatcher(path)).
Return("Permission denied (you must be root)", errDummy)
return runner
},
unsupportedMessage: "Permission denied (you must be root) (exit code 4)",
},
"list input rules unsupported": {
buildRunner: func(ctrl *gomock.Controller) CmdRunner {
runner := NewMockCmdRunner(ctrl)
runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(newDeleteTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(newListInputRulesMatcher(path)).
Return("some output", errDummy)
return runner
},
unsupportedMessage: "some output (exit code 4)",
},
"list input rules no policy": {
buildRunner: func(ctrl *gomock.Controller) CmdRunner {
runner := NewMockCmdRunner(ctrl)
runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(newDeleteTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(newListInputRulesMatcher(path)).
Return("some\noutput", nil)
return runner
},
criticalErrWrapped: ErrInputPolicyNotFound,
criticalErrMessage: "input policy not found: in INPUT rules: some\noutput",
},
"set policy permission denied": {
buildRunner: func(ctrl *gomock.Controller) CmdRunner {
runner := NewMockCmdRunner(ctrl)
runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(newDeleteTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(newListInputRulesMatcher(path)).
Return("\nChain INPUT (policy "+inputPolicy+")\nAA\n", nil)
runner.EXPECT().Run(newSetPolicyMatcher(path, inputPolicy)).
Return("Permission denied (you must be root)", errDummy)
return runner
},
unsupportedMessage: "Permission denied (you must be root) (exit code 4)",
},
"set policy unsupported": {
buildRunner: func(ctrl *gomock.Controller) CmdRunner {
runner := NewMockCmdRunner(ctrl)
runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(newDeleteTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(newListInputRulesMatcher(path)).
Return("\nChain INPUT (policy "+inputPolicy+")\nBB\n", nil)
runner.EXPECT().Run(newSetPolicyMatcher(path, inputPolicy)).
Return("some output", errDummy)
return runner
},
unsupportedMessage: "some output (exit code 4)",
},
"success": {
buildRunner: func(ctrl *gomock.Controller) CmdRunner {
runner := NewMockCmdRunner(ctrl)
runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(newDeleteTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(newListInputRulesMatcher(path)).
Return("\nChain INPUT (policy "+inputPolicy+")\nCC\n", nil)
runner.EXPECT().Run(newSetPolicyMatcher(path, inputPolicy)).
Return("some output", nil)
return runner
},
ok: true,
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
runner := testCase.buildRunner(ctrl)
ok, unsupportedMessage, criticalErr := testIptablesPath(ctx, path, runner)
assert.Equal(t, testCase.ok, ok)
assert.Equal(t, testCase.unsupportedMessage, unsupportedMessage)
assert.ErrorIs(t, criticalErr, testCase.criticalErrWrapped)
if testCase.criticalErrWrapped != nil {
assert.EqualError(t, criticalErr, testCase.criticalErrMessage)
}
})
}
}
func Test_isPermissionDenied(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
errMessage string
ok bool
}{
"empty error": {},
"other error": {
errMessage: "some error",
},
"permission denied": {
errMessage: "Permission denied (you must be root) have you tried blabla",
ok: true,
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
ok := isPermissionDenied(testCase.errMessage)
assert.Equal(t, testCase.ok, ok)
})
}
}
func Test_extractInputPolicy(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
line string
policy string
ok bool
}{
"empty line": {},
"random line": {
line: "random line",
},
"only first part": {
line: "Chain INPUT (policy ",
},
"empty policy": {
line: "Chain INPUT (policy )",
},
"ACCEPT policy": {
line: "Chain INPUT (policy ACCEPT)",
policy: "ACCEPT",
ok: true,
},
"ACCEPT policy with surrounding garbage": {
line: "garbage Chain INPUT (policy ACCEPT\t) )g()arbage",
policy: "ACCEPT",
ok: true,
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
policy, ok := extractInputPolicy(testCase.line)
assert.Equal(t, testCase.policy, policy)
assert.Equal(t, testCase.ok, ok)
})
}
}
func Test_randomInterfaceName(t *testing.T) {
t.Parallel()
const expectedRegex = `^[a-z0-9]{15}$`
interfaceName := randomInterfaceName()
assert.Regexp(t, expectedRegex, interfaceName)
}