-
-
Notifications
You must be signed in to change notification settings - Fork 367
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(firewall): delete chain rules by line number
- Loading branch information
Showing
14 changed files
with
1,100 additions
and
64 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
package firewall | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"os/exec" | ||
"strings" | ||
) | ||
|
||
func isDeleteInstruction(instruction string) bool { | ||
fields := strings.Fields(instruction) | ||
for i := 0; i < len(fields); i += 2 { | ||
switch fields[i] { | ||
case "-D", "--delete": //nolint:goconst | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
|
||
func deleteIPTablesRule(ctx context.Context, iptablesBinary, instruction string, | ||
runner Runner, logger Logger) (err error) { | ||
targetRule, err := parseIptablesInstruction(instruction) | ||
if err != nil { | ||
return fmt.Errorf("parsing iptables command: %w", err) | ||
} | ||
|
||
lineNumber, err := findLineNumber(ctx, iptablesBinary, | ||
targetRule, runner, logger) | ||
if err != nil { | ||
return fmt.Errorf("finding iptables chain rule line number: %w", err) | ||
} else if lineNumber == 0 { | ||
logger.Debug("rule matching \"" + instruction + "\" not found") | ||
return nil | ||
} | ||
logger.Debug(fmt.Sprintf("found iptables chain rule matching %q at line number %d", | ||
instruction, lineNumber)) | ||
|
||
cmd := exec.CommandContext(ctx, iptablesBinary, "-t", targetRule.table, | ||
"-D", targetRule.chain, fmt.Sprint(lineNumber)) // #nosec G204 | ||
logger.Debug(cmd.String()) | ||
output, err := runner.Run(cmd) | ||
if err != nil { | ||
err = fmt.Errorf("command failed: %q: %w", cmd, err) | ||
if output != "" { | ||
err = fmt.Errorf("%w: %s", err, output) | ||
} | ||
return err | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// findLineNumber finds the line number of an iptables rule. | ||
// It returns 0 if the rule is not found. | ||
func findLineNumber(ctx context.Context, iptablesBinary string, | ||
instruction iptablesInstruction, runner Runner, logger Logger) ( | ||
lineNumber uint16, err error) { | ||
listFlags := []string{"-t", instruction.table, "-L", instruction.chain, | ||
"--line-numbers", "-n", "-v"} | ||
cmd := exec.CommandContext(ctx, iptablesBinary, listFlags...) // #nosec G204 | ||
logger.Debug(cmd.String()) | ||
output, err := runner.Run(cmd) | ||
if err != nil { | ||
err = fmt.Errorf("command failed: %q: %w", cmd, err) | ||
if output != "" { | ||
err = fmt.Errorf("%w: %s", err, output) | ||
} | ||
return 0, err | ||
} | ||
|
||
chain, err := parseChain(output) | ||
if err != nil { | ||
return 0, fmt.Errorf("parsing chain list: %w", err) | ||
} | ||
|
||
for _, rule := range chain.rules { | ||
if instruction.equalToRule(instruction.table, chain.name, rule) { | ||
return rule.lineNumber, nil | ||
} | ||
} | ||
|
||
return 0, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
package firewall | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"testing" | ||
|
||
"github.com/golang/mock/gomock" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func newCmdMatcherListRules(iptablesBinary, table, chain string) *cmdMatcher { //nolint:unparam | ||
return newCmdMatcher(iptablesBinary, "^-t$", "^"+table+"$", "^-L$", "^"+chain+"$", | ||
"^--line-numbers$", "^-n$", "^-v$") | ||
} | ||
|
||
func Test_deleteIPTablesRule(t *testing.T) { | ||
t.Parallel() | ||
|
||
const iptablesBinary = "/sbin/iptables" | ||
errTest := errors.New("test error") | ||
|
||
testCases := map[string]struct { | ||
instruction string | ||
makeRunner func(ctrl *gomock.Controller) *MockRunner | ||
makeLogger func(ctrl *gomock.Controller) *MockLogger | ||
errWrapped error | ||
errMessage string | ||
}{ | ||
"invalid_instruction": { | ||
instruction: "invalid", | ||
errWrapped: ErrIptablesCommandMalformed, | ||
errMessage: "parsing iptables command: iptables command is malformed: " + | ||
"fields count 1 is not even: \"invalid\"", | ||
}, | ||
"list_error": { | ||
instruction: "-t nat --delete PREROUTING -i tun0 -p tcp --dport 43716 -j REDIRECT --to-ports 5678", | ||
makeRunner: func(ctrl *gomock.Controller) *MockRunner { | ||
runner := NewMockRunner(ctrl) | ||
runner.EXPECT(). | ||
Run(newCmdMatcherListRules(iptablesBinary, "nat", "PREROUTING")). | ||
Return("", errTest) | ||
return runner | ||
}, | ||
makeLogger: func(ctrl *gomock.Controller) *MockLogger { | ||
logger := NewMockLogger(ctrl) | ||
logger.EXPECT().Debug("/sbin/iptables -t nat -L PREROUTING --line-numbers -n -v") | ||
return logger | ||
}, | ||
errWrapped: errTest, | ||
errMessage: `finding iptables chain rule line number: command failed: ` + | ||
`"/sbin/iptables -t nat -L PREROUTING --line-numbers -n -v": test error`, | ||
}, | ||
"rule_not_found": { | ||
instruction: "-t nat --delete PREROUTING -i tun0 -p tcp --dport 43716 -j REDIRECT --to-ports 5678", | ||
makeRunner: func(ctrl *gomock.Controller) *MockRunner { | ||
runner := NewMockRunner(ctrl) | ||
runner.EXPECT().Run(newCmdMatcherListRules(iptablesBinary, "nat", "PREROUTING")). | ||
Return(`Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes) | ||
num pkts bytes target prot opt in out source destination | ||
1 0 0 REDIRECT 6 -- tun0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:5000 redir ports 9999`, //nolint:lll | ||
nil) | ||
return runner | ||
}, | ||
makeLogger: func(ctrl *gomock.Controller) *MockLogger { | ||
logger := NewMockLogger(ctrl) | ||
logger.EXPECT().Debug("/sbin/iptables -t nat -L PREROUTING --line-numbers -n -v") | ||
logger.EXPECT().Debug("rule matching \"-t nat --delete PREROUTING " + | ||
"-i tun0 -p tcp --dport 43716 -j REDIRECT --to-ports 5678\" not found") | ||
return logger | ||
}, | ||
}, | ||
"rule_found_delete_error": { | ||
instruction: "-t nat --delete PREROUTING -i tun0 -p tcp --dport 43716 -j REDIRECT --to-ports 5678", | ||
makeRunner: func(ctrl *gomock.Controller) *MockRunner { | ||
runner := NewMockRunner(ctrl) | ||
runner.EXPECT().Run(newCmdMatcherListRules(iptablesBinary, "nat", "PREROUTING")). | ||
Return("Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)\n"+ | ||
"num pkts bytes target prot opt in out source destination \n"+ | ||
"1 0 0 REDIRECT 6 -- tun0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:5000 redir ports 9999\n"+ //nolint:lll | ||
"2 0 0 REDIRECT 6 -- tun0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:43716 redir ports 5678\n", //nolint:lll | ||
nil) | ||
runner.EXPECT().Run(newCmdMatcher(iptablesBinary, "^-t$", "^nat$", | ||
"^-D$", "^PREROUTING$", "^2$")).Return("details", errTest) | ||
return runner | ||
}, | ||
makeLogger: func(ctrl *gomock.Controller) *MockLogger { | ||
logger := NewMockLogger(ctrl) | ||
logger.EXPECT().Debug("/sbin/iptables -t nat -L PREROUTING --line-numbers -n -v") | ||
logger.EXPECT().Debug("found iptables chain rule matching \"-t nat --delete PREROUTING " + | ||
"-i tun0 -p tcp --dport 43716 -j REDIRECT --to-ports 5678\" at line number 2") | ||
logger.EXPECT().Debug("/sbin/iptables -t nat -D PREROUTING 2") | ||
return logger | ||
}, | ||
errWrapped: errTest, | ||
errMessage: "command failed: \"/sbin/iptables -t nat -D PREROUTING 2\": test error: details", | ||
}, | ||
"rule_found_delete_success": { | ||
instruction: "-t nat --delete PREROUTING -i tun0 -p tcp --dport 43716 -j REDIRECT --to-ports 5678", | ||
makeRunner: func(ctrl *gomock.Controller) *MockRunner { | ||
runner := NewMockRunner(ctrl) | ||
runner.EXPECT().Run(newCmdMatcherListRules(iptablesBinary, "nat", "PREROUTING")). | ||
Return("Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)\n"+ | ||
"num pkts bytes target prot opt in out source destination \n"+ | ||
"1 0 0 REDIRECT 6 -- tun0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:5000 redir ports 9999\n"+ //nolint:lll | ||
"2 0 0 REDIRECT 6 -- tun0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:43716 redir ports 5678\n", //nolint:lll | ||
nil) | ||
runner.EXPECT().Run(newCmdMatcher(iptablesBinary, "^-t$", "^nat$", | ||
"^-D$", "^PREROUTING$", "^2$")).Return("", nil) | ||
return runner | ||
}, | ||
makeLogger: func(ctrl *gomock.Controller) *MockLogger { | ||
logger := NewMockLogger(ctrl) | ||
logger.EXPECT().Debug("/sbin/iptables -t nat -L PREROUTING --line-numbers -n -v") | ||
logger.EXPECT().Debug("found iptables chain rule matching \"-t nat --delete PREROUTING " + | ||
"-i tun0 -p tcp --dport 43716 -j REDIRECT --to-ports 5678\" at line number 2") | ||
logger.EXPECT().Debug("/sbin/iptables -t nat -D PREROUTING 2") | ||
return logger | ||
}, | ||
}, | ||
} | ||
|
||
for name, testCase := range testCases { | ||
testCase := testCase | ||
t.Run(name, func(t *testing.T) { | ||
t.Parallel() | ||
ctrl := gomock.NewController(t) | ||
|
||
ctx := context.Background() | ||
instruction := testCase.instruction | ||
var runner *MockRunner | ||
if testCase.makeRunner != nil { | ||
runner = testCase.makeRunner(ctrl) | ||
} | ||
var logger *MockLogger | ||
if testCase.makeLogger != nil { | ||
logger = testCase.makeLogger(ctrl) | ||
} | ||
|
||
err := deleteIPTablesRule(ctx, iptablesBinary, instruction, runner, logger) | ||
|
||
assert.ErrorIs(t, err, testCase.errWrapped) | ||
if testCase.errWrapped != nil { | ||
assert.EqualError(t, err, testCase.errMessage) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package firewall | ||
|
||
import "github.com/qdm12/golibs/command" | ||
|
||
type Runner interface { | ||
Run(cmd command.ExecCmd) (output string, err error) | ||
} | ||
|
||
type Logger interface { | ||
Debug(s string) | ||
Info(s string) | ||
Error(s string) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.