From 9405e3704fae9c30b24ad8807174639005b1db6c Mon Sep 17 00:00:00 2001 From: Matthew Heon Date: Sun, 9 Sep 2018 13:16:34 -0400 Subject: Vendor CNI plugins firewall code The upstream CNI project has a PR open for adding iptables and firewalld support, but this has been stalled for the better part of a year upstream. On advice of several maintainers, we are vendoring this code into libpod, to perform the relevant firewall configuration ourselves. Signed-off-by: Matthew Heon Closes: #1431 Approved by: baude --- pkg/firewall/common.go | 51 +++++++++ pkg/firewall/firewall_linux.go | 47 ++++++++ pkg/firewall/firewall_none.go | 41 +++++++ pkg/firewall/firewall_unsupported.go | 27 +++++ pkg/firewall/firewalld.go | 119 ++++++++++++++++++++ pkg/firewall/iptables.go | 212 +++++++++++++++++++++++++++++++++++ 6 files changed, 497 insertions(+) create mode 100644 pkg/firewall/common.go create mode 100644 pkg/firewall/firewall_linux.go create mode 100644 pkg/firewall/firewall_none.go create mode 100644 pkg/firewall/firewall_unsupported.go create mode 100644 pkg/firewall/firewalld.go create mode 100644 pkg/firewall/iptables.go (limited to 'pkg') diff --git a/pkg/firewall/common.go b/pkg/firewall/common.go new file mode 100644 index 000000000..993c691cd --- /dev/null +++ b/pkg/firewall/common.go @@ -0,0 +1,51 @@ +package firewall + +// 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. + +import ( + "net" + + "github.com/containernetworking/cni/pkg/types/current" +) + +// FirewallNetConf represents the firewall configuration. +type FirewallNetConf struct { + //types.NetConf + + // IptablesAdminChainName is an optional name to use instead of the default + // admin rules override chain name that includes the interface name. + IptablesAdminChainName string + + // FirewalldZone is an optional firewalld zone to place the interface into. If + // the firewalld backend is used but the zone is not given, it defaults + // to 'trusted' + FirewalldZone string + + PrevResult *current.Result +} + +// FirewallBackend is an interface to the system firewall, allowing addition and +// removal of firewall rules. +type FirewallBackend interface { + Add(*FirewallNetConf) error + Del(*FirewallNetConf) error +} + +func ipString(ip net.IPNet) string { + if ip.IP.To4() == nil { + return ip.IP.String() + "/128" + } + return ip.IP.String() + "/32" +} diff --git a/pkg/firewall/firewall_linux.go b/pkg/firewall/firewall_linux.go new file mode 100644 index 000000000..4ac45427b --- /dev/null +++ b/pkg/firewall/firewall_linux.go @@ -0,0 +1,47 @@ +// +build linux + +// 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. + +package firewall + +import ( + "fmt" +) + +// GetBackend retrieves a firewall backend for adding or removing firewall rules +// on the system. +// Valid backend names are firewalld, iptables, and none. +// If the empty string is given, a firewalld backend will be returned if +// firewalld is running, and an iptables backend will be returned otherwise. +func GetBackend(backend string) (FirewallBackend, error) { + switch backend { + case "firewalld": + return newFirewalldBackend() + case "iptables": + return newIptablesBackend() + case "none": + return newNoneBackend() + case "": + // Default to firewalld if it's running + if isFirewalldRunning() { + return newFirewalldBackend() + } + + // Otherwise iptables + return newIptablesBackend() + default: + return nil, fmt.Errorf("unrecognized firewall backend %q", backend) + } +} diff --git a/pkg/firewall/firewall_none.go b/pkg/firewall/firewall_none.go new file mode 100644 index 000000000..9f9594b4a --- /dev/null +++ b/pkg/firewall/firewall_none.go @@ -0,0 +1,41 @@ +// 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. + +package firewall + +import ( + "fmt" +) + +// FirewallNone is a firewall backend for environments where manipulating the +// system firewall is unsupported (for example, when running without root) +type FirewallNone struct {} + +func newNoneBackend() (FirewallBackend, error) { + return &FirewallNone{}, nil +} + +// Add adds a rule to the system firewall. +// No action is taken and an error is unconditionally returned as this backend +// does not support manipulating the firewall. +func (f *FirewallNone) Add(conf *FirewallNetConf) error { + return fmt.Errorf("cannot modify system firewall rules") +} + +// Del deletes a rule from the system firewall. +// No action is taken and an error is unconditionally returned as this backend +// does not support manipulating the firewall. +func (f *FirewallNone) Del(conf *FirewallNetConf) error { + return fmt.Errorf("cannot modify system firewall rules") +} diff --git a/pkg/firewall/firewall_unsupported.go b/pkg/firewall/firewall_unsupported.go new file mode 100644 index 000000000..24c07a8a9 --- /dev/null +++ b/pkg/firewall/firewall_unsupported.go @@ -0,0 +1,27 @@ +// +build !linux + +// 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. + +package firewall + +import ( + "fmt" +) + +// GetBackend retrieves a firewall backend for adding or removing firewall rules +// on the system. +func GetBackend(backend string) (FirewallBackend, error) { + return nil, fmt.Errorf("firewall backends are not presently supported on this OS") +} diff --git a/pkg/firewall/firewalld.go b/pkg/firewall/firewalld.go new file mode 100644 index 000000000..32c2337a0 --- /dev/null +++ b/pkg/firewall/firewalld.go @@ -0,0 +1,119 @@ +// +build linux + +// 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 firewall + +import ( + "fmt" + "strings" + + "github.com/godbus/dbus" +) + +const ( + dbusName = "org.freedesktop.DBus" + dbusPath = "/org/freedesktop/DBus" + dbusGetNameOwnerMethod = "GetNameOwner" + + firewalldName = "org.fedoraproject.FirewallD1" + firewalldPath = "/org/fedoraproject/FirewallD1" + firewalldZoneInterface = "org.fedoraproject.FirewallD1.zone" + firewalldAddSourceMethod = "addSource" + firewalldRemoveSourceMethod = "removeSource" + + errZoneAlreadySet = "ZONE_ALREADY_SET" +) + +// Only used for testcases to override the D-Bus connection +var testConn *dbus.Conn + +type fwdBackend struct { + conn *dbus.Conn +} + +// fwdBackend implements the FirewallBackend interface +var _ FirewallBackend = &fwdBackend{} + +func getConn() (*dbus.Conn, error) { + if testConn != nil { + return testConn, nil + } + return dbus.SystemBus() +} + +// isFirewalldRunning checks whether firewalld is running. +func isFirewalldRunning() bool { + conn, err := getConn() + if err != nil { + return false + } + + dbusObj := conn.Object(dbusName, dbusPath) + var res string + if err := dbusObj.Call(dbusName+"."+dbusGetNameOwnerMethod, 0, firewalldName).Store(&res); err != nil { + return false + } + + return true +} + +func newFirewalldBackend() (FirewallBackend, error) { + conn, err := getConn() + if err != nil { + return nil, err + } + + backend := &fwdBackend{ + conn: conn, + } + return backend, nil +} + +func getFirewalldZone(conf *FirewallNetConf) string { + if conf.FirewalldZone != "" { + return conf.FirewalldZone + } + + return "trusted" +} + +func (fb *fwdBackend) Add(conf *FirewallNetConf) error { + zone := getFirewalldZone(conf) + + for _, ip := range conf.PrevResult.IPs { + ipStr := ipString(ip.Address) + // Add a firewalld rule which assigns the given source IP to the given zone + firewalldObj := fb.conn.Object(firewalldName, firewalldPath) + var res string + if err := firewalldObj.Call(firewalldZoneInterface+"."+firewalldAddSourceMethod, 0, zone, ipStr).Store(&res); err != nil { + if !strings.Contains(err.Error(), errZoneAlreadySet) { + return fmt.Errorf("failed to add the address %v to %v zone: %v", ipStr, zone, err) + } + } + } + return nil +} + +func (fb *fwdBackend) Del(conf *FirewallNetConf) error { + for _, ip := range conf.PrevResult.IPs { + ipStr := ipString(ip.Address) + // Remove firewalld rules which assigned the given source IP to the given zone + firewalldObj := fb.conn.Object(firewalldName, firewalldPath) + var res string + firewalldObj.Call(firewalldZoneInterface+"."+firewalldRemoveSourceMethod, 0, getFirewalldZone(conf), ipStr).Store(&res) + } + return nil +} diff --git a/pkg/firewall/iptables.go b/pkg/firewall/iptables.go new file mode 100644 index 000000000..9f065dbcf --- /dev/null +++ b/pkg/firewall/iptables.go @@ -0,0 +1,212 @@ +// +build linux + +// 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 firewall + +import ( + "fmt" + "net" + + "github.com/coreos/go-iptables/iptables" +) + +func getPrivChainRules(ip string) [][]string { + var rules [][]string + rules = append(rules, []string{"-d", ip, "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT"}) + rules = append(rules, []string{"-s", ip, "-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 generateFilterRule(privChainName string) []string { + return []string{"-m", "comment", "--comment", "CNI firewall plugin rules", "-j", privChainName} +} + +func generateAdminRule(adminChainName string) []string { + return []string{"-m", "comment", "--comment", "CNI firewall plugin admin overrides", "-j", adminChainName} +} + +func cleanupRules(ipt *iptables.IPTables, privChainName string, rules [][]string) { + for _, rule := range rules { + ipt.Delete("filter", privChainName, rule...) + } +} + +func ensureFirstChainRule(ipt *iptables.IPTables, chain string, rule []string) error { + exists, err := ipt.Exists("filter", chain, rule...) + if !exists && err == nil { + err = ipt.Insert("filter", chain, 1, rule...) + } + return err +} + +func (ib *iptablesBackend) setupChains(ipt *iptables.IPTables) error { + privRule := generateFilterRule(ib.privChainName) + adminRule := generateFilterRule(ib.adminChainName) + + // Ensure our private chains exist + if err := ensureChain(ipt, "filter", ib.privChainName); err != nil { + return err + } + if err := ensureChain(ipt, "filter", ib.adminChainName); err != nil { + return err + } + + // Ensure our filter rule exists in the forward chain + if err := ensureFirstChainRule(ipt, "FORWARD", privRule); err != nil { + return err + } + + // Ensure our admin override chain rule exists in our private chain + if err := ensureFirstChainRule(ipt, ib.privChainName, adminRule); err != nil { + return err + } + + return nil +} + +func protoForIP(ip net.IPNet) iptables.Protocol { + if ip.IP.To4() != nil { + return iptables.ProtocolIPv4 + } + return iptables.ProtocolIPv6 +} + +func (ib *iptablesBackend) addRules(conf *FirewallNetConf, ipt *iptables.IPTables, proto iptables.Protocol) error { + rules := make([][]string, 0) + for _, ip := range conf.PrevResult.IPs { + if protoForIP(ip.Address) == proto { + rules = append(rules, getPrivChainRules(ipString(ip.Address))...) + } + } + + if len(rules) > 0 { + if err := ib.setupChains(ipt); err != nil { + return err + } + + // Clean up on any errors + var err error + defer func() { + if err != nil { + cleanupRules(ipt, ib.privChainName, rules) + } + }() + + for _, rule := range rules { + err = ipt.AppendUnique("filter", ib.privChainName, rule...) + if err != nil { + return err + } + } + } + + return nil +} + +func (ib *iptablesBackend) delRules(conf *FirewallNetConf, ipt *iptables.IPTables, proto iptables.Protocol) error { + rules := make([][]string, 0) + for _, ip := range conf.PrevResult.IPs { + if protoForIP(ip.Address) == proto { + rules = append(rules, getPrivChainRules(ipString(ip.Address))...) + } + } + + if len(rules) > 0 { + cleanupRules(ipt, ib.privChainName, rules) + } + + return nil +} + +func findProtos(conf *FirewallNetConf) []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 +} + +type iptablesBackend struct { + protos map[iptables.Protocol]*iptables.IPTables + privChainName string + adminChainName string + ifName string +} + +// iptablesBackend implements the FirewallBackend interface +var _ FirewallBackend = &iptablesBackend{} + +func newIptablesBackend() (FirewallBackend, error) { + adminChainName := "CNI-ADMIN" + + backend := &iptablesBackend{ + privChainName: "CNI-FORWARD", + adminChainName: adminChainName, + protos: make(map[iptables.Protocol]*iptables.IPTables), + } + + for _, proto := range []iptables.Protocol{iptables.ProtocolIPv4, iptables.ProtocolIPv6} { + ipt, err := iptables.NewWithProtocol(proto) + if err != nil { + return nil, fmt.Errorf("could not initialize iptables protocol %v: %v", proto, err) + } + backend.protos[proto] = ipt + } + + return backend, nil +} + +func (ib *iptablesBackend) Add(conf *FirewallNetConf) error { + for proto, ipt := range ib.protos { + if err := ib.addRules(conf, ipt, proto); err != nil { + return err + } + } + return nil +} + +func (ib *iptablesBackend) Del(conf *FirewallNetConf) error { + for proto, ipt := range ib.protos { + ib.delRules(conf, ipt, proto) + } + return nil +} -- cgit v1.2.3-54-g00ecf