From 4acad8dd26bfbe8025d92942d1c7b199a82fcc53 Mon Sep 17 00:00:00 2001 From: Paul Greenberg Date: Sun, 15 Mar 2020 22:38:35 -0400 Subject: [PATCH] firewall: add nftables backend Resolves: #461 Signed-off-by: Paul Greenberg --- go.mod | 7 +- go.sum | 22 ++ plugins/meta/firewall/README.md | 176 ++++++++++++ plugins/meta/firewall/firewall.go | 2 + .../meta/firewall/firewall_nftables_test.go | 203 ++++++++++++++ plugins/meta/firewall/nftables.go | 261 ++++++++++++++++++ 6 files changed, 667 insertions(+), 4 deletions(-) create mode 100644 plugins/meta/firewall/firewall_nftables_test.go create mode 100644 plugins/meta/firewall/nftables.go diff --git a/go.mod b/go.mod index 2b4c4df34..7e974c44c 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4 // indirect github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c github.com/golang/protobuf v1.3.1 // indirect + github.com/google/nftables v0.0.0-20200316075819-7127d9d22474 github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56 github.com/mattn/go-shellwords v1.0.3 github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b @@ -24,10 +25,8 @@ require ( github.com/sirupsen/logrus v1.0.6 // indirect github.com/stretchr/testify v1.3.0 // indirect github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf - github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc // indirect - golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941 // indirect - golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1 // indirect - golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f + github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc + golang.org/x/sys v0.0.0-20191029155521-f43be2a4598c gopkg.in/airbrake/gobrake.v2 v2.0.9 // indirect gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 // indirect ) diff --git a/go.sum b/go.sum index ed3e97026..d190beb11 100644 --- a/go.sum +++ b/go.sum @@ -26,10 +26,20 @@ github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c h1:RBUpb2b14UnmRHNd2uH github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/nftables v0.0.0-20200316075819-7127d9d22474 h1:D6bN82zzK92ywYsE+Zjca7EHZCRZbcNTU3At7WdxQ+c= +github.com/google/nftables v0.0.0-20200316075819-7127d9d22474/go.mod h1:cfspEyr/Ap+JDIITA+N9a0ernqG0qZ4W1aqMRgDZa1g= github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56 h1:742eGXur0715JMq73aD95/FU0XpVKXqNuTnEfXsLOYQ= github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= +github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= +github.com/koneu/natend v0.0.0-20150829182554-ec0926ea948d h1:MFX8DxRnKMY/2M3H61iSsVbo/n3h0MWGmWNN1UViOU0= +github.com/koneu/natend v0.0.0-20150829182554-ec0926ea948d/go.mod h1:QHb4k4cr1fQikUahfcRVPcEXiUgFsdIstGqlurL0XL4= github.com/mattn/go-shellwords v1.0.3 h1:K/VxK7SZ+cvuPgFSLKi5QPI9Vr/ipOf4C1gN+ntueUk= github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= +github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA= +github.com/mdlayher/netlink v0.0.0-20191009155606-de872b0d824b h1:W3er9pI7mt2gOqOWzwvx20iJ8Akiqz1mUMTxU6wdvl8= +github.com/mdlayher/netlink v0.0.0-20191009155606-de872b0d824b/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M= github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b h1:Ey6yH0acn50T/v6CB75bGP4EMJqnv9WvnjN7oZaj+xE= github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a h1:KfNOeFvoAssuZLT7IntKZElKwi/5LRuxY71k+t6rfaM= @@ -49,10 +59,22 @@ github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc h1:R83G5ikgLMxrB github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941 h1:qBTHLajHecfu+xzRI9PqVDcqx7SdHj9d4B+EzSn3tAc= golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1 h1:Y/KGZSOdz/2r0WJ9Mkmz6NJBusp0kiNx1Cn82lzJQ6w= golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191028085509-fe3aa8a45271 h1:N66aaryRB3Ax92gH0v3hp1QYZ3zWWCCUR/j8Ifh45Ss= +golang.org/x/net v0.0.0-20191028085509-fe3aa8a45271/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f h1:25KHgbfyiSm6vwQLbM3zZIe1v9p/3ea4Rz+nnM5K/i4= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191029155521-f43be2a4598c h1:S/FtSvpNLtFBgjTqcKsRpsa6aVsI6iztaz1bQd9BJwE= +golang.org/x/sys v0.0.0-20191029155521-f43be2a4598c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 h1:OAj3g0cR6Dx/R07QgQe8wkA9RNjB2u4i700xBkIT4e0= diff --git a/plugins/meta/firewall/README.md b/plugins/meta/firewall/README.md index e53c0155b..a3b7b927a 100644 --- a/plugins/meta/firewall/README.md +++ b/plugins/meta/firewall/README.md @@ -133,3 +133,179 @@ of the container as shown: - `-s 10.88.0.2 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT` - `-d 10.88.0.2 -j ACCEPT` +## nftables backend rule structure + +The prerequisite for the backend is the existence of `filter` table and +the existence of `FORWARD` chain in the table. + +A sample standalone config list (with the file extension `.conflist`) using +`nftables` backend might look like: + +```json +{ + "cniVersion": "0.4.0", + "name": "podman", + "plugins": [ + { + "type": "bridge", + "bridge": "cni-podman0", + "isGateway": true, + "ipMasq": true, + "ipam": { + "type": "host-local", + "routes": [ + { + "dst": "0.0.0.0/0" + } + ], + "ranges": [ + [ + { + "subnet": "192.168.124.0/24", + "gateway": "192.168.124.1" + } + ] + ] + } + }, + { + "type": "portmap", + "capabilities": { + "portMappings": true + } + }, + { + "type": "firewall", + "backend": "nftables" + } + ] +} +``` + +Prior to the invocation of CNI `firewall` plugin, the `FORWARD` chain in `filter` table is: + +``` +$ sudo nft -a -n list chain ip filter FORWARD +table ip filter { + chain FORWARD { # handle 2 + type filter hook forward priority 0; policy drop; + oifname "virbr0" ip daddr 192.168.122.0/24 ct state 0x2,0x4 counter packets 0 bytes 0 accept # handle 25 + iifname "virbr0" ip saddr 192.168.122.0/24 counter packets 0 bytes 0 accept # handle 26 + iifname "virbr0" oifname "virbr0" counter packets 0 bytes 0 accept # handle 27 + log prefix "IPv4 FORWARD drop: " flags all # handle 28 + counter packets 0 bytes 0 drop # handle 29 + } +} +``` + +After starting a container, the plugin executes the following commands based +on the configuration above. Please note that `position 25` refers to the handle +at the top of the chain. + +``` +sudo nft insert rule ip filter FORWARD position 25 oifname "cni-podman0" ip daddr 192.168.124.0/24 ct state established,related counter packets 0 bytes 0 accept +sudo nft insert rule ip filter FORWARD position 25 iifname "cni-podman0" ip saddr 192.168.124.0/24 counter packets 0 bytes 0 accept +sudo nft insert rule ip filter FORWARD position 25 iifname "cni-podman0" oifname "cni-podman0" counter packets 0 bytes 0 accept +``` + +After the plugin's execution, the chain looks like this: + +``` +$ sudo nft -a -n list chain ip filter FORWARD +table ip filter { + chain FORWARD { # handle 2 + type filter hook forward priority 0; policy drop; + oifname "cni-podman0" ip daddr 192.168.124.0/24 ct state 0x2,0x4 counter packets 0 bytes 0 accept # handle 31 + iifname "cni-podman0" ip saddr 192.168.124.0/24 counter packets 0 bytes 0 accept # handle 32 + iifname "cni-podman0" oifname "cni-podman0" counter packets 0 bytes 0 accept # handle 33 + oifname "virbr0" ip daddr 192.168.122.0/24 ct state 0x2,0x4 counter packets 0 bytes 0 accept # handle 25 + iifname "virbr0" ip saddr 192.168.122.0/24 counter packets 0 bytes 0 accept # handle 26 + iifname "virbr0" oifname "virbr0" counter packets 0 bytes 0 accept # handle 27 + log prefix "IPv4 FORWARD drop: " flags all # handle 28 + counter packets 0 bytes 0 drop # handle 29 + } +} +``` + +Subsequent executions of the plugin do not create additional rules in the chain, unless +the CNI network configuration changes. + +## Testing + +Generally, the plugin testing begins with defining the data structure +the plugin would receive when processing a request. In this example, +the plugin received single interface `dummy0`, with IPv4 and IPv6 addresses. +The backend is `nftables`. + +```json +{ + "name": "test", + "type": "firewall", + "backend": "nftables", + "ifName": "dummy0", + "cniVersion": "0.4.0", + "prevResult": { + "interfaces": [ + { + "name": "dummy0" + } + ], + "ips": [ + { + "version": "4", + "address": "192.168.200.10/24", + "interface": 0 + }, + { + "version": "6", + "address": "2001:db8:1:2::1/64", + "interface": 0 + } + ] + } +} +``` + +Prior to running tests, the test harness does the following: + +1. creates `originalNS` namespace +2. adds `dummy0` interface to `originalNS` via Netlink +3. checks that the `dummy0` interface is available in the `originalNS` +4. creates `targetNS` namespace + +Upon the completion of the testing, the test harness does the following: + +1. closes `originalNS` namespace +2. closes `targetNS` namespace + +The tests in the harness start with `It()`. + +Generally, a test contains a number of input arguments. In the case of +"installs nftables rules, checks the rules exist, then cleans up on delete using v4.0.x", +the test has the following arguments: + +* container id: `dummy` +* the path to container namespace, i.e. `targetNS` +* the name of the interface +* the JSON payload containing a dummy request + +The test uses the same arguments and runs the following operations in +`originalNS` namespace: + +* `cmdAdd` +* `cmdCheck` +* `cmdDel` + +The operations correspond to the following functions: + +| **Operation** | **Function** | +| --- | --- | +| `cmdAdd` | `func (nb *nftBackend) Add(conf *FirewallNetConf, result *current.Result)` | +| `cmdCheck` | `func (nb *nftBackend) Del(conf *FirewallNetConf, result *current.Result)` | +| `cmdDel` | `func (nb *nftBackend) Check(conf *FirewallNetConf, result *current.Result)` | + +The following command triggers the testing of `firewall` plugin: + +```bash +sudo go test -v ./plugins/meta/firewall +``` diff --git a/plugins/meta/firewall/firewall.go b/plugins/meta/firewall/firewall.go index 875943beb..f33ac54dd 100644 --- a/plugins/meta/firewall/firewall.go +++ b/plugins/meta/firewall/firewall.go @@ -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) } diff --git a/plugins/meta/firewall/firewall_nftables_test.go b/plugins/meta/firewall/firewall_nftables_test.go new file mode 100644 index 000000000..c6d460675 --- /dev/null +++ b/plugins/meta/firewall/firewall_nftables_test.go @@ -0,0 +1,203 @@ +// 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/containernetworking/cni/pkg/skel" + "github.com/containernetworking/cni/pkg/types/current" + "github.com/containernetworking/plugins/pkg/ns" + "github.com/containernetworking/plugins/pkg/testutils" + + "github.com/google/nftables" + "github.com/vishvananda/netlink" + "path" + "strings" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func validateNftRules(bytes []byte) { + prevResult := getPrevResult(bytes) + + for _, ip := range prevResult.IPs { + Expect(ip).To(Equal(true)) + /* + ipt, err := iptables.NewWithProtocol(protoForIP(ip.Address)) + Expect(err).NotTo(HaveOccurred()) + + // Ensure chains + chains, err := ipt.ListChains("filter") + Expect(err).NotTo(HaveOccurred()) + foundAdmin, foundPriv := findChains(chains) + Expect(foundAdmin).To(Equal(true)) + Expect(foundPriv).To(Equal(true)) + + // Look for the FORWARD chain jump rules to our custom chains + rules, err := ipt.List("filter", "FORWARD") + Expect(err).NotTo(HaveOccurred()) + Expect(len(rules)).Should(BeNumerically(">", 1)) + _, foundPriv = findForwardJumpRules(rules) + Expect(foundPriv).To(Equal(true)) + + // Look for the allow rules in our custom FORWARD chain + rules, err = ipt.List("filter", "CNI-FORWARD") + Expect(err).NotTo(HaveOccurred()) + Expect(len(rules)).Should(BeNumerically(">", 1)) + foundAdmin, _ = findForwardJumpRules(rules) + Expect(foundAdmin).To(Equal(true)) + + // Look for the IP allow rules + foundOne, foundTwo := findForwardAllowRules(rules, ipString(ip.Address)) + Expect(foundOne).To(Equal(true)) + Expect(foundTwo).To(Equal(true)) + */ + } +} + +var _ = Describe("firewall plugin nftables backend v0.4.x", func() { + var originalNS, targetNS ns.NetNS + const IFNAME string = "dummy0" + + fullConf := []byte(`{ + "name": "test", + "type": "firewall", + "backend": "nftables", + "ifName": "dummy0", + "cniVersion": "0.4.0", + "prevResult": { + "interfaces": [ + {"name": "dummy0"} + ], + "ips": [ + { + "version": "4", + "address": "192.168.200.10/24", + "interface": 0 + }, + { + "version": "6", + "address": "2001:db8:1:2::1/64", + "interface": 0 + } + ] + } + }`) + + BeforeEach(func() { + // Create a new NetNS so we don't modify the host + var err error + originalNS, err = testutils.NewNS() + Expect(err).NotTo(HaveOccurred()) + + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + err = netlink.LinkAdd(&netlink.Dummy{ + LinkAttrs: netlink.LinkAttrs{ + Name: IFNAME, + }, + }) + Expect(err).NotTo(HaveOccurred()) + _, err = netlink.LinkByName(IFNAME) + Expect(err).NotTo(HaveOccurred()) + + // Add netfilter connection + nftc := &nftables.Conn{} + // Add IPv4 filter table + filter4Table := nftc.AddTable(&nftables.Table{ + Family: nftables.TableFamilyIPv4, + Name: "filter", + }) + Expect(filter4Table).NotTo(BeNil()) + // Add FORWARD chain in IPv4 filter table + forwardFilter4TableChain := nftc.AddChain(&nftables.Chain{ + Name: "FORWARD", + Table: filter4Table, + Type: nftables.ChainTypeFilter, + Hooknum: nftables.ChainHookForward, + Priority: nftables.ChainPriorityFilter, + }) + Expect(forwardFilter4TableChain).NotTo(BeNil()) + // Add IPv6 filter table + filter6Table := nftc.AddTable(&nftables.Table{ + Family: nftables.TableFamilyIPv6, + Name: "filter", + }) + Expect(filter6Table).NotTo(BeNil()) + // Add FORWARD chain in IPv6 filter table + forwardFilter6TableChain := nftc.AddChain(&nftables.Chain{ + Name: "FORWARD", + Table: filter4Table, + Type: nftables.ChainTypeFilter, + Hooknum: nftables.ChainHookForward, + Priority: nftables.ChainPriorityFilter, + }) + Expect(forwardFilter6TableChain).NotTo(BeNil()) + // Execute netfilter changes + err = nftc.Flush() + Expect(err).NotTo(HaveOccurred()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + targetNS, err = testutils.NewNS() + Expect(err).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + Expect(originalNS.Close()).To(Succeed()) + Expect(targetNS.Close()).To(Succeed()) + }) + + It("installs nftables rules, checks the rules exist, then cleans up on delete using v4.0.x", func() { + _, containerID := path.Split(targetNS.Path()) + containerID = strings.ReplaceAll(containerID, "cnitest", "dummy") + args := &skel.CmdArgs{ + ContainerID: containerID, + Netns: targetNS.Path(), + IfName: IFNAME, + StdinData: fullConf, + } + + err := originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + r, _, err := testutils.CmdAddWithArgs(args, func() error { + return cmdAdd(args) + }) + Expect(err).NotTo(HaveOccurred()) + + _, err = current.GetResult(r) + Expect(err).NotTo(HaveOccurred()) + + err = testutils.CmdCheckWithArgs(args, func() error { + return cmdCheck(args) + }) + Expect(err).NotTo(HaveOccurred()) + validateNftRules(fullConf) + + /* + err = testutils.CmdDelWithArgs(args, func() error { + return cmdDel(args) + }) + Expect(err).NotTo(HaveOccurred()) + validateCleanedUp(fullConf) + */ + return nil + }) + Expect(err).NotTo(HaveOccurred()) + }) +}) diff --git a/plugins/meta/firewall/nftables.go b/plugins/meta/firewall/nftables.go new file mode 100644 index 000000000..8491a9758 --- /dev/null +++ b/plugins/meta/firewall/nftables.go @@ -0,0 +1,261 @@ +// 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 ( + "fmt" + "github.com/containernetworking/cni/pkg/types/current" + "github.com/google/nftables" + "github.com/vishvananda/netns" + "net" + "strconv" + "strings" +) + +type nftBackend struct { + targetTable string + targetChain string + targetHandle uint64 + targetInterfaces map[string][]*current.IPConfig + targetAddresses []*nftAddress + rules []*nftRule +} + +type nftAddress struct { + ifname string + ipaddr *net.IPNet + netaddr *net.IPNet + handle uint64 + inRuleExists bool + outRuleExists bool + inRuleParts []string + outRuleParts []string +} + +func newNftAddress(ifname string, addr *net.IPNet) (*nftAddress, error) { + _, netAddr, err := net.ParseCIDR(addr.String()) + if err != nil { + return nil, err + } + ifaddr := &nftAddress{ + ifname: ifname, + ipaddr: addr, + netaddr: netAddr, + } + ifaddr.inRuleParts = []string{ + "oifname", fmt.Sprintf("\"%s\"", ifname), + "ip", "daddr", netAddr.String(), + "ct", "state", "established,related", + } + ifaddr.outRuleParts = []string{ + "iifname", fmt.Sprintf("\"%s\"", ifname), + "ip", "saddr", netAddr.String(), + } + return ifaddr, nil +} + +func (a *nftAddress) getInboundRulePattern() string { + return strings.Join(a.inRuleParts, " ") +} + +func (a *nftAddress) getOutboundRulePattern() string { + return strings.Join(a.outRuleParts, " ") +} + +func (a *nftAddress) getInboundRule(table string, chain string, handle uint64) []string { + rule := []string{} + if handle == 0 { + rule = append(rule, []string{"add", "rule", table, chain}...) + } else { + rule = append(rule, []string{"insert", "rule", table, chain, "position", fmt.Sprintf("%d", handle)}...) + } + rule = append(rule, a.inRuleParts...) + rule = append(rule, []string{"counter", "packets", "0", "bytes", "0", "accept"}...) + return rule + +} + +func (a *nftAddress) getOutboundRule(table string, chain string, handle uint64) []string { + rule := []string{} + if handle == 0 { + rule = append(rule, []string{"add", "rule", table, chain}...) + } else { + rule = append(rule, []string{"insert", "rule", table, chain, "position", fmt.Sprintf("%d", handle)}...) + } + rule = append(rule, a.outRuleParts...) + rule = append(rule, []string{"counter", "packets", "0", "bytes", "0", "accept"}...) + return rule +} + +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, + targetAddresses: []*nftAddress{}, + rules: []*nftRule{}, + } + + return backend, nil +} + +func (nb *nftBackend) getRules() error { + + ns, err := netns.Get() + if err != nil { + return err + } + + c := &nftables.Conn{ + NetNS: int(ns), + } + + tables, err := c.ListTables() + if err != nil { + return err + } + + if len(tables) > 0 { + return fmt.Errorf("%v", tables) + } + + return fmt.Errorf("%v", tables) + // return nil +} + +func (nb *nftBackend) validateInput(result *current.Result) error { + + if len(result.Interfaces) == 0 { + return fmt.Errorf("the data passed to firewall plugin did not contain network interfaces") + } + + nb.targetInterfaces = make(map[string][]*current.IPConfig) + intfMap := make(map[int]string) + for i, intf := range result.Interfaces { + if intf.Name == "" { + return fmt.Errorf("the data passed to firewall plugin has no bridge name, e.g. cnibr0") + } + if _, interfaceExists := nb.targetInterfaces[intf.Name]; interfaceExists { + return fmt.Errorf("found duplicate interface name %s", intf.Name) + } + nb.targetInterfaces[intf.Name] = []*current.IPConfig{} + intfMap[i] = intf.Name + } + + if len(result.IPs) == 0 { + return fmt.Errorf("the data passed to firewall plugin has no IP addresses") + } + + for _, addr := range result.IPs { + if addr.Interface == nil { + return fmt.Errorf("the ip config interface is nil: %v", addr) + } + if _, interfaceExists := intfMap[*addr.Interface]; !interfaceExists { + return fmt.Errorf("the ip config points to non-existing interface: %v", addr) + } + intfName := intfMap[*addr.Interface] + nb.targetInterfaces[intfName] = append(nb.targetInterfaces[intfName], addr) + } + + for intf, conf := range nb.targetInterfaces { + if len(conf) == 0 { + return fmt.Errorf("interface %s has no associated IP information", intf) + } + } + + //for _, entry := range result.IPs { + // if entry.Address.String() == "" { + // return fmt.Errorf("the data passed to firewall plugin has empty IP address") + // } + // + // addr, err := newNftAddress(nb.targetInterface, &entry.Address) + // if err != nil { + // return fmt.Errorf( + // "the data passed to firewall plugin triggered error %s %s", + // entry.Address.String(), + // err, + // ) + // } + // + // nb.targetAddresses = append(nb.targetAddresses, addr) + // } + + return nil +} + +func (nb *nftBackend) Add(conf *FirewallNetConf, result *current.Result) error { + if err := nb.validateInput(result); err != nil { + return fmt.Errorf("nftBackend.Add() failed validation: %s", err) + } + if err := nb.getRules(); err != nil { + return fmt.Errorf("nftBackend.Add() failed parsing rules: %s", err) + } + + //if err := nb.addRules(); err != nil { + // return fmt.Errorf("nftBackend.Add() failed adding rules: %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 +}