Skip to content

Commit

Permalink
Add iptables-restore at startup to add order in iptables rules
Browse files Browse the repository at this point in the history
Add iptables-restore wrapper and apply it in iptables
  • Loading branch information
ryarnyah authored and Pierre MORVAN committed Mar 10, 2020
1 parent 0672e0b commit 0eee12d
Show file tree
Hide file tree
Showing 4 changed files with 480 additions and 35 deletions.
104 changes: 83 additions & 21 deletions network/iptables.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ package network

import (
"fmt"
"strings"

log "github.com/golang/glog"

Expand Down Expand Up @@ -97,21 +96,74 @@ func ipTablesRulesExist(ipt IPTables, rules []IPTablesRule) (bool, error) {
return true, nil
}

func ipTablesCleanAndBuild(ipt IPTables, rules []IPTablesRule) (IPTablesRestoreRules, error) {
tablesRules := IPTablesRestoreRules{}

// Build delete rules
for _, rule := range rules {
exists, err := ipt.Exists(rule.table, rule.chain, rule.rulespec...)
if err != nil {
// this shouldn't ever happen
return nil, fmt.Errorf("failed to check rule existence: %v", err)
}
if exists {
if _, ok := tablesRules[rule.table]; !ok {
tablesRules[rule.table] = []IPTablesRestoreRuleSpec{}
}
tablesRules[rule.table] = append(tablesRules[rule.table], append(IPTablesRestoreRuleSpec{"-D", rule.chain}, rule.rulespec...))
}
tablesRules[rule.table] = append(tablesRules[rule.table], append(IPTablesRestoreRuleSpec{"-A", rule.chain}, rule.rulespec...))
}

return tablesRules, nil
}

func ipTablesBootstrap(ipt IPTables, iptRestore IPTablesRestore, rules []IPTablesRule) error {
tablesRules, err := ipTablesCleanAndBuild(ipt, rules)
if err != nil {
// if we can't find iptables, give up and return
return fmt.Errorf("Failed to setup IPTables-restore payload: %v", err)
}

log.V(6).Infof("trying to run iptable-restore < %+v", tablesRules)

err = iptRestore.ApplyPartial(tablesRules)
if err != nil {
return fmt.Errorf("failed to apply partial iptables-restore %v", err)
}

log.Infof("bootstrap done")

return nil
}

func SetupAndEnsureIPTables(rules []IPTablesRule, resyncPeriod int) {
ipt, err := iptables.New()
if err != nil {
// if we can't find iptables, give up and return
log.Errorf("Failed to setup IPTables. iptables binary was not found: %v", err)
return
}
iptRestore, err := NewIPTablesRestore()
if err != nil {
// if we can't find iptables, give up and return
log.Errorf("Failed to setup IPTables. iptables binary was not found: %v", err)
return
}

err = ipTablesBootstrap(ipt, iptRestore, rules)
if err != nil {
// if we can't find iptables, give up and return
log.Errorf("Failed to bootstrap IPTables: %v", err)
}

defer func() {
teardownIPTables(ipt, rules)
teardownIPTables(ipt, iptRestore, rules)
}()

for {
// Ensure that all the iptables rules exist every 5 seconds
if err := ensureIPTables(ipt, rules); err != nil {
if err := ensureIPTables(ipt, iptRestore, rules); err != nil {
log.Errorf("Failed to ensure iptables rules: %v", err)
}

Expand All @@ -127,11 +179,17 @@ func DeleteIPTables(rules []IPTablesRule) error {
log.Errorf("Failed to setup IPTables. iptables binary was not found: %v", err)
return err
}
teardownIPTables(ipt, rules)
iptRestore, err := NewIPTablesRestore()
if err != nil {
// if we can't find iptables, give up and return
log.Errorf("Failed to setup IPTables-restore: %v", err)
return err
}
teardownIPTables(ipt, iptRestore, rules)
return nil
}

func ensureIPTables(ipt IPTables, rules []IPTablesRule) error {
func ensureIPTables(ipt IPTables, iptRestore IPTablesRestore, rules []IPTablesRule) error {
exists, err := ipTablesRulesExist(ipt, rules)
if err != nil {
return fmt.Errorf("Error checking rule existence: %v", err)
Expand All @@ -143,30 +201,34 @@ func ensureIPTables(ipt IPTables, rules []IPTablesRule) error {
// Otherwise, teardown all the rules and set them up again
// We do this because the order of the rules is important
log.Info("Some iptables rules are missing; deleting and recreating rules")
teardownIPTables(ipt, rules)
if err = setupIPTables(ipt, rules); err != nil {
err = ipTablesBootstrap(ipt, iptRestore, rules)
if err != nil {
// if we can't find iptables, give up and return
return fmt.Errorf("Error setting up rules: %v", err)
}
return nil
}

func setupIPTables(ipt IPTables, rules []IPTablesRule) error {
func teardownIPTables(ipt IPTables, iptr IPTablesRestore, rules []IPTablesRule) {
tablesRules := IPTablesRestoreRules{}

// Build delete rules
for _, rule := range rules {
log.Info("Adding iptables rule: ", strings.Join(rule.rulespec, " "))
err := ipt.AppendUnique(rule.table, rule.chain, rule.rulespec...)
exists, err := ipt.Exists(rule.table, rule.chain, rule.rulespec...)
if err != nil {
return fmt.Errorf("failed to insert IPTables rule: %v", err)
// this shouldn't ever happen
log.Errorf("failed to check rule existence: %v", err)
return
}
if exists {
if _, ok := tablesRules[rule.table]; !ok {
tablesRules[rule.table] = []IPTablesRestoreRuleSpec{}
}
tablesRules[rule.table] = append(tablesRules[rule.table], append(IPTablesRestoreRuleSpec{"-D", rule.chain}, rule.rulespec...))
}
}

return nil
}

func teardownIPTables(ipt IPTables, rules []IPTablesRule) {
for _, rule := range rules {
log.Info("Deleting iptables rule: ", strings.Join(rule.rulespec, " "))
// We ignore errors here because if there's an error it's almost certainly because the rule
// doesn't exist, which is fine (we don't need to delete rules that don't exist)
ipt.Delete(rule.table, rule.chain, rule.rulespec...)
err := iptr.ApplyPartial(tablesRules)
if err != nil {
log.Errorf("unable to teardown iptables: %v", err)
}
}
213 changes: 213 additions & 0 deletions network/iptables_restore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
// Copyright 2015 flannel authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build !windows

package network

import (
"bytes"
"fmt"
"io"
"os/exec"
"regexp"
"strconv"
"strings"

"github.com/coreos/go-iptables/iptables"
log "github.com/golang/glog"
)

const (
ipTablesRestoreCmd string = "iptables-restore"
ip6TablesRestoreCmd string = "ip6tables-restore"
)

// IPTablesRestore wrapper for iptables-restore
type IPTablesRestore interface {
// ApplyFully apply all rules and flush chains
ApplyFully(rules IPTablesRestoreRules) error
// ApplyPartial apply without flush chains
ApplyPartial(rules IPTablesRestoreRules) error
}

// ipTablesRestore internale type
type ipTablesRestore struct {
path string
protocol iptables.Protocol
hasWait bool
}

// IPTablesRestoreRules represents iptables-restore table block
type IPTablesRestoreRules map[string][]IPTablesRestoreRuleSpec

// IPTablesRestoreRuleSpec represents one rule spec delimited by space
type IPTablesRestoreRuleSpec []string

// NewIPTablesRestore build new IPTablesRestore for IPv4
func NewIPTablesRestore() (IPTablesRestore, error) {
return NewIPTablesRestoreWithProtocol(iptables.ProtocolIPv4)
}

// NewIPTablesRestoreWithProtocol build new IPTablesRestore for supplied protocol
func NewIPTablesRestoreWithProtocol(protocol iptables.Protocol) (IPTablesRestore, error) {
cmd := getIptablesRestoreCommand(protocol)
_, err := exec.LookPath(cmd)
if err != nil {
return nil, err
}
hasWait, err := getIptablesRestoreSupport(cmd)
if err != nil {
return nil, err
}

ipt := ipTablesRestore{
path: cmd,
protocol: protocol,
hasWait: hasWait,
}
return &ipt, nil
}

// ApplyFully apply all rules and flush chains
func (iptr *ipTablesRestore) ApplyFully(rules IPTablesRestoreRules) error {
payload := buildIPTablesRestorePayload(rules)

log.V(6).Infof("trying to run with payload %s", payload)
stdout, stderr, err := iptr.runWithOutput([]string{}, bytes.NewBuffer([]byte(payload)))
if err != nil {
return fmt.Errorf("unable to run iptables-restore (%s, %s): %v", stdout, stderr, err)
}
return nil
}

// ApplyPartial apply without flush chains
func (iptr *ipTablesRestore) ApplyPartial(rules IPTablesRestoreRules) error {
payload := buildIPTablesRestorePayload(rules)

log.V(6).Infof("trying to run with payload %s", payload)
stdout, stderr, err := iptr.runWithOutput([]string{"--noflush"}, bytes.NewBuffer([]byte(payload)))
if err != nil {
return fmt.Errorf("unable to run iptables-restore (%s, %s): %v", stdout, stderr, err)
}
return nil
}

// runWithOutput runs an iptables command with the given arguments,
// writing any stdout output to the given writer
func (ipt *ipTablesRestore) runWithOutput(args []string, stdin io.Reader) (string, string, error) {
var stdout bytes.Buffer
var stderr bytes.Buffer
if ipt.hasWait {
args = append(args, "--wait")
}

cmd := exec.Command(ipt.path, args...)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
cmd.Stdin = stdin

if err := cmd.Run(); err != nil {
return "", "", err
}

return stdout.String(), stderr.String(), nil
}

// buildIPTablesRestorePayload build table/COMMIT payload
func buildIPTablesRestorePayload(tableRules IPTablesRestoreRules) string {
iptablesRestorePayload := ""
for table, rules := range tableRules {
iptablesRestorePayload += "*" + table + "\n"

for _, rule := range rules {
iptablesRestorePayload += strings.Join(rule[:], " ") + "\n"
}

iptablesRestorePayload += "COMMIT\n"
}
return iptablesRestorePayload
}

// getIptablesRestoreSupport get current iptables-restore support
func getIptablesRestoreSupport(path string) (hasWait bool, err error) {
version, err := getIptablesRestoreVersionString(path)
if err != nil {
return false, err
}
v1, v2, v3, err := extractIptablesRestoreVersion(version)
if err != nil {
return false, err
}
return ipTablesHasWaitSupport(v1, v2, v3), nil
}

// Checks if an iptables-restore version is after 1.4.20, when --wait was added
func ipTablesHasWaitSupport(v1, v2, v3 int) bool {
if v1 > 1 {
return true
}
if v1 == 1 && v2 > 4 {
return true
}
if v1 == 1 && v2 == 4 && v3 >= 20 {
return true
}
return false
}

// extractIptablesRestoreVersion returns the first three components of the iptables-restore version
// e.g. "iptables-restore v1.3.66" would return (1, 3, 66, nil)
func extractIptablesRestoreVersion(str string) (int, int, int, error) {
versionMatcher := regexp.MustCompile(`v([0-9]+)\.([0-9]+)\.([0-9]+)`)
result := versionMatcher.FindStringSubmatch(str)
if result == nil {
return 0, 0, 0, fmt.Errorf("no iptables-restore version found in string: %s", str)
}

v1, err := strconv.Atoi(result[1])
if err != nil {
return 0, 0, 0, err
}

v2, err := strconv.Atoi(result[2])
if err != nil {
return 0, 0, 0, err
}

v3, err := strconv.Atoi(result[3])
if err != nil {
return 0, 0, 0, err
}
return v1, v2, v3, nil
}

// getIptablesRestoreVersionString run iptables-restore --version
func getIptablesRestoreVersionString(path string) (string, error) {
cmd := exec.Command(path, "--version")
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return "", fmt.Errorf("unable to find iptables-restore version: %v", err)
}
return out.String(), nil
}

// getIptablesRestoreCommand returns the correct command for the given protocol, either "iptables-restore" or "ip6tables-restore".
func getIptablesRestoreCommand(proto iptables.Protocol) string {
if proto == iptables.ProtocolIPv6 {
return ip6TablesRestoreCmd
}
return ipTablesRestoreCmd
}
Loading

0 comments on commit 0eee12d

Please sign in to comment.