fix(firewall): iptables detection with permission denied

This commit is contained in:
Quentin McGaw
2022-11-15 12:34:25 +00:00
parent 7421dcb45f
commit 5a06d8e155
3 changed files with 177 additions and 70 deletions

View File

@@ -48,7 +48,7 @@ func (cm *cmdMatcher) String() string {
return fmt.Sprintf("path %s, argument regular expressions %v", cm.path, cm.argsRegex) return fmt.Sprintf("path %s, argument regular expressions %v", cm.path, cm.argsRegex)
} }
func newCmdMatcher(path string, argsRegex ...string) *cmdMatcher { //nolint:unparam func newCmdMatcher(path string, argsRegex ...string) *cmdMatcher {
argsRegexp := make([]*regexp.Regexp, len(argsRegex)) argsRegexp := make([]*regexp.Regexp, len(argsRegex))
for i, argRegex := range argsRegex { for i, argRegex := range argsRegex {
argsRegexp[i] = regexp.MustCompile(argRegex) argsRegexp[i] = regexp.MustCompile(argRegex)

View File

@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"math/rand" "math/rand"
"os/exec" "os/exec"
"sort"
"strings" "strings"
"github.com/qdm12/golibs/command" "github.com/qdm12/golibs/command"
@@ -20,7 +21,7 @@ var (
func checkIptablesSupport(ctx context.Context, runner command.Runner, func checkIptablesSupport(ctx context.Context, runner command.Runner,
iptablesPathsToTry ...string) (iptablesPath string, err error) { iptablesPathsToTry ...string) (iptablesPath string, err error) {
var lastUnsupportedMessage string iptablesPathToUnsupportedMessage := make(map[string]string, len(iptablesPathsToTry))
for _, pathToTest := range iptablesPathsToTry { for _, pathToTest := range iptablesPathsToTry {
ok, unsupportedMessage, err := testIptablesPath(ctx, pathToTest, runner) ok, unsupportedMessage, err := testIptablesPath(ctx, pathToTest, runner)
if err != nil { if err != nil {
@@ -29,17 +30,35 @@ func checkIptablesSupport(ctx context.Context, runner command.Runner,
iptablesPath = pathToTest iptablesPath = pathToTest
break break
} }
iptablesPathToUnsupportedMessage[pathToTest] = unsupportedMessage
lastUnsupportedMessage = unsupportedMessage
} }
if iptablesPath == "" { // all iptables to try failed if iptablesPath != "" {
return "", fmt.Errorf("%w: from %s: last error is: %s", // some paths may be unsupported but that does not matter
ErrIPTablesNotSupported, strings.Join(iptablesPathsToTry, ", "), // since we found one working.
lastUnsupportedMessage) return iptablesPath, nil
} }
return iptablesPath, nil allArePermissionDenied := true
allUnsupportedMessages := make(sort.StringSlice, 0, len(iptablesPathToUnsupportedMessage))
for iptablesPath, unsupportedMessage := range iptablesPathToUnsupportedMessage {
if !isPermissionDenied(unsupportedMessage) {
allArePermissionDenied = false
}
unsupportedMessage = iptablesPath + ": " + unsupportedMessage
allUnsupportedMessages = append(allUnsupportedMessages, unsupportedMessage)
}
allUnsupportedMessages.Sort() // predictable order for tests
if allArePermissionDenied {
// If the error is related to a denied permission for all iptables path,
// return an error describing what to do from an end-user perspective.
return "", fmt.Errorf("%w: %s", ErrNetAdminMissing, strings.Join(allUnsupportedMessages, "; "))
}
return "", fmt.Errorf("%w: errors encountered are: %s",
ErrIPTablesNotSupported, strings.Join(allUnsupportedMessages, "; "))
} }
func testIptablesPath(ctx context.Context, path string, func testIptablesPath(ctx context.Context, path string,
@@ -56,14 +75,6 @@ func testIptablesPath(ctx context.Context, path string,
"-A", "OUTPUT", "-o", testInterfaceName, "-j", "DROP") "-A", "OUTPUT", "-o", testInterfaceName, "-j", "DROP")
output, err := runner.Run(cmd) output, err := runner.Run(cmd)
if err != nil { if err != nil {
if isPermissionDenied(output) {
// If the error is related to a denied permission,
// return an error describing what to do from an end-user
// perspective. This is a critical error and likely
// applies to all iptables.
criticalErr = fmt.Errorf("%w: %s", ErrNetAdminMissing, output)
return false, "", criticalErr
}
unsupportedMessage = fmt.Sprintf("%s (%s)", output, err) unsupportedMessage = fmt.Sprintf("%s (%s)", output, err)
return false, unsupportedMessage, nil return false, unsupportedMessage, nil
} }
@@ -84,10 +95,6 @@ func testIptablesPath(ctx context.Context, path string,
cmd = exec.CommandContext(ctx, path, "-L", "INPUT") cmd = exec.CommandContext(ctx, path, "-L", "INPUT")
output, err = runner.Run(cmd) output, err = runner.Run(cmd)
if err != nil { if err != nil {
if isPermissionDenied(output) {
criticalErr = fmt.Errorf("%w: %s", ErrNetAdminMissing, output)
return false, "", criticalErr
}
unsupportedMessage = fmt.Sprintf("%s (%s)", output, err) unsupportedMessage = fmt.Sprintf("%s (%s)", output, err)
return false, unsupportedMessage, nil return false, unsupportedMessage, nil
} }
@@ -109,10 +116,6 @@ func testIptablesPath(ctx context.Context, path string,
cmd = exec.CommandContext(ctx, path, "--policy", "INPUT", inputPolicy) cmd = exec.CommandContext(ctx, path, "--policy", "INPUT", inputPolicy)
output, err = runner.Run(cmd) output, err = runner.Run(cmd)
if err != nil { if err != nil {
if isPermissionDenied(output) {
criticalErr = fmt.Errorf("%w: %s", ErrNetAdminMissing, output)
return false, "", criticalErr
}
unsupportedMessage = fmt.Sprintf("%s (%s)", output, err) unsupportedMessage = fmt.Sprintf("%s (%s)", output, err)
return false, unsupportedMessage, nil return false, unsupportedMessage, nil
} }

View File

@@ -8,10 +8,130 @@ import (
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/qdm12/golibs/command" "github.com/qdm12/golibs/command"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
//go:generate mockgen -destination=runner_mock_test.go -package $GOPACKAGE github.com/qdm12/golibs/command Runner //go:generate mockgen -destination=runner_mock_test.go -package $GOPACKAGE github.com/qdm12/golibs/command Runner
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) command.Runner
iptablesPathsToTry []string
iptablesPath string
errSentinel error
errMessage string
}{
"critical error when checking": {
buildRunner: func(ctrl *gomock.Controller) command.Runner {
runner := NewMockRunner(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) command.Runner {
runner := NewMockRunner(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) command.Runner {
runner := NewMockRunner(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) command.Runner {
runner := NewMockRunner(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 {
testCase := testCase
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) { func Test_testIptablesPath(t *testing.T) {
t.Parallel() t.Parallel()
@@ -20,17 +140,6 @@ func Test_testIptablesPath(t *testing.T) {
errDummy := errors.New("exit code 4") errDummy := errors.New("exit code 4")
const inputPolicy = "ACCEPT" const inputPolicy = "ACCEPT"
appendTestRuleMatcher := newCmdMatcher(path,
"^-A$", "^OUTPUT$", "^-o$", "^[a-z0-9]{15}$",
"^-j$", "^DROP$")
deleteTestRuleMatcher := newCmdMatcher(path,
"^-D$", "^OUTPUT$", "^-o$", "^[a-z0-9]{15}$",
"^-j$", "^DROP$")
listInputRulesMatcher := newCmdMatcher(path,
"^-L$", "^INPUT$")
setPolicyMatcher := newCmdMatcher(path,
"^--policy$", "^INPUT$", "^"+inputPolicy+"$")
testCases := map[string]struct { testCases := map[string]struct {
buildRunner func(ctrl *gomock.Controller) command.Runner buildRunner func(ctrl *gomock.Controller) command.Runner
ok bool ok bool
@@ -41,18 +150,16 @@ func Test_testIptablesPath(t *testing.T) {
"append test rule permission denied": { "append test rule permission denied": {
buildRunner: func(ctrl *gomock.Controller) command.Runner { buildRunner: func(ctrl *gomock.Controller) command.Runner {
runner := NewMockRunner(ctrl) runner := NewMockRunner(ctrl)
runner.EXPECT().Run(appendTestRuleMatcher). runner.EXPECT().Run(newAppendTestRuleMatcher(path)).
Return("Permission denied (you must be root)", errDummy) Return("Permission denied (you must be root)", errDummy)
return runner return runner
}, },
criticalErrWrapped: ErrNetAdminMissing, unsupportedMessage: "Permission denied (you must be root) (exit code 4)",
criticalErrMessage: "NET_ADMIN capability is missing: " +
"Permission denied (you must be root)",
}, },
"append test rule unsupported": { "append test rule unsupported": {
buildRunner: func(ctrl *gomock.Controller) command.Runner { buildRunner: func(ctrl *gomock.Controller) command.Runner {
runner := NewMockRunner(ctrl) runner := NewMockRunner(ctrl)
runner.EXPECT().Run(appendTestRuleMatcher). runner.EXPECT().Run(newAppendTestRuleMatcher(path)).
Return("some output", errDummy) Return("some output", errDummy)
return runner return runner
}, },
@@ -61,8 +168,8 @@ func Test_testIptablesPath(t *testing.T) {
"remove test rule error": { "remove test rule error": {
buildRunner: func(ctrl *gomock.Controller) command.Runner { buildRunner: func(ctrl *gomock.Controller) command.Runner {
runner := NewMockRunner(ctrl) runner := NewMockRunner(ctrl)
runner.EXPECT().Run(appendTestRuleMatcher).Return("", nil) runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(deleteTestRuleMatcher). runner.EXPECT().Run(newDeleteTestRuleMatcher(path)).
Return("some output", errDummy) Return("some output", errDummy)
return runner return runner
}, },
@@ -72,22 +179,20 @@ func Test_testIptablesPath(t *testing.T) {
"list input rules permission denied": { "list input rules permission denied": {
buildRunner: func(ctrl *gomock.Controller) command.Runner { buildRunner: func(ctrl *gomock.Controller) command.Runner {
runner := NewMockRunner(ctrl) runner := NewMockRunner(ctrl)
runner.EXPECT().Run(appendTestRuleMatcher).Return("", nil) runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(deleteTestRuleMatcher).Return("", nil) runner.EXPECT().Run(newDeleteTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(listInputRulesMatcher). runner.EXPECT().Run(newListInputRulesMatcher(path)).
Return("Permission denied (you must be root)", errDummy) Return("Permission denied (you must be root)", errDummy)
return runner return runner
}, },
criticalErrWrapped: ErrNetAdminMissing, unsupportedMessage: "Permission denied (you must be root) (exit code 4)",
criticalErrMessage: "NET_ADMIN capability is missing: " +
"Permission denied (you must be root)",
}, },
"list input rules unsupported": { "list input rules unsupported": {
buildRunner: func(ctrl *gomock.Controller) command.Runner { buildRunner: func(ctrl *gomock.Controller) command.Runner {
runner := NewMockRunner(ctrl) runner := NewMockRunner(ctrl)
runner.EXPECT().Run(appendTestRuleMatcher).Return("", nil) runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(deleteTestRuleMatcher).Return("", nil) runner.EXPECT().Run(newDeleteTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(listInputRulesMatcher). runner.EXPECT().Run(newListInputRulesMatcher(path)).
Return("some output", errDummy) Return("some output", errDummy)
return runner return runner
}, },
@@ -96,9 +201,9 @@ func Test_testIptablesPath(t *testing.T) {
"list input rules no policy": { "list input rules no policy": {
buildRunner: func(ctrl *gomock.Controller) command.Runner { buildRunner: func(ctrl *gomock.Controller) command.Runner {
runner := NewMockRunner(ctrl) runner := NewMockRunner(ctrl)
runner.EXPECT().Run(appendTestRuleMatcher).Return("", nil) runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(deleteTestRuleMatcher).Return("", nil) runner.EXPECT().Run(newDeleteTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(listInputRulesMatcher). runner.EXPECT().Run(newListInputRulesMatcher(path)).
Return("some\noutput", nil) Return("some\noutput", nil)
return runner return runner
}, },
@@ -108,26 +213,24 @@ func Test_testIptablesPath(t *testing.T) {
"set policy permission denied": { "set policy permission denied": {
buildRunner: func(ctrl *gomock.Controller) command.Runner { buildRunner: func(ctrl *gomock.Controller) command.Runner {
runner := NewMockRunner(ctrl) runner := NewMockRunner(ctrl)
runner.EXPECT().Run(appendTestRuleMatcher).Return("", nil) runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(deleteTestRuleMatcher).Return("", nil) runner.EXPECT().Run(newDeleteTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(listInputRulesMatcher). runner.EXPECT().Run(newListInputRulesMatcher(path)).
Return("\nChain INPUT (policy "+inputPolicy+")\nxx\n", nil) Return("\nChain INPUT (policy "+inputPolicy+")\nxx\n", nil)
runner.EXPECT().Run(setPolicyMatcher). runner.EXPECT().Run(newSetPolicyMatcher(path, inputPolicy)).
Return("Permission denied (you must be root)", errDummy) Return("Permission denied (you must be root)", errDummy)
return runner return runner
}, },
criticalErrWrapped: ErrNetAdminMissing, unsupportedMessage: "Permission denied (you must be root) (exit code 4)",
criticalErrMessage: "NET_ADMIN capability is missing: " +
"Permission denied (you must be root)",
}, },
"set policy unsupported": { "set policy unsupported": {
buildRunner: func(ctrl *gomock.Controller) command.Runner { buildRunner: func(ctrl *gomock.Controller) command.Runner {
runner := NewMockRunner(ctrl) runner := NewMockRunner(ctrl)
runner.EXPECT().Run(appendTestRuleMatcher).Return("", nil) runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(deleteTestRuleMatcher).Return("", nil) runner.EXPECT().Run(newDeleteTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(listInputRulesMatcher). runner.EXPECT().Run(newListInputRulesMatcher(path)).
Return("\nChain INPUT (policy "+inputPolicy+")\nxx\n", nil) Return("\nChain INPUT (policy "+inputPolicy+")\nxx\n", nil)
runner.EXPECT().Run(setPolicyMatcher). runner.EXPECT().Run(newSetPolicyMatcher(path, inputPolicy)).
Return("some output", errDummy) Return("some output", errDummy)
return runner return runner
}, },
@@ -136,11 +239,12 @@ func Test_testIptablesPath(t *testing.T) {
"success": { "success": {
buildRunner: func(ctrl *gomock.Controller) command.Runner { buildRunner: func(ctrl *gomock.Controller) command.Runner {
runner := NewMockRunner(ctrl) runner := NewMockRunner(ctrl)
runner.EXPECT().Run(appendTestRuleMatcher).Return("", nil) runner.EXPECT().Run(newAppendTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(deleteTestRuleMatcher).Return("", nil) runner.EXPECT().Run(newDeleteTestRuleMatcher(path)).Return("", nil)
runner.EXPECT().Run(listInputRulesMatcher). runner.EXPECT().Run(newListInputRulesMatcher(path)).
Return("\nChain INPUT (policy "+inputPolicy+")\nxx\n", nil) Return("\nChain INPUT (policy "+inputPolicy+")\nxx\n", nil)
runner.EXPECT().Run(setPolicyMatcher).Return("some output", nil) runner.EXPECT().Run(newSetPolicyMatcher(path, inputPolicy)).
Return("some output", nil)
return runner return runner
}, },
ok: true, ok: true,