Skip to content

Commit

Permalink
iptables-allow: new plugin which adds a host interface to the filter/…
Browse files Browse the repository at this point in the history
…FORWARD chain

Distros often have additional rules in the filter table that do things like:

-A FORWARD -j REJECT --reject-with icmp-host-prohibited

docker, for example, gets around this by adding explicit rules to the filter
table's FORWARD chain to allow traffic from the docker0 interface.  Do that
for a given host interface too, as a chained plugin.  eg:

{
    "cniVersion": "0.3.1",
    "name": "bridge-thing",
    "plugins": [
      {
        "type": "bridge",
        "bridge": "cni0",
        "isGateway": true,
        "ipMasq": true,
        "ipam": {
            "type": "host-local",
            "subnet": "10.88.0.0/16",
            "routes": [
                { "dst": "0.0.0.0/0" }
            ]
        }
      },
      {
        "type": "iptables-allow"
      }
    ]
}
  • Loading branch information
dcbw committed Jan 22, 2018
1 parent 03e316b commit a0691a7
Show file tree
Hide file tree
Showing 5 changed files with 810 additions and 0 deletions.
1 change: 1 addition & 0 deletions plugins/linux_only.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ plugins/main/ptp
plugins/main/vlan
plugins/meta/portmap
plugins/meta/tuning
plugins/meta/iptables-allow
20 changes: 20 additions & 0 deletions plugins/meta/iptables-allow/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# iptables-allow plugin

## Overview

This plugin creates iptables rules to allow traffic from the host network interface given by "ifName".
It does not create any network interfaces and therefore does not set up connectivity by itself.
It is only useful when used in addition to other plugins.

## Operation
The following network configuration file
```
{
"name": "mynet",
"type": "iptables-allow",
"ifName": "cni0"
}
```
will create two new iptables chains in the `filter` table and add rules that allow the given interface to send/receive traffic.

A successful result would simply be an empty result, unless a previous plugin passed a previous result, in which case this plugin will return that verbatim.
297 changes: 297 additions & 0 deletions plugins/meta/iptables-allow/iptables_allow.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
// Copyright 2016 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.

// This is a "meta-plugin". It reads in its own netconf, it does not create
// any network interface but just changes the network sysctl.

package main

import (
"encoding/json"
"fmt"

"github.com/coreos/go-iptables/iptables"

"github.com/containernetworking/cni/pkg/skel"
"github.com/containernetworking/cni/pkg/types"
"github.com/containernetworking/cni/pkg/types/current"
"github.com/containernetworking/cni/pkg/version"
"github.com/containernetworking/plugins/pkg/ns"

"github.com/vishvananda/netlink"
// . "github.com/onsi/ginkgo"
)

// IptAllowConf represents the iptables allow configuration.
type IptAllowConf struct {
types.NetConf

// IfName is the interface name to apply rules for; if not given the
// interface name will be pulled from the first host interface of
// the PrevResult (eg, the first one without a sandbox key).
IfName string `json:"ifName,omitempty"`
// AdminChainName is an optional name to use instead of the default
// admin rules override chain name that includes the interface name.
AdminChainName string `json:"adminChainName,omitempty"`

RawPrevResult map[string]interface{} `json:"prevResult,omitempty"`
PrevResult *current.Result `json:"-"`
}

func parseConf(data []byte) (*IptAllowConf, error) {
conf := IptAllowConf{}
if err := json.Unmarshal(data, &conf); err != nil {
return nil, fmt.Errorf("failed to load netconf: %v", err)
}

// Parse previous result.
if conf.RawPrevResult != nil {
resultBytes, err := json.Marshal(conf.RawPrevResult)
if err != nil {
return nil, fmt.Errorf("could not serialize prevResult: %v", err)
}
res, err := version.NewResult(conf.CNIVersion, resultBytes)
if err != nil {
return nil, fmt.Errorf("could not parse prevResult: %v", err)
}
conf.RawPrevResult = nil
conf.PrevResult, err = current.NewResultFromResult(res)
if err != nil {
return nil, fmt.Errorf("could not convert result to current version: %v", err)
}
}

return &conf, nil
}

func getPrivChainRules(intf string) [][]string {
var rules [][]string
rules = append(rules, []string{"-i", intf, "-j", "ACCEPT"})
rules = append(rules, []string{"-o", intf, "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT"})
return rules
}

func ensureChain(ipt *iptables.IPTables, table, chain string) error {
chains, err := ipt.ListChains(table)
if err != nil {
return fmt.Errorf("failed to list iptables chains: %v", err)
}
for _, ch := range chains {
if ch == chain {
return nil
}
}

return ipt.NewChain(table, chain)
}

func generateFilterRules(intf, privChainName, adminChainName string) [][]string {
var filterRules [][]string
filterRules = append(filterRules, []string{"-m", "comment", "--comment", fmt.Sprintf("CNI interface %s private rules", intf), "-j", privChainName})
filterRules = append(filterRules, []string{"-i", intf, "!", "-o", intf, "-m", "comment", "--comment", fmt.Sprintf("CNI interface %s administrator overrides", intf), "-j", adminChainName})
return filterRules
}

func generateChainNames(intf, adminChainName string) (string, string) {
if adminChainName == "" {
adminChainName = fmt.Sprintf("CNI-ADMIN-%s", intf)
}
return fmt.Sprintf("CNI-FORWARD-%s", intf), adminChainName
}

func cleanupRules(ipt *iptables.IPTables, filterRules [][]string, privChainName, adminChainName string) {
for _, rule := range filterRules {
ipt.Delete("filter", "FORWARD", rule...)
}

ipt.ClearChain("filter", privChainName)
ipt.DeleteChain("filter", privChainName)
ipt.ClearChain("filter", adminChainName)
ipt.DeleteChain("filter", adminChainName)
}

func addRules(intf, adminChainName string, proto iptables.Protocol) error {
var (
err error
ipt *iptables.IPTables
exists bool
privChainName string
)

ipt, err = iptables.NewWithProtocol(proto)
if err != nil {
return fmt.Errorf("failed to initialize iptables helper: %v", err)
}

privChainName, adminChainName = generateChainNames(intf, adminChainName)
filterRules := generateFilterRules(intf, privChainName, adminChainName)
defer func() {
// Clean up on error
if err != nil {
cleanupRules(ipt, filterRules, privChainName, adminChainName)
}
}()

if err := ensureChain(ipt, "filter", privChainName); err != nil {
return err
}
if err := ensureChain(ipt, "filter", adminChainName); err != nil {
return err
}

for _, rule := range filterRules {
exists, err = ipt.Exists("filter", "FORWARD", rule...)
if !exists && err == nil {
err = ipt.Insert("filter", "FORWARD", 1, rule...)
}
if err != nil {
return err
}
}

for _, rule := range getPrivChainRules(intf) {
if err := ipt.AppendUnique("filter", privChainName, rule...); err != nil {
return err
}
}

return nil
}

func delRules(intf, adminChainName string, proto iptables.Protocol) {
var privChainName string
if ipt, err := iptables.NewWithProtocol(proto); err == nil {
privChainName, adminChainName = generateChainNames(intf, adminChainName)
filterRules := generateFilterRules(intf, privChainName, adminChainName)
cleanupRules(ipt, filterRules, privChainName, adminChainName)
}
}

func hasSlavePorts(ifName, contIfName string, containerNS ns.NetNS) (bool, error) {
link, err := netlink.LinkByName(ifName)
if err != nil {
return false, fmt.Errorf("could not lookup %q: %v", ifName, err)
}

// DEL calls plugins in reverse order, thus the container interface and
// any peer (if it's a veth, for example) may not have been removed from
// the target interface (eg, a bridge) yet. So we need to ignore any
// peer when looking for slave links of the target interface.
var peerIndex int
if containerNS != nil {
containerNS.Do(func(_ ns.NetNS) error {
containerLink, err := netlink.LinkByName(contIfName)
if err != nil {
return err
}
peerIndex = containerLink.Attrs().ParentIndex
return nil
})
}

allLinks, err := netlink.LinkList()
if err != nil {
return false, fmt.Errorf("could not list links %q: %v", ifName, err)
}
for _, l := range allLinks {
if l.Attrs().Index != peerIndex && l.Attrs().MasterIndex == link.Attrs().Index {
return true, nil
}
}

return false, nil
}

func getIfname(conf *IptAllowConf) (string, error) {
if conf.IfName == "" {
return "", fmt.Errorf("failed to find host interface name from config")
}

return conf.IfName, nil
}

func findProtos(conf *IptAllowConf) []iptables.Protocol {
protos := []iptables.Protocol{iptables.ProtocolIPv4, iptables.ProtocolIPv6}
if conf.PrevResult != nil {
// If PrevResult is given, scan all IP addresses to figure out
// which IP versions to use
protos = []iptables.Protocol{}
for _, addr := range conf.PrevResult.IPs {
if addr.Address.IP.To4() != nil {
protos = append(protos, iptables.ProtocolIPv4)
} else {
protos = append(protos, iptables.ProtocolIPv6)
}
}
}
return protos
}

func cmdAdd(args *skel.CmdArgs) error {
iptConf, err := parseConf(args.StdinData)
if err != nil {
return err
}

ifName, err := getIfname(iptConf)
if err != nil {
return err
}

for _, proto := range findProtos(iptConf) {
if err := addRules(ifName, iptConf.AdminChainName, proto); err != nil {
return err
}
}

result := iptConf.PrevResult
if result == nil {
result = &current.Result{}
}
return types.PrintResult(result, iptConf.CNIVersion)
}

func cmdDel(args *skel.CmdArgs) error {
iptConf, err := parseConf(args.StdinData)
if err != nil {
return err
}

ifName, err := getIfname(iptConf)
if err != nil {
return err
}

// Tolerate errors if the container namespace has been torn down already
containerNS, err := ns.GetNS(args.Netns)
if err == nil {
defer containerNS.Close()
}

// Runtime errors are ignored
// Cleanup happens if the device is not a master-type device, or if
// it is a master-type device but has no slaves/ports
hasPorts, err := hasSlavePorts(ifName, args.IfName, containerNS)
if !hasPorts && err == nil {
for _, proto := range findProtos(iptConf) {
delRules(ifName, iptConf.AdminChainName, proto)
}
}

return nil
}

func main() {
skel.PluginMain(cmdAdd, cmdDel, version.All)
}
27 changes: 27 additions & 0 deletions plugins/meta/iptables-allow/iptables_allow_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright 2017 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 (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"

"testing"
)

func TestIptablesAllow(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "iptables_allow Suite")
}
Loading

0 comments on commit a0691a7

Please sign in to comment.