Skip to content

Commit

Permalink
add nftables firewall backend
Browse files Browse the repository at this point in the history
Resolves: #461

Signed-off-by: Paul Greenberg <greenpau@outlook.com>
  • Loading branch information
greenpau committed Mar 16, 2020
1 parent 47a9fd8 commit f2cecbe
Show file tree
Hide file tree
Showing 2 changed files with 326 additions and 0 deletions.
2 changes: 2 additions & 0 deletions plugins/meta/firewall/firewall.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ func getBackend(conf *FirewallNetConf) (FirewallBackend, error) {
switch conf.Backend {
case "iptables":
return newIptablesBackend(conf)
case "nftables":
return newNftablesBackend(conf)
case "firewalld":
return newFirewalldBackend(conf)
}
Expand Down
324 changes: 324 additions & 0 deletions plugins/meta/firewall/nftables.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,324 @@
// Copyright 2018 CNI 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.

package main

import (
"bytes"
"fmt"
"github.com/containernetworking/cni/pkg/types/current"
"net"
"os/exec"
"strconv"
"strings"
)

type nftBackend struct {
targetTable string
targetChain string
targetHandle uint64
targetInterface string
targetNetwork *net.IPNet
rules []*nftRule
markedDelRules []uint64
newRules [][]string
}

type nftRule struct {
text string
handle uint64
verdict string
}

func newNftRule(s string) (*nftRule, error) {
r := &nftRule{
text: s,
verdict: "unknown",
}
if err := r.parseText(); err != nil {
return nil, err
}
if strings.HasSuffix(r.text, " accept") {
r.verdict = "accept"
}
if strings.HasSuffix(r.text, " drop") {
r.verdict = "drop"
}
return r, nil
}

func (r *nftRule) parseText() error {
offset := strings.LastIndex(r.text, "# handle ")
if offset > 0 {
// The rule handle was found
handle := strings.TrimLeft(r.text[offset:], "# handle ")
r.text = strings.TrimSpace(r.text[:offset-1])
handleID, err := strconv.ParseUint(handle, 0, 64)
if err != nil {
return err
}
r.handle = handleID
}

return nil
}

// nftBackend implements the FirewallBackend interface
var _ FirewallBackend = &nftBackend{}

func newNftablesBackend(conf *FirewallNetConf) (FirewallBackend, error) {
backend := &nftBackend{
targetTable: "filter",
targetChain: "FORWARD",
targetHandle: 0,
rules: []*nftRule{},
markedDelRules: []uint64{},
newRules: [][]string{},
}
return backend, nil
}

func (nb *nftBackend) execCommand(args []string) ([]string, []string, error) {
var stdout, stderr bytes.Buffer
cmd := exec.Command("nft", args...)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return []string{}, []string{}, fmt.Errorf("Error executing %s: %s", args, err)
}

stdoutString := stdout.String()
stderrString := stderr.String()
stdoutLines := strings.Split(stdoutString, "\n")
stderrLines := strings.Split(stderrString, "\n")
return stdoutLines, stderrLines, nil
}

func (nb *nftBackend) getRules() error {
cmdArgs := []string{"list", "chain", "ip", nb.targetTable, nb.targetChain, "-a"}
stdoutLines, _, err := nb.execCommand(cmdArgs)
if err != nil {
return err
}
for _, line := range stdoutLines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
if strings.HasPrefix(line, "table") {
continue
}
if strings.HasPrefix(line, "type") {
continue
}
if strings.HasPrefix(line, "chain") {
continue
}
if strings.HasPrefix(line, "}") {
continue
}
rule, err := newNftRule(line)
if err != nil {
return err
}
if nb.targetHandle == 0 {
nb.targetHandle = rule.handle
}
nb.rules = append(nb.rules, rule)
}
return nil
}

func (nb *nftBackend) isRuleChangeRequired() bool {
var requireChange bool
var isIntraInterfaceRuleExists bool
var isInboundInterfaceRuleExists bool
var isOutboundInterfaceRuleExists bool

if len(nb.rules) == 0 {
return true
}

intraInterfaceRule := fmt.Sprintf(
"iifname \"%s\" oifname \"%s\"",
nb.targetInterface,
nb.targetInterface,
)

inboundInterfaceRule := fmt.Sprintf(
"oifname \"%s\" ip daddr %s ct state established,related",
nb.targetInterface,
nb.targetNetwork.String(),
)

outboundInterfaceRule := fmt.Sprintf(
"iifname \"%s\" ip saddr %s",
nb.targetInterface,
nb.targetNetwork.String(),
)

for _, rule := range nb.rules {
if rule.verdict != "accept" {
continue
}
if strings.HasPrefix(rule.text, intraInterfaceRule) {
isIntraInterfaceRuleExists = true
continue
}
if strings.HasPrefix(rule.text, inboundInterfaceRule) {
isInboundInterfaceRuleExists = true
continue
}
if strings.HasPrefix(rule.text, outboundInterfaceRule) {
isOutboundInterfaceRuleExists = true
continue
}
}

if !isIntraInterfaceRuleExists {
requireChange = true
rule := []string{
"insert", "rule", nb.targetTable, nb.targetChain,
"position", fmt.Sprintf("%d", nb.targetHandle),
"iifname", fmt.Sprintf("\"%s\"", nb.targetInterface),
"oifname", fmt.Sprintf("\"%s\"", nb.targetInterface),
"counter", "packets", "0", "bytes", "0", "accept",
}
nb.newRules = append(nb.newRules, rule)
}

if !isInboundInterfaceRuleExists {
rule := []string{
"insert", "rule", nb.targetTable, nb.targetChain,
"position", fmt.Sprintf("%d", nb.targetHandle),
"oifname", fmt.Sprintf("\"%s\"", nb.targetInterface),
"ip", "daddr", nb.targetNetwork.String(),
"ct", "state", "established,related",
"counter", "packets", "0", "bytes", "0", "accept",
}
nb.newRules = append(nb.newRules, rule)
requireChange = true
}

if !isOutboundInterfaceRuleExists {
rule := []string{
"insert", "rule", nb.targetTable, nb.targetChain,
"position", fmt.Sprintf("%d", nb.targetHandle),
"iifname", fmt.Sprintf("\"%s\"", nb.targetInterface),
"ip", "saddr", nb.targetNetwork.String(),
"counter", "packets", "0", "bytes", "0", "accept",
}
nb.newRules = append(nb.newRules, rule)
requireChange = true
}

if requireChange {
return true
}
return false
}

func (nb *nftBackend) addRules() error {
if len(nb.newRules) == 0 {
return nil
}
for _, cmdArgs := range nb.newRules {
stdoutLines, stderrLines, err := nb.execCommand(cmdArgs)
if err != nil {
return fmt.Errorf(
"encountered error %s: %s (stdout: %s, stderr: %s)",
cmdArgs, err, stdoutLines, stderrLines,
)
}
}
return nil
}

func (nb *nftBackend) delRules() error {
if len(nb.markedDelRules) == 0 {
return nil
}
return nil
}

func (nb *nftBackend) isValidInput(result *current.Result) error {
if len(result.Interfaces) == 0 {
return fmt.Errorf("the data passed to firewall plugin did not contain network interfaces")
}

if result.Interfaces[0].Name == "" {
return fmt.Errorf("the data passed to firewall plugin has no bridge name, e.g. cnibr0")
}

nb.targetInterface = result.Interfaces[0].Name

if len(result.IPs) == 0 {
return fmt.Errorf("the data passed to firewall plugin has no IP addresses")
}

if len(result.IPs) != 1 {
return fmt.Errorf("the data passed to firewall plugin has more than one IP address")
}

addr := result.IPs[0].Address

if addr.String() == "" {
return fmt.Errorf("the data passed to firewall plugin has empty IP address")
}

if addr.IP.To4() == nil {
return fmt.Errorf("the data passed to firewall plugin has non-IPv4 address")
}

_, netAddr, err := net.ParseCIDR(addr.String())
if err != nil {
return fmt.Errorf("the data passed to firewall plugin has invalid IPv4 address")
}

nb.targetNetwork = netAddr

return nil
}

func (nb *nftBackend) Add(conf *FirewallNetConf, result *current.Result) error {
if err := nb.isValidInput(result); err != nil {
return fmt.Errorf("nftBackend.Add() %s", err)
}

if err := nb.getRules(); err != nil {
return fmt.Errorf("nftBackend.Add() %s", err)
}

if !nb.isRuleChangeRequired() {
return nil
}

if err := nb.addRules(); err != nil {
return fmt.Errorf("nftBackend.Add() %s", err)
}

if err := nb.delRules(); err != nil {
return fmt.Errorf("nftBackend.Add() %s", err)
}

return nil
}

func (nb *nftBackend) Del(conf *FirewallNetConf, result *current.Result) error {
return nil
}

func (nb *nftBackend) Check(conf *FirewallNetConf, result *current.Result) error {
return nil
}

0 comments on commit f2cecbe

Please sign in to comment.