diff --git a/.travis.yml b/.travis.yml index aa18b8ce..68e6d4c0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,7 @@ go: env: global: - PATH=$GOROOT/bin:$GOPATH/bin:$PATH + - CGO_ENABLED=0 matrix: - TARGET=amd64 - TARGET=arm @@ -29,6 +30,9 @@ matrix: install: - go get github.com/onsi/ginkgo/ginkgo - go get github.com/containernetworking/cni/cnitool + - go get golang.org/x/tools/cmd/cover + - go get github.com/modocache/gover + - go get github.com/mattn/goveralls script: - | diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index d1c2e2df..e0386f02 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -1,7 +1,7 @@ { "ImportPath": "github.com/containernetworking/plugins", "GoVersion": "go1.7", - "GodepVersion": "v79", + "GodepVersion": "v80", "Packages": [ "./..." ], @@ -14,7 +14,7 @@ { "ImportPath": "github.com/Microsoft/hcsshim", "Comment": "v0.7.6", - "Rev": "6efef912cc0ecd8778bab95d105662d4f73f8ccd" + "Rev": "e44e499d29527b244d6858772f1b9090eeaddc4e" }, { "ImportPath": "github.com/Microsoft/hcsshim/internal/guid", @@ -81,38 +81,38 @@ }, { "ImportPath": "github.com/containernetworking/cni/libcni", - "Comment": "v0.7.0-alpha1", - "Rev": "07c1a6da47b7fbf8b357f4949ecce2113e598491" + "Comment": "v0.7.0-rc2", + "Rev": "fbb95fff8a5239a4295c991efa8a397d43118f7e" }, { "ImportPath": "github.com/containernetworking/cni/pkg/invoke", - "Comment": "v0.7.0-alpha1", - "Rev": "07c1a6da47b7fbf8b357f4949ecce2113e598491" + "Comment": "v0.7.0-rc2", + "Rev": "fbb95fff8a5239a4295c991efa8a397d43118f7e" }, { "ImportPath": "github.com/containernetworking/cni/pkg/skel", - "Comment": "v0.7.0-alpha1", - "Rev": "07c1a6da47b7fbf8b357f4949ecce2113e598491" + "Comment": "v0.7.0-rc2", + "Rev": "fbb95fff8a5239a4295c991efa8a397d43118f7e" }, { "ImportPath": "github.com/containernetworking/cni/pkg/types", - "Comment": "v0.7.0-alpha1", - "Rev": "07c1a6da47b7fbf8b357f4949ecce2113e598491" + "Comment": "v0.7.0-rc2", + "Rev": "fbb95fff8a5239a4295c991efa8a397d43118f7e" }, { "ImportPath": "github.com/containernetworking/cni/pkg/types/020", - "Comment": "v0.7.0-alpha1", - "Rev": "07c1a6da47b7fbf8b357f4949ecce2113e598491" + "Comment": "v0.7.0-rc2", + "Rev": "fbb95fff8a5239a4295c991efa8a397d43118f7e" }, { "ImportPath": "github.com/containernetworking/cni/pkg/types/current", - "Comment": "v0.7.0-alpha1", - "Rev": "07c1a6da47b7fbf8b357f4949ecce2113e598491" + "Comment": "v0.7.0-rc2", + "Rev": "fbb95fff8a5239a4295c991efa8a397d43118f7e" }, { "ImportPath": "github.com/containernetworking/cni/pkg/version", - "Comment": "v0.7.0-alpha1", - "Rev": "07c1a6da47b7fbf8b357f4949ecce2113e598491" + "Comment": "v0.7.0-rc2", + "Rev": "fbb95fff8a5239a4295c991efa8a397d43118f7e" }, { "ImportPath": "github.com/coreos/go-iptables/iptables", @@ -144,6 +144,11 @@ "ImportPath": "github.com/d2g/dhcp4server/leasepool/memorypool", "Rev": "477b11cea4dcc56af002849238d4f9c1e093c744" }, + { + "ImportPath": "github.com/godbus/dbus", + "Comment": "v4.1.0-6-g885f9cc", + "Rev": "885f9cc04c9c1a6a61a2008e211d36c5737be3f5" + }, { "ImportPath": "github.com/j-keck/arping", "Rev": "2cf9dc699c5640a7e2c81403a44127bf28033600" @@ -318,39 +323,45 @@ }, { "ImportPath": "github.com/vishvananda/netlink", - "Rev": "6e453822d85ef5721799774b654d4d02fed62afb" + "Comment": "v1.0.0-40-g023a6da", + "Rev": "023a6dafdcdfa7068ac83b260ab7f03cd4131aca" }, { "ImportPath": "github.com/vishvananda/netlink/nl", - "Rev": "6e453822d85ef5721799774b654d4d02fed62afb" + "Comment": "v1.0.0-40-g023a6da", + "Rev": "023a6dafdcdfa7068ac83b260ab7f03cd4131aca" }, { "ImportPath": "github.com/vishvananda/netns", - "Rev": "54f0e4339ce73702a0607f49922aaa1e749b418d" + "Rev": "13995c7128ccc8e51e9a6bd2b551020a27180abd" }, { "ImportPath": "golang.org/x/crypto/ssh/terminal", - "Rev": "94eea52f7b742c7cbe0b03b22f0c4c8631ece122" + "Rev": "7c1a557ab941a71c619514f229f0b27ccb0c27cf" }, { "ImportPath": "golang.org/x/net/bpf", - "Rev": "e90d6d0afc4c315a0d87a568ae68577cc15149a0" + "Rev": "49bb7cea24b1df9410e1712aa6433dae904ff66a" }, { "ImportPath": "golang.org/x/net/internal/iana", - "Rev": "e90d6d0afc4c315a0d87a568ae68577cc15149a0" + "Rev": "49bb7cea24b1df9410e1712aa6433dae904ff66a" }, { "ImportPath": "golang.org/x/net/ipv4", - "Rev": "e90d6d0afc4c315a0d87a568ae68577cc15149a0" + "Rev": "49bb7cea24b1df9410e1712aa6433dae904ff66a" }, { "ImportPath": "golang.org/x/sys/unix", - "Rev": "d5840adf789d732bc8b00f37b26ca956a7cc8e79" + "Rev": "66b7b1311ac80bbafcd2daeef9a5e6e2cd1e2399" }, { "ImportPath": "golang.org/x/sys/windows", - "Rev": "d5840adf789d732bc8b00f37b26ca956a7cc8e79" + "Rev": "66b7b1311ac80bbafcd2daeef9a5e6e2cd1e2399" + }, + { + "ImportPath": "golang.org/x/net/internal/socket", + "Rev": "49bb7cea24b1df9410e1712aa6433dae904ff66a" } ] } diff --git a/README.md b/README.md index f156bbba..ca904a51 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Read [CONTRIBUTING](CONTRIBUTING.md) for build and test instructions. * `portmap`: An iptables-based portmapping plugin. Maps ports from the host's address space to the container. * `bandwidth`: Allows bandwidth-limiting through use of traffic control tbf (ingress/egress). * `sbr`: A plugin that configures source based routing for an interface (from which it is chained). +* `firewall`: A firewall plugin which uses iptables or firewalld to add rules to allow traffic to/from the container. ### Sample The sample plugin provides an example for building your own plugin. diff --git a/build_linux.sh b/build_linux.sh index ddae34b4..bd9a52eb 100755 --- a/build_linux.sh +++ b/build_linux.sh @@ -20,7 +20,7 @@ export GO="${GO:-go}" mkdir -p "${PWD}/bin" echo "Building plugins ${GOOS}" -PLUGINS="plugins/meta/* plugins/main/* plugins/ipam/* plugins/sample" +PLUGINS="plugins/meta/* plugins/main/* plugins/ipam/*" for d in $PLUGINS; do if [ -d "$d" ]; then plugin="$(basename "$d")" diff --git a/pkg/ip/utils_linux.go b/pkg/ip/utils_linux.go new file mode 100644 index 00000000..7623c5e1 --- /dev/null +++ b/pkg/ip/utils_linux.go @@ -0,0 +1,120 @@ +// +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 ip + +import ( + "fmt" + "net" + + "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/current" + "github.com/vishvananda/netlink" +) + +func ValidateExpectedInterfaceIPs(ifName string, resultIPs []*current.IPConfig) error { + + // Ensure ips + for _, ips := range resultIPs { + ourAddr := netlink.Addr{IPNet: &ips.Address} + match := false + + link, err := netlink.LinkByName(ifName) + if err != nil { + return fmt.Errorf("Cannot find container link %v", ifName) + } + + addrList, err := netlink.AddrList(link, netlink.FAMILY_ALL) + if err != nil { + return fmt.Errorf("Cannot obtain List of IP Addresses") + } + + for _, addr := range addrList { + if addr.Equal(ourAddr) { + match = true + break + } + } + if match == false { + return fmt.Errorf("Failed to match addr %v on interface %v", ourAddr, ifName) + } + + // Convert the host/prefixlen to just prefix for route lookup. + _, ourPrefix, err := net.ParseCIDR(ourAddr.String()) + + findGwy := &netlink.Route{Dst: ourPrefix} + routeFilter := netlink.RT_FILTER_DST + var family int + + switch { + case ips.Version == "4": + family = netlink.FAMILY_V4 + case ips.Version == "6": + family = netlink.FAMILY_V6 + default: + return fmt.Errorf("Invalid IP Version %v for interface %v", ips.Version, ifName) + } + + gwy, err := netlink.RouteListFiltered(family, findGwy, routeFilter) + if err != nil { + return fmt.Errorf("Error %v trying to find Gateway %v for interface %v", err, ips.Gateway, ifName) + } + if gwy == nil { + return fmt.Errorf("Failed to find Gateway %v for interface %v", ips.Gateway, ifName) + } + } + + return nil +} + +func ValidateExpectedRoute(resultRoutes []*types.Route) error { + + // Ensure that each static route in prevResults is found in the routing table + for _, route := range resultRoutes { + find := &netlink.Route{Dst: &route.Dst, Gw: route.GW} + routeFilter := netlink.RT_FILTER_DST | netlink.RT_FILTER_GW + var family int + + switch { + case route.Dst.IP.To4() != nil: + family = netlink.FAMILY_V4 + // Default route needs Dst set to nil + if route.Dst.String() == "0.0.0.0/0" { + find = &netlink.Route{Dst: nil, Gw: route.GW} + routeFilter = netlink.RT_FILTER_DST + } + case len(route.Dst.IP) == net.IPv6len: + family = netlink.FAMILY_V6 + // Default route needs Dst set to nil + if route.Dst.String() == "::/0" { + find = &netlink.Route{Dst: nil, Gw: route.GW} + routeFilter = netlink.RT_FILTER_DST + } + default: + return fmt.Errorf("Invalid static route found %v", route) + } + + wasFound, err := netlink.RouteListFiltered(family, find, routeFilter) + if err != nil { + return fmt.Errorf("Expected Route %v not route table lookup error %v", route, err) + } + if wasFound == nil { + return fmt.Errorf("Expected Route %v not found in routing table", route) + } + } + + return nil +} diff --git a/pkg/ipam/ipam.go b/pkg/ipam/ipam.go index 967ad7e4..4463035d 100644 --- a/pkg/ipam/ipam.go +++ b/pkg/ipam/ipam.go @@ -24,6 +24,10 @@ func ExecAdd(plugin string, netconf []byte) (types.Result, error) { return invoke.DelegateAdd(context.TODO(), plugin, netconf, nil) } +func ExecCheck(plugin string, netconf []byte) error { + return invoke.DelegateCheck(context.TODO(), plugin, netconf, nil) +} + func ExecDel(plugin string, netconf []byte) error { return invoke.DelegateDel(context.TODO(), plugin, netconf, nil) } diff --git a/pkg/testutils/cmd.go b/pkg/testutils/cmd.go index b4e71fa5..ce600f68 100644 --- a/pkg/testutils/cmd.go +++ b/pkg/testutils/cmd.go @@ -81,6 +81,21 @@ func CmdAddWithArgs(args *skel.CmdArgs, f func() error) (types.Result, []byte, e return CmdAdd(args.Netns, args.ContainerID, args.IfName, args.StdinData, f) } +func CmdCheck(cniNetns, cniContainerID, cniIfname string, conf []byte, f func() error) error { + os.Setenv("CNI_COMMAND", "CHECK") + os.Setenv("CNI_PATH", os.Getenv("PATH")) + os.Setenv("CNI_NETNS", cniNetns) + os.Setenv("CNI_IFNAME", cniIfname) + os.Setenv("CNI_CONTAINERID", cniContainerID) + defer envCleanup() + + return f() +} + +func CmdCheckWithArgs(args *skel.CmdArgs, f func() error) error { + return CmdCheck(args.Netns, args.ContainerID, args.IfName, args.StdinData, f) +} + func CmdDel(cniNetns, cniContainerID, cniIfname string, f func() error) error { os.Setenv("CNI_COMMAND", "DEL") os.Setenv("CNI_PATH", os.Getenv("PATH")) diff --git a/pkg/utils/buildversion/buildversion.go b/pkg/utils/buildversion/buildversion.go new file mode 100644 index 00000000..734d4189 --- /dev/null +++ b/pkg/utils/buildversion/buildversion.go @@ -0,0 +1,26 @@ +// Copyright 2019 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. + +// Buildversion is a destination for the linker trickery so we can auto +// set the build-version +package buildversion + +import "fmt" + +// This is overridden in the linker script +var BuildVersion = "version unknown" + +func BuildString(pluginName string) string { + return fmt.Sprintf("CNI %s plugin %s", pluginName, BuildVersion) +} diff --git a/plugins/ipam/dhcp/README.md b/plugins/ipam/dhcp/README.md index 0ec4c4cb..8b46180f 100644 --- a/plugins/ipam/dhcp/README.md +++ b/plugins/ipam/dhcp/README.md @@ -18,7 +18,7 @@ $ ./dhcp daemon If given `-pidfile ` arguments after 'daemon', the dhcp plugin will write its PID to the given file. -If given `-hostprefix ` arguments after 'daemon', the dhcp plugin will use this prefix for netns as `/`. It could be used in case of running dhcp daemon as container. +If given `-hostprefix ` arguments after 'daemon', the dhcp plugin will use this prefix for DHCP socket as `/run/cni/dhcp.sock`. You can use this prefix for references to the host filesystem, e.g. to access netns and the unix socket. Alternatively, you can use systemd socket activation protocol. Be sure that the .socket file uses /run/cni/dhcp.sock as the socket path. diff --git a/plugins/ipam/dhcp/daemon.go b/plugins/ipam/dhcp/daemon.go index 4d1c1a6f..aeb87aaa 100644 --- a/plugins/ipam/dhcp/daemon.go +++ b/plugins/ipam/dhcp/daemon.go @@ -172,7 +172,7 @@ func runDaemon(pidfilePath string, hostPrefix string, socketPath string) error { } } - l, err := getListener(socketPath) + l, err := getListener(hostPrefix + socketPath) if err != nil { return fmt.Errorf("Error getting listener: %v", err) } diff --git a/plugins/ipam/dhcp/lease.go b/plugins/ipam/dhcp/lease.go index 17a3f912..dc2a9d92 100644 --- a/plugins/ipam/dhcp/lease.go +++ b/plugins/ipam/dhcp/lease.go @@ -130,7 +130,7 @@ func (l *DHCPLease) acquire() error { opts := make(dhcp4.Options) opts[dhcp4.OptionClientIdentifier] = []byte(l.clientID) - opts[dhcp4.OptionParameterRequestList] = []byte{byte(dhcp4.OptionRouter)} + opts[dhcp4.OptionParameterRequestList] = []byte{byte(dhcp4.OptionRouter), byte(dhcp4.OptionSubnetMask)} pkt, err := backoffRetry(func() (*dhcp4.Packet, error) { ok, ack, err := DhcpRequest(c, opts) diff --git a/plugins/ipam/dhcp/main.go b/plugins/ipam/dhcp/main.go index 70768b43..08b148ca 100644 --- a/plugins/ipam/dhcp/main.go +++ b/plugins/ipam/dhcp/main.go @@ -27,6 +27,7 @@ import ( "github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/cni/pkg/version" + bv "github.com/containernetworking/plugins/pkg/utils/buildversion" ) const defaultSocketPath = "/run/cni/dhcp.sock" @@ -38,7 +39,7 @@ func main() { var socketPath string daemonFlags := flag.NewFlagSet("daemon", flag.ExitOnError) daemonFlags.StringVar(&pidfilePath, "pidfile", "", "optional path to write daemon PID to") - daemonFlags.StringVar(&hostPrefix, "hostprefix", "", "optional prefix to netns") + daemonFlags.StringVar(&hostPrefix, "hostprefix", "", "optional prefix to host root") daemonFlags.StringVar(&socketPath, "socketpath", "", "optional dhcp server socketpath") daemonFlags.Parse(os.Args[2:]) @@ -51,8 +52,7 @@ func main() { os.Exit(1) } } else { - // TODO: implement plugin version - skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.All, "TODO") + skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("dhcp")) } } @@ -80,9 +80,23 @@ func cmdDel(args *skel.CmdArgs) error { return nil } -func cmdGet(args *skel.CmdArgs) error { +func cmdCheck(args *skel.CmdArgs) error { // TODO: implement - return fmt.Errorf("not implemented") + //return fmt.Errorf("not implemented") + // Plugin must return result in same version as specified in netconf + versionDecoder := &version.ConfigDecoder{} + //confVersion, err := versionDecoder.Decode(args.StdinData) + _, err := versionDecoder.Decode(args.StdinData) + if err != nil { + return err + } + + result := ¤t.Result{} + if err := rpcCall("DHCP.Allocate", args, result); err != nil { + return err + } + + return nil } type SocketPathConf struct { diff --git a/plugins/ipam/dhcp/options.go b/plugins/ipam/dhcp/options.go index 6e2e05c6..910e1cc6 100644 --- a/plugins/ipam/dhcp/options.go +++ b/plugins/ipam/dhcp/options.go @@ -98,7 +98,7 @@ func parseCIDRRoutes(opts dhcp4.Options) []*types.Route { } routes = append(routes, rt) - opt = opt[octets+5 : len(opt)] + opt = opt[octets+5:] } } return routes diff --git a/plugins/ipam/dhcp/options_test.go b/plugins/ipam/dhcp/options_test.go index 9f2904bc..961070c2 100644 --- a/plugins/ipam/dhcp/options_test.go +++ b/plugins/ipam/dhcp/options_test.go @@ -24,14 +24,14 @@ import ( func validateRoutes(t *testing.T, routes []*types.Route) { expected := []*types.Route{ - &types.Route{ + { Dst: net.IPNet{ IP: net.IPv4(10, 0, 0, 0), Mask: net.CIDRMask(8, 32), }, GW: net.IPv4(10, 1, 2, 3), }, - &types.Route{ + { Dst: net.IPNet{ IP: net.IPv4(192, 168, 1, 0), Mask: net.CIDRMask(24, 32), diff --git a/plugins/ipam/dhcp/systemd/cni-dhcp.service b/plugins/ipam/dhcp/systemd/cni-dhcp.service new file mode 100644 index 00000000..d6d7bcea --- /dev/null +++ b/plugins/ipam/dhcp/systemd/cni-dhcp.service @@ -0,0 +1,11 @@ +[Unit] +Description=CNI DHCP service +Documentation=https://github.com/containernetworking/plugins/tree/master/plugins/ipam/dhcp +After=network.target cni-dhcp.socket +Requires=cni-dhcp.socket + +[Service] +ExecStart=/opt/cni/bin/dhcp daemon + +[Install] +WantedBy=multi-user.target diff --git a/plugins/ipam/dhcp/systemd/cni-dhcp.socket b/plugins/ipam/dhcp/systemd/cni-dhcp.socket new file mode 100644 index 00000000..8276f91e --- /dev/null +++ b/plugins/ipam/dhcp/systemd/cni-dhcp.socket @@ -0,0 +1,14 @@ +[Unit] +Description=CNI DHCP service socket +Documentation=https://github.com/containernetworking/plugins/tree/master/plugins/ipam/dhcp +PartOf=cni-dhcp.service + +[Socket] +ListenStream=/run/cni/dhcp.sock +SocketMode=0660 +SocketUser=root +SocketGroup=root +RemoveOnStop=true + +[Install] +WantedBy=sockets.target diff --git a/plugins/ipam/host-local/backend/allocator/config.go b/plugins/ipam/host-local/backend/allocator/config.go index b9686316..c8cb2a74 100644 --- a/plugins/ipam/host-local/backend/allocator/config.go +++ b/plugins/ipam/host-local/backend/allocator/config.go @@ -97,7 +97,7 @@ func LoadIPAMConfig(bytes []byte, envArgs string) (*IPAMConfig, string, error) { n.IPAM.IPArgs = append(n.IPAM.IPArgs, n.Args.A.IPs...) } - for idx, _ := range n.IPAM.IPArgs { + for idx := range n.IPAM.IPArgs { if err := canonicalizeIP(&n.IPAM.IPArgs[idx]); err != nil { return nil, "", fmt.Errorf("cannot understand ip: %v", err) } @@ -122,7 +122,7 @@ func LoadIPAMConfig(bytes []byte, envArgs string) (*IPAMConfig, string, error) { // Validate all ranges numV4 := 0 numV6 := 0 - for i, _ := range n.IPAM.Ranges { + for i := range n.IPAM.Ranges { if err := n.IPAM.Ranges[i].Canonicalize(); err != nil { return nil, "", fmt.Errorf("invalid range set %d: %s", i, err) } diff --git a/plugins/ipam/host-local/backend/allocator/config_test.go b/plugins/ipam/host-local/backend/allocator/config_test.go index fc3793f7..84a0398b 100644 --- a/plugins/ipam/host-local/backend/allocator/config_test.go +++ b/plugins/ipam/host-local/backend/allocator/config_test.go @@ -45,7 +45,7 @@ var _ = Describe("IPAM config", func() { Name: "mynet", Type: "host-local", Ranges: []RangeSet{ - RangeSet{ + { { RangeStart: net.IP{10, 1, 2, 9}, RangeEnd: net.IP{10, 1, 2, 20}, diff --git a/plugins/ipam/host-local/backend/allocator/range_set.go b/plugins/ipam/host-local/backend/allocator/range_set.go index efe2f940..da957f53 100644 --- a/plugins/ipam/host-local/backend/allocator/range_set.go +++ b/plugins/ipam/host-local/backend/allocator/range_set.go @@ -61,7 +61,7 @@ func (s *RangeSet) Canonicalize() error { } fam := 0 - for i, _ := range *s { + for i := range *s { if err := (*s)[i].Canonicalize(); err != nil { return err } diff --git a/plugins/ipam/host-local/backend/disk/backend.go b/plugins/ipam/host-local/backend/disk/backend.go index 76407ff3..c3e6b496 100644 --- a/plugins/ipam/host-local/backend/disk/backend.go +++ b/plugins/ipam/host-local/backend/disk/backend.go @@ -98,6 +98,43 @@ func (s *Store) Release(ip net.IP) error { return os.Remove(GetEscapedPath(s.dataDir, ip.String())) } +func (s *Store) FindByKey(id string, ifname string, match string) (bool, error) { + found := false + + err := filepath.Walk(s.dataDir, func(path string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() { + return nil + } + data, err := ioutil.ReadFile(path) + if err != nil { + return nil + } + if strings.TrimSpace(string(data)) == match { + found = true + } + return nil + }) + return found, err + +} + +func (s *Store) FindByID(id string, ifname string) bool { + s.Lock() + defer s.Unlock() + + found := false + match := strings.TrimSpace(id) + LineBreak + ifname + found, err := s.FindByKey(id, ifname, match) + + // Match anything created by this id + if !found && err == nil { + match := strings.TrimSpace(id) + found, err = s.FindByKey(id, ifname, match) + } + + return found +} + func (s *Store) ReleaseByKey(id string, ifname string, match string) (bool, error) { found := false err := filepath.Walk(s.dataDir, func(path string, info os.FileInfo, err error) error { diff --git a/plugins/ipam/host-local/main.go b/plugins/ipam/host-local/main.go index cb1328e4..b5a8f84c 100644 --- a/plugins/ipam/host-local/main.go +++ b/plugins/ipam/host-local/main.go @@ -15,10 +15,12 @@ package main import ( + "encoding/json" "fmt" "net" "strings" + bv "github.com/containernetworking/plugins/pkg/utils/buildversion" "github.com/containernetworking/plugins/plugins/ipam/host-local/backend/allocator" "github.com/containernetworking/plugins/plugins/ipam/host-local/backend/disk" @@ -29,13 +31,38 @@ import ( ) func main() { - // TODO: implement plugin version - skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.All, "TODO") + skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("host-local")) } -func cmdGet(args *skel.CmdArgs) error { - // TODO: implement - return fmt.Errorf("not implemented") +func loadNetConf(bytes []byte) (*types.NetConf, string, error) { + n := &types.NetConf{} + if err := json.Unmarshal(bytes, n); err != nil { + return nil, "", fmt.Errorf("failed to load netconf: %v", err) + } + return n, n.CNIVersion, nil +} + +func cmdCheck(args *skel.CmdArgs) error { + + ipamConf, _, err := allocator.LoadIPAMConfig(args.StdinData, args.Args) + if err != nil { + return err + } + + // Look to see if there is at least one IP address allocated to the container + // in the data dir, irrespective of what that address actually is + store, err := disk.New(ipamConf.Name, ipamConf.DataDir) + if err != nil { + return err + } + defer store.Close() + + containerIpFound := store.FindByID(args.ContainerID, args.IfName) + if containerIpFound == false { + return fmt.Errorf("host-local: Failed to find address added by container %v", args.ContainerID) + } + + return nil } func cmdAdd(args *skel.CmdArgs) error { diff --git a/plugins/ipam/static/main.go b/plugins/ipam/static/main.go index 7ebdab45..b5c501a1 100644 --- a/plugins/ipam/static/main.go +++ b/plugins/ipam/static/main.go @@ -22,10 +22,10 @@ import ( "github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/types" + types020 "github.com/containernetworking/cni/pkg/types/020" "github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/cni/pkg/version" - - "github.com/containernetworking/cni/pkg/types/020" + bv "github.com/containernetworking/plugins/pkg/utils/buildversion" ) // The top-level network config - IPAM plugins are passed the full configuration @@ -58,13 +58,59 @@ type Address struct { } func main() { - // TODO: implement plugin version - skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.All, "TODO") + skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("static")) } -func cmdGet(args *skel.CmdArgs) error { - // TODO: implement - return fmt.Errorf("not implemented") +func loadNetConf(bytes []byte) (*types.NetConf, string, error) { + n := &types.NetConf{} + if err := json.Unmarshal(bytes, n); err != nil { + return nil, "", fmt.Errorf("failed to load netconf: %v", err) + } + return n, n.CNIVersion, nil +} + +func cmdCheck(args *skel.CmdArgs) error { + ipamConf, _, err := LoadIPAMConfig(args.StdinData, args.Args) + if err != nil { + return err + } + + // Get PrevResult from stdin... store in RawPrevResult + n, _, err := loadNetConf(args.StdinData) + if err != nil { + return err + } + + // Parse previous result. + if n.RawPrevResult == nil { + return fmt.Errorf("Required prevResult missing") + } + + if err := version.ParsePrevResult(n); err != nil { + return err + } + + result, err := current.NewResultFromResult(n.PrevResult) + if err != nil { + return err + } + + // Each configured IP should be found in result.IPs + for _, rangeset := range ipamConf.Addresses { + for _, ips := range result.IPs { + // Ensure values are what we expect + if rangeset.Address.IP.Equal(ips.Address.IP) { + if rangeset.Gateway == nil { + break + } else if rangeset.Gateway.Equal(ips.Gateway) { + break + } + return fmt.Errorf("static: Failed to match addr %v on interface %v", ips.Address.IP, args.IfName) + } + } + } + + return nil } // canonicalizeIP makes sure a provided ip is in standard form diff --git a/plugins/linux_only.txt b/plugins/linux_only.txt new file mode 100644 index 00000000..b789fb4c --- /dev/null +++ b/plugins/linux_only.txt @@ -0,0 +1,12 @@ +plugins/ipam/dhcp +plugins/main/bridge +plugins/main/host-device +plugins/main/ipvlan +plugins/main/loopback +plugins/main/macvlan +plugins/main/ptp +plugins/main/vlan +plugins/meta/portmap +plugins/meta/tuning +plugins/meta/bandwidth +plugins/meta/firewall diff --git a/plugins/main/bridge/README.md b/plugins/main/bridge/README.md index 75f8ede0..793c7e43 100644 --- a/plugins/main/bridge/README.md +++ b/plugins/main/bridge/README.md @@ -52,3 +52,8 @@ If the bridge is missing, the plugin will create one on first use and, if gatewa * `hairpinMode` (boolean, optional): set hairpin mode for interfaces on the bridge. Defaults to false. * `ipam` (dictionary, required): IPAM configuration to be used for this network. For L2-only network, create empty dictionary. * `promiscMode` (boolean, optional): set promiscuous mode on the bridge. Defaults to false. +* `vlan` (int, optional): assign VLAN tag. Defaults to none. + +*Note:* The VLAN parameter configures the VLAN tag on the host end of the veth and also enables the vlan_filtering feature on the bridge interface. + +*Note:* To configure uplink for L2 network you need to allow the vlan on the uplink interface by using the following command ``` bridge vlan add vid VLAN_ID dev DEV```. \ No newline at end of file diff --git a/plugins/main/bridge/bridge.go b/plugins/main/bridge/bridge.go index b66f81f0..ae5c3769 100644 --- a/plugins/main/bridge/bridge.go +++ b/plugins/main/bridge/bridge.go @@ -18,12 +18,14 @@ import ( "encoding/json" "errors" "fmt" + "io/ioutil" "net" "os" "runtime" "syscall" - "io/ioutil" + "github.com/j-keck/arping" + "github.com/vishvananda/netlink" "github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/types" @@ -33,8 +35,7 @@ import ( "github.com/containernetworking/plugins/pkg/ipam" "github.com/containernetworking/plugins/pkg/ns" "github.com/containernetworking/plugins/pkg/utils" - "github.com/j-keck/arping" - "github.com/vishvananda/netlink" + bv "github.com/containernetworking/plugins/pkg/utils/buildversion" ) // For testcases to force an error after IPAM has been performed @@ -52,6 +53,7 @@ type NetConf struct { MTU int `json:"mtu"` HairpinMode bool `json:"hairpinMode"` PromiscMode bool `json:"promiscMode"` + Vlan int `json:"vlan"` } type gwInfo struct { @@ -144,7 +146,7 @@ func calcGateways(result *current.Result, n *NetConf) (*gwInfo, *gwInfo, error) return gwsV4, gwsV6, nil } -func ensureBridgeAddr(br *netlink.Bridge, family int, ipn *net.IPNet, forceAddress bool) error { +func ensureAddr(br netlink.Link, family int, ipn *net.IPNet, forceAddress bool) error { addrs, err := netlink.AddrList(br, family) if err != nil && err != syscall.ENOENT { return fmt.Errorf("could not get list of IP addresses: %v", err) @@ -164,34 +166,34 @@ func ensureBridgeAddr(br *netlink.Bridge, family int, ipn *net.IPNet, forceAddre // forceAddress is true, otherwise throw an error. if family == netlink.FAMILY_V4 || a.IPNet.Contains(ipn.IP) || ipn.Contains(a.IPNet.IP) { if forceAddress { - if err = deleteBridgeAddr(br, a.IPNet); err != nil { + if err = deleteAddr(br, a.IPNet); err != nil { return err } } else { - return fmt.Errorf("%q already has an IP address different from %v", br.Name, ipnStr) + return fmt.Errorf("%q already has an IP address different from %v", br.Attrs().Name, ipnStr) } } } addr := &netlink.Addr{IPNet: ipn, Label: ""} if err := netlink.AddrAdd(br, addr); err != nil { - return fmt.Errorf("could not add IP address to %q: %v", br.Name, err) + return fmt.Errorf("could not add IP address to %q: %v", br.Attrs().Name, err) } // Set the bridge's MAC to itself. Otherwise, the bridge will take the // lowest-numbered mac on the bridge, and will change as ifs churn - if err := netlink.LinkSetHardwareAddr(br, br.HardwareAddr); err != nil { + if err := netlink.LinkSetHardwareAddr(br, br.Attrs().HardwareAddr); err != nil { return fmt.Errorf("could not set bridge's mac: %v", err) } return nil } -func deleteBridgeAddr(br *netlink.Bridge, ipn *net.IPNet) error { +func deleteAddr(br netlink.Link, ipn *net.IPNet) error { addr := &netlink.Addr{IPNet: ipn, Label: ""} if err := netlink.AddrDel(br, addr); err != nil { - return fmt.Errorf("could not remove IP address from %q: %v", br.Name, err) + return fmt.Errorf("could not remove IP address from %q: %v", br.Attrs().Name, err) } return nil @@ -209,7 +211,7 @@ func bridgeByName(name string) (*netlink.Bridge, error) { return br, nil } -func ensureBridge(brName string, mtu int, promiscMode bool) (*netlink.Bridge, error) { +func ensureBridge(brName string, mtu int, promiscMode, vlanFiltering bool) (*netlink.Bridge, error) { br := &netlink.Bridge{ LinkAttrs: netlink.LinkAttrs{ Name: brName, @@ -220,6 +222,7 @@ func ensureBridge(brName string, mtu int, promiscMode bool) (*netlink.Bridge, er // default packet limit TxQLen: -1, }, + VlanFiltering: &vlanFiltering, } err := netlink.LinkAdd(br) @@ -247,7 +250,35 @@ func ensureBridge(brName string, mtu int, promiscMode bool) (*netlink.Bridge, er return br, nil } -func setupVeth(netns ns.NetNS, br *netlink.Bridge, ifName string, mtu int, hairpinMode bool) (*current.Interface, *current.Interface, error) { +func ensureVlanInterface(br *netlink.Bridge, vlanId int) (netlink.Link, error) { + name := fmt.Sprintf("%s.%d", br.Name, vlanId) + + brGatewayVeth, err := netlink.LinkByName(name) + if err != nil { + if err.Error() != "Link not found" { + return nil, fmt.Errorf("failed to find interface %q: %v", name, err) + } + + hostNS, err := ns.GetCurrentNS() + if err != nil { + return nil, fmt.Errorf("faild to find host namespace: %v", err) + } + + _, brGatewayIface, err := setupVeth(hostNS, br, name, br.MTU, false, vlanId) + if err != nil { + return nil, fmt.Errorf("faild to create vlan gateway %q: %v", name, err) + } + + brGatewayVeth, err = netlink.LinkByName(brGatewayIface.Name) + if err != nil { + return nil, fmt.Errorf("failed to lookup %q: %v", brGatewayIface.Name, err) + } + } + + return brGatewayVeth, nil +} + +func setupVeth(netns ns.NetNS, br *netlink.Bridge, ifName string, mtu int, hairpinMode bool, vlanID int) (*current.Interface, *current.Interface, error) { contIface := ¤t.Interface{} hostIface := ¤t.Interface{} @@ -284,6 +315,13 @@ func setupVeth(netns ns.NetNS, br *netlink.Bridge, ifName string, mtu int, hairp return nil, nil, fmt.Errorf("failed to setup hairpin mode for %v: %v", hostVeth.Attrs().Name, err) } + if vlanID != 0 { + err = netlink.BridgeVlanAdd(hostVeth, uint16(vlanID), true, true, false, true) + if err != nil { + return nil, nil, fmt.Errorf("failed to setup vlan tag on interface %q: %v", hostIface.Name, err) + } + } + return hostIface, contIface, nil } @@ -293,8 +331,12 @@ func calcGatewayIP(ipn *net.IPNet) net.IP { } func setupBridge(n *NetConf) (*netlink.Bridge, *current.Interface, error) { + vlanFiltering := false + if n.Vlan != 0 { + vlanFiltering = true + } // create bridge if necessary - br, err := ensureBridge(n.BrName, n.MTU, n.PromiscMode) + br, err := ensureBridge(n.BrName, n.MTU, n.PromiscMode, vlanFiltering) if err != nil { return nil, nil, fmt.Errorf("failed to create bridge %q: %v", n.BrName, err) } @@ -355,7 +397,7 @@ func cmdAdd(args *skel.CmdArgs) error { } defer netns.Close() - hostInterface, containerInterface, err := setupVeth(netns, br, args.IfName, n.MTU, n.HairpinMode) + hostInterface, containerInterface, err := setupVeth(netns, br, args.IfName, n.MTU, n.HairpinMode, n.Vlan) if err != nil { return err } @@ -435,16 +477,34 @@ func cmdAdd(args *skel.CmdArgs) error { if n.IsGW { var firstV4Addr net.IP + var vlanInterface *current.Interface // Set the IP address(es) on the bridge and enable forwarding for _, gws := range []*gwInfo{gwsV4, gwsV6} { for _, gw := range gws.gws { if gw.IP.To4() != nil && firstV4Addr == nil { firstV4Addr = gw.IP } + if n.Vlan != 0 { + vlanIface, err := ensureVlanInterface(br, n.Vlan) + if err != nil { + return fmt.Errorf("failed to create vlan interface: %v", err) + } - err = ensureBridgeAddr(br, gws.family, &gw, n.ForceAddress) - if err != nil { - return fmt.Errorf("failed to set bridge addr: %v", err) + if vlanInterface == nil { + vlanInterface = ¤t.Interface{Name: vlanIface.Attrs().Name, + Mac: vlanIface.Attrs().HardwareAddr.String()} + result.Interfaces = append(result.Interfaces, vlanInterface) + } + + err = ensureAddr(vlanIface, gws.family, &gw, n.ForceAddress) + if err != nil { + return fmt.Errorf("failed to set vlan interface for bridge with addr: %v", err) + } + } else { + err = ensureAddr(br, gws.family, &gw, n.ForceAddress) + if err != nil { + return fmt.Errorf("failed to set bridge addr: %v", err) + } } } @@ -536,11 +596,269 @@ func cmdDel(args *skel.CmdArgs) error { } func main() { - // TODO: implement plugin version - skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.All, "TODO") + skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("bridge")) } -func cmdGet(args *skel.CmdArgs) error { - // TODO: implement - return fmt.Errorf("not implemented") +type cniBridgeIf struct { + Name string + ifIndex int + peerIndex int + masterIndex int + found bool +} + +func validateInterface(intf current.Interface, expectInSb bool) (cniBridgeIf, netlink.Link, error) { + + ifFound := cniBridgeIf{found: false} + if intf.Name == "" { + return ifFound, nil, fmt.Errorf("Interface name missing ") + } + + link, err := netlink.LinkByName(intf.Name) + if err != nil { + return ifFound, nil, fmt.Errorf("Interface name %s not found", intf.Name) + } + + if expectInSb { + if intf.Sandbox == "" { + return ifFound, nil, fmt.Errorf("Interface %s is expected to be in a sandbox", intf.Name) + } + } else { + if intf.Sandbox != "" { + return ifFound, nil, fmt.Errorf("Interface %s should not be in sandbox", intf.Name) + } + } + + return ifFound, link, err +} + +func validateCniBrInterface(intf current.Interface, n *NetConf) (cniBridgeIf, error) { + + brFound, link, err := validateInterface(intf, false) + if err != nil { + return brFound, err + } + + _, isBridge := link.(*netlink.Bridge) + if !isBridge { + return brFound, fmt.Errorf("Interface %s does not have link type of bridge", intf.Name) + } + + if intf.Mac != "" { + if intf.Mac != link.Attrs().HardwareAddr.String() { + return brFound, fmt.Errorf("Bridge interface %s Mac doesn't match: %s", intf.Name, intf.Mac) + } + } + + linkPromisc := link.Attrs().Promisc != 0 + if linkPromisc != n.PromiscMode { + return brFound, fmt.Errorf("Bridge interface %s configured Promisc Mode %v doesn't match current state: %v ", + intf.Name, n.PromiscMode, linkPromisc) + } + + brFound.found = true + brFound.Name = link.Attrs().Name + brFound.ifIndex = link.Attrs().Index + brFound.masterIndex = link.Attrs().MasterIndex + + return brFound, nil +} + +func validateCniVethInterface(intf *current.Interface, brIf cniBridgeIf, contIf cniBridgeIf) (cniBridgeIf, error) { + + vethFound, link, err := validateInterface(*intf, false) + if err != nil { + return vethFound, err + } + + _, isVeth := link.(*netlink.Veth) + if !isVeth { + // just skip it, it's not what CNI created + return vethFound, nil + } + + _, vethFound.peerIndex, err = ip.GetVethPeerIfindex(link.Attrs().Name) + if err != nil { + return vethFound, fmt.Errorf("Unable to obtain veth peer index for veth %s", link.Attrs().Name) + } + vethFound.ifIndex = link.Attrs().Index + vethFound.masterIndex = link.Attrs().MasterIndex + + if vethFound.ifIndex != contIf.peerIndex { + return vethFound, nil + } + + if contIf.ifIndex != vethFound.peerIndex { + return vethFound, nil + } + + if vethFound.masterIndex != brIf.ifIndex { + return vethFound, nil + } + + if intf.Mac != "" { + if intf.Mac != link.Attrs().HardwareAddr.String() { + return vethFound, fmt.Errorf("Interface %s Mac doesn't match: %s not found", intf.Name, intf.Mac) + } + } + + vethFound.found = true + vethFound.Name = link.Attrs().Name + + return vethFound, nil +} + +func validateCniContainerInterface(intf current.Interface) (cniBridgeIf, error) { + + vethFound, link, err := validateInterface(intf, true) + if err != nil { + return vethFound, err + } + + _, isVeth := link.(*netlink.Veth) + if !isVeth { + return vethFound, fmt.Errorf("Error: Container interface %s not of type veth", link.Attrs().Name) + } + _, vethFound.peerIndex, err = ip.GetVethPeerIfindex(link.Attrs().Name) + if err != nil { + return vethFound, fmt.Errorf("Unable to obtain veth peer index for veth %s", link.Attrs().Name) + } + vethFound.ifIndex = link.Attrs().Index + + if intf.Mac != "" { + if intf.Mac != link.Attrs().HardwareAddr.String() { + return vethFound, fmt.Errorf("Interface %s Mac %s doesn't match container Mac: %s", intf.Name, intf.Mac, link.Attrs().HardwareAddr) + } + } + + vethFound.found = true + vethFound.Name = link.Attrs().Name + + return vethFound, nil +} + +func cmdCheck(args *skel.CmdArgs) error { + + n, _, err := loadNetConf(args.StdinData) + if err != nil { + return err + } + netns, err := ns.GetNS(args.Netns) + if err != nil { + return fmt.Errorf("failed to open netns %q: %v", args.Netns, err) + } + defer netns.Close() + + // run the IPAM plugin and get back the config to apply + err = ipam.ExecCheck(n.IPAM.Type, args.StdinData) + if err != nil { + return err + } + + // Parse previous result. + if n.NetConf.RawPrevResult == nil { + return fmt.Errorf("Required prevResult missing") + } + + if err := version.ParsePrevResult(&n.NetConf); err != nil { + return err + } + + result, err := current.NewResultFromResult(n.PrevResult) + if err != nil { + return err + } + + var errLink error + var contCNI, vethCNI cniBridgeIf + var brMap, contMap current.Interface + + // Find interfaces for names whe know, CNI Bridge and container + for _, intf := range result.Interfaces { + if n.BrName == intf.Name { + brMap = *intf + continue + } else if args.IfName == intf.Name { + if args.Netns == intf.Sandbox { + contMap = *intf + continue + } + } + } + + brCNI, err := validateCniBrInterface(brMap, n) + if err != nil { + return err + } + + // The namespace must be the same as what was configured + if args.Netns != contMap.Sandbox { + return fmt.Errorf("Sandbox in prevResult %s doesn't match configured netns: %s", + contMap.Sandbox, args.Netns) + } + + // Check interface against values found in the container + if err := netns.Do(func(_ ns.NetNS) error { + contCNI, errLink = validateCniContainerInterface(contMap) + if errLink != nil { + return errLink + } + return nil + }); err != nil { + return err + } + + // Now look for veth that is peer with container interface. + // Anything else wasn't created by CNI, skip it + for _, intf := range result.Interfaces { + // Skip this result if name is the same as cni bridge + // It's either the cni bridge we dealt with above, or something with the + // same name in a different namespace. We just skip since it's not ours + if brMap.Name == intf.Name { + continue + } + + // same here for container name + if contMap.Name == intf.Name { + continue + } + + vethCNI, errLink = validateCniVethInterface(intf, brCNI, contCNI) + if errLink != nil { + return errLink + } + + if vethCNI.found { + // veth with container interface as peer and bridge as master found + break + } + } + + if !brCNI.found { + return fmt.Errorf("CNI created bridge %s in host namespace was not found", n.BrName) + } + if !contCNI.found { + return fmt.Errorf("CNI created interface in container %s not found", args.IfName) + } + if !vethCNI.found { + return fmt.Errorf("CNI veth created for bridge %s was not found", n.BrName) + } + + // Check prevResults for ips, routes and dns against values found in the container + if err := netns.Do(func(_ ns.NetNS) error { + err = ip.ValidateExpectedInterfaceIPs(args.IfName, result.IPs) + if err != nil { + return err + } + + err = ip.ValidateExpectedRoute(result.Routes) + if err != nil { + return err + } + return nil + }); err != nil { + return err + } + + return nil } diff --git a/plugins/main/bridge/bridge_test.go b/plugins/main/bridge/bridge_test.go index 420773bf..97a93645 100644 --- a/plugins/main/bridge/bridge_test.go +++ b/plugins/main/bridge/bridge_test.go @@ -15,7 +15,9 @@ package main import ( + "encoding/json" "fmt" + "github.com/vishvananda/netlink/nl" "io/ioutil" "net" "os" @@ -30,15 +32,33 @@ import ( "github.com/vishvananda/netlink" + "github.com/containernetworking/plugins/plugins/ipam/host-local/backend/allocator" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) const ( - BRNAME = "bridge0" - IFNAME = "eth0" + BRNAME = "bridge0" + BRNAMEVLAN = "bridge0.100" + IFNAME = "eth0" ) +type Net struct { + Name string `json:"name"` + CNIVersion string `json:"cniVersion"` + Type string `json:"type,omitempty"` + BrName string `json:"bridge"` + IPAM *allocator.IPAMConfig `json:"ipam"` + //RuntimeConfig struct { // The capability arg + // IPRanges []RangeSet `json:"ipRanges,omitempty"` + //} `json:"runtimeConfig,omitempty"` + //Args *struct { + // A *IPAMArgs `json:"cni"` + DNS types.DNS `json:"dns"` + RawPrevResult map[string]interface{} `json:"prevResult,omitempty"` + PrevResult current.Result `json:"-"` +} + // testCase defines the CNI network configuration and the expected // bridge addresses for a test case. type testCase struct { @@ -49,6 +69,7 @@ type testCase struct { isGW bool isLayer2 bool expGWCIDRs []string // Expected gateway addresses in CIDR form + vlan int } // Range definition for each entry in the ranges list @@ -78,9 +99,12 @@ const ( "cniVersion": "%s", "name": "testConfig", "type": "bridge", - "bridge": "%s",` + "bridge": "%s"` - netDefault = ` + vlan = `, + "vlan": %d` + + netDefault = `, "isDefaultGateway": true, "ipMasq": false` @@ -120,6 +144,10 @@ const ( // for a test case. func (tc testCase) netConfJSON(dataDir string) string { conf := fmt.Sprintf(netConfStr, tc.cniVersion, BRNAME) + if tc.vlan != 0 { + conf += fmt.Sprintf(vlan, tc.vlan) + } + if !tc.isLayer2 { conf += netDefault if tc.subnet != "" || tc.ranges != nil { @@ -136,7 +164,7 @@ func (tc testCase) netConfJSON(dataDir string) string { conf += ipamEndStr } } else { - conf += ` + conf += `, "ipam": {}` } return "{" + conf + "\n}" @@ -171,7 +199,24 @@ var counter uint // arguments for a test case. func (tc testCase) createCmdArgs(targetNS ns.NetNS, dataDir string) *skel.CmdArgs { conf := tc.netConfJSON(dataDir) - defer func() { counter += 1 }() + //defer func() { counter += 1 }() + return &skel.CmdArgs{ + ContainerID: fmt.Sprintf("dummy-%d", counter), + Netns: targetNS.Path(), + IfName: IFNAME, + StdinData: []byte(conf), + } +} + +// createCheckCmdArgs generates network configuration and creates command +// arguments for a Check test case. +func (tc testCase) createCheckCmdArgs(targetNS ns.NetNS, config *Net, dataDir string) *skel.CmdArgs { + + conf, err := json.Marshal(config) + Expect(err).NotTo(HaveOccurred()) + + // TODO Don't we need to use the same counter as before? + //defer func() { counter += 1 }() return &skel.CmdArgs{ ContainerID: fmt.Sprintf("dummy-%d", counter), Netns: targetNS.Path(), @@ -218,6 +263,38 @@ func delBridgeAddrs(testNS ns.NetNS) { } } + br, err = netlink.LinkByName(BRNAMEVLAN) + if err == nil { + addrs, err = netlink.AddrList(br, netlink.FAMILY_ALL) + Expect(err).NotTo(HaveOccurred()) + for _, addr := range addrs { + if !addr.IP.IsLinkLocalUnicast() { + err = netlink.AddrDel(br, &addr) + Expect(err).NotTo(HaveOccurred()) + } + } + } + + return nil + }) + Expect(err).NotTo(HaveOccurred()) +} + +func delVlanAddrs(testNS ns.NetNS, vlan int) { + err := testNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + vlanLink, err := netlink.LinkByName(fmt.Sprintf("%s.%d", BRNAME, vlan)) + Expect(err).NotTo(HaveOccurred()) + addrs, err := netlink.AddrList(vlanLink, netlink.FAMILY_ALL) + Expect(err).NotTo(HaveOccurred()) + for _, addr := range addrs { + if !addr.IP.IsLinkLocalUnicast() { + err = netlink.AddrDel(vlanLink, &addr) + Expect(err).NotTo(HaveOccurred()) + } + } + return nil }) Expect(err).NotTo(HaveOccurred()) @@ -248,14 +325,27 @@ func countIPAMIPs(path string) (int, error) { return count, nil } +func checkVlan(vlanId int, bridgeVlanInfo []*nl.BridgeVlanInfo) bool { + for _, vlan := range bridgeVlanInfo { + if vlan.Vid == uint16(vlanId) { + return true + } + } + + return false +} + type cmdAddDelTester interface { setNS(testNS ns.NetNS, targetNS ns.NetNS) - cmdAddTest(tc testCase, dataDir string) - cmdDelTest(tc testCase) + cmdAddTest(tc testCase, dataDir string) (*current.Result, error) + cmdCheckTest(tc testCase, conf *Net, dataDir string) + cmdDelTest(tc testCase, dataDir string) } func testerByVersion(version string) cmdAddDelTester { switch { + case strings.HasPrefix(version, "0.4."): + return &testerV04x{} case strings.HasPrefix(version, "0.3."): return &testerV03x{} default: @@ -263,19 +353,19 @@ func testerByVersion(version string) cmdAddDelTester { } } -type testerV03x struct { +type testerV04x struct { testNS ns.NetNS targetNS ns.NetNS args *skel.CmdArgs vethName string } -func (tester *testerV03x) setNS(testNS ns.NetNS, targetNS ns.NetNS) { +func (tester *testerV04x) setNS(testNS ns.NetNS, targetNS ns.NetNS) { tester.testNS = testNS tester.targetNS = targetNS } -func (tester *testerV03x) cmdAddTest(tc testCase, dataDir string) { +func (tester *testerV04x) cmdAddTest(tc testCase, dataDir string) (*current.Result, error) { // Generate network config and command arguments tester.args = tc.createCmdArgs(tester.targetNS, dataDir) @@ -345,10 +435,83 @@ func (tester *testerV03x) cmdAddTest(tc testCase, dataDir string) { // Check that the bridge has a different mac from the veth // If not, it means the bridge has an unstable mac and will change // as ifs are added and removed - // this check is not relevant for a layer 2 bridge - if !tc.isLayer2 { - Expect(link.Attrs().HardwareAddr.String()).NotTo(Equal(bridgeMAC)) + Expect(link.Attrs().HardwareAddr.String()).NotTo(Equal(bridgeMAC)) + + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + // Find the veth peer in the container namespace and the default route + err = tester.targetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + link, err := netlink.LinkByName(IFNAME) + Expect(err).NotTo(HaveOccurred()) + Expect(link.Attrs().Name).To(Equal(IFNAME)) + Expect(link).To(BeAssignableToTypeOf(&netlink.Veth{})) + + expCIDRsV4, expCIDRsV6 := tc.expectedCIDRs() + addrs, err := netlink.AddrList(link, netlink.FAMILY_V4) + Expect(err).NotTo(HaveOccurred()) + Expect(len(addrs)).To(Equal(len(expCIDRsV4))) + addrs, err = netlink.AddrList(link, netlink.FAMILY_V6) + Expect(len(addrs)).To(Equal(len(expCIDRsV6) + 1)) //add one for the link-local + Expect(err).NotTo(HaveOccurred()) + // Ignore link local address which may or may not be + // ready when we read addresses. + var foundAddrs int + for _, addr := range addrs { + if !addr.IP.IsLinkLocalUnicast() { + foundAddrs++ + } } + Expect(foundAddrs).To(Equal(len(expCIDRsV6))) + + // Ensure the default route(s) + routes, err := netlink.RouteList(link, 0) + Expect(err).NotTo(HaveOccurred()) + + var defaultRouteFound4, defaultRouteFound6 bool + for _, cidr := range tc.expGWCIDRs { + gwIP, _, err := net.ParseCIDR(cidr) + Expect(err).NotTo(HaveOccurred()) + var found *bool + if ipVersion(gwIP) == "4" { + found = &defaultRouteFound4 + } else { + found = &defaultRouteFound6 + } + if *found == true { + continue + } + for _, route := range routes { + *found = (route.Dst == nil && route.Src == nil && route.Gw.Equal(gwIP)) + if *found { + break + } + } + Expect(*found).To(Equal(true)) + } + + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + return result, nil +} + +func (tester *testerV04x) cmdCheckTest(tc testCase, conf *Net, dataDir string) { + // Generate network config and command arguments + tester.args = tc.createCheckCmdArgs(tester.targetNS, conf, dataDir) + + // Execute cmdCHECK on the plugin + err := tester.testNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + err := testutils.CmdCheckWithArgs(tester.args, func() error { + return cmdCheck(tester.args) + }) + Expect(err).NotTo(HaveOccurred()) return nil }) @@ -411,7 +574,244 @@ func (tester *testerV03x) cmdAddTest(tc testCase, dataDir string) { Expect(err).NotTo(HaveOccurred()) } -func (tester *testerV03x) cmdDelTest(tc testCase) { +func (tester *testerV04x) cmdDelTest(tc testCase, dataDir string) { + tester.args = tc.createCmdArgs(tester.targetNS, dataDir) + err := tester.testNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + err := testutils.CmdDelWithArgs(tester.args, func() error { + return cmdDel(tester.args) + }) + Expect(err).NotTo(HaveOccurred()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + // Make sure the host veth has been deleted + err = tester.targetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + link, err := netlink.LinkByName(IFNAME) + Expect(err).To(HaveOccurred()) + Expect(link).To(BeNil()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + // Make sure the container veth has been deleted + err = tester.testNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + link, err := netlink.LinkByName(tester.vethName) + Expect(err).To(HaveOccurred()) + Expect(link).To(BeNil()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) +} + +type testerV03x struct { + testNS ns.NetNS + targetNS ns.NetNS + args *skel.CmdArgs + vethName string +} + +func (tester *testerV03x) setNS(testNS ns.NetNS, targetNS ns.NetNS) { + tester.testNS = testNS + tester.targetNS = targetNS +} + +func (tester *testerV03x) cmdAddTest(tc testCase, dataDir string) (*current.Result, error) { + // Generate network config and command arguments + tester.args = tc.createCmdArgs(tester.targetNS, dataDir) + + // Execute cmdADD on the plugin + var result *current.Result + err := tester.testNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + r, raw, err := testutils.CmdAddWithArgs(tester.args, func() error { + return cmdAdd(tester.args) + }) + Expect(err).NotTo(HaveOccurred()) + Expect(strings.Index(string(raw), "\"interfaces\":")).Should(BeNumerically(">", 0)) + + result, err = current.GetResult(r) + Expect(err).NotTo(HaveOccurred()) + + if !tc.isLayer2 && tc.vlan != 0 { + Expect(len(result.Interfaces)).To(Equal(4)) + } else { + Expect(len(result.Interfaces)).To(Equal(3)) + } + + Expect(result.Interfaces[0].Name).To(Equal(BRNAME)) + Expect(result.Interfaces[0].Mac).To(HaveLen(17)) + + Expect(result.Interfaces[1].Name).To(HavePrefix("veth")) + Expect(result.Interfaces[1].Mac).To(HaveLen(17)) + + Expect(result.Interfaces[2].Name).To(Equal(IFNAME)) + Expect(result.Interfaces[2].Mac).To(HaveLen(17)) //mac is random + Expect(result.Interfaces[2].Sandbox).To(Equal(tester.targetNS.Path())) + + // Make sure bridge link exists + link, err := netlink.LinkByName(result.Interfaces[0].Name) + Expect(err).NotTo(HaveOccurred()) + Expect(link.Attrs().Name).To(Equal(BRNAME)) + Expect(link).To(BeAssignableToTypeOf(&netlink.Bridge{})) + Expect(link.Attrs().HardwareAddr.String()).To(Equal(result.Interfaces[0].Mac)) + bridgeMAC := link.Attrs().HardwareAddr.String() + + var vlanLink netlink.Link + if !tc.isLayer2 && tc.vlan != 0 { + // Make sure vlan link exists + vlanLink, err = netlink.LinkByName(fmt.Sprintf("%s.%d", BRNAME, tc.vlan)) + Expect(err).NotTo(HaveOccurred()) + Expect(vlanLink.Attrs().Name).To(Equal(fmt.Sprintf("%s.%d", BRNAME, tc.vlan))) + Expect(vlanLink).To(BeAssignableToTypeOf(&netlink.Veth{})) + + // Check the bridge dot vlan interface have the vlan tag + peerLink, err := netlink.LinkByIndex(vlanLink.Attrs().Index - 1) + Expect(err).NotTo(HaveOccurred()) + interfaceMap, err := netlink.BridgeVlanList() + Expect(err).NotTo(HaveOccurred()) + vlans, isExist := interfaceMap[int32(peerLink.Attrs().Index)] + Expect(isExist).To(BeTrue()) + Expect(checkVlan(tc.vlan, vlans)).To(BeTrue()) + } + + // Check the bridge vlan filtering equals true + if tc.vlan != 0 { + Expect(*link.(*netlink.Bridge).VlanFiltering).To(Equal(true)) + } else { + Expect(*link.(*netlink.Bridge).VlanFiltering).To(Equal(false)) + } + + // Ensure bridge has expected gateway address(es) + var addrs []netlink.Addr + if tc.vlan == 0 { + addrs, err = netlink.AddrList(link, netlink.FAMILY_ALL) + } else { + addrs, err = netlink.AddrList(vlanLink, netlink.FAMILY_ALL) + } + Expect(err).NotTo(HaveOccurred()) + Expect(len(addrs)).To(BeNumerically(">", 0)) + for _, cidr := range tc.expGWCIDRs { + ip, subnet, err := net.ParseCIDR(cidr) + Expect(err).NotTo(HaveOccurred()) + + found := false + subnetPrefix, subnetBits := subnet.Mask.Size() + for _, a := range addrs { + aPrefix, aBits := a.IPNet.Mask.Size() + if a.IPNet.IP.Equal(ip) && aPrefix == subnetPrefix && aBits == subnetBits { + found = true + break + } + } + Expect(found).To(Equal(true)) + } + + // Check for the veth link in the main namespace + links, err := netlink.LinkList() + Expect(err).NotTo(HaveOccurred()) + if !tc.isLayer2 && tc.vlan != 0 { + Expect(len(links)).To(Equal(5)) // Bridge, Bridge vlan veth, veth, and loopback + } else { + Expect(len(links)).To(Equal(3)) // Bridge, veth, and loopback + } + + link, err = netlink.LinkByName(result.Interfaces[1].Name) + Expect(err).NotTo(HaveOccurred()) + Expect(link).To(BeAssignableToTypeOf(&netlink.Veth{})) + tester.vethName = result.Interfaces[1].Name + + // check vlan exist on the veth interface + if tc.vlan != 0 { + interfaceMap, err := netlink.BridgeVlanList() + Expect(err).NotTo(HaveOccurred()) + vlans, isExist := interfaceMap[int32(link.Attrs().Index)] + Expect(isExist).To(BeTrue()) + Expect(checkVlan(tc.vlan, vlans)).To(BeTrue()) + } + + // Check that the bridge has a different mac from the veth + // If not, it means the bridge has an unstable mac and will change + // as ifs are added and removed + // this check is not relevant for a layer 2 bridge + if !tc.isLayer2 && tc.vlan == 0 { + Expect(link.Attrs().HardwareAddr.String()).NotTo(Equal(bridgeMAC)) + } + + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + // Find the veth peer in the container namespace and the default route + err = tester.targetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + link, err := netlink.LinkByName(IFNAME) + Expect(err).NotTo(HaveOccurred()) + Expect(link.Attrs().Name).To(Equal(IFNAME)) + Expect(link).To(BeAssignableToTypeOf(&netlink.Veth{})) + + expCIDRsV4, expCIDRsV6 := tc.expectedCIDRs() + addrs, err := netlink.AddrList(link, netlink.FAMILY_V4) + Expect(err).NotTo(HaveOccurred()) + Expect(len(addrs)).To(Equal(len(expCIDRsV4))) + addrs, err = netlink.AddrList(link, netlink.FAMILY_V6) + Expect(len(addrs)).To(Equal(len(expCIDRsV6) + 1)) //add one for the link-local + Expect(err).NotTo(HaveOccurred()) + // Ignore link local address which may or may not be + // ready when we read addresses. + var foundAddrs int + for _, addr := range addrs { + if !addr.IP.IsLinkLocalUnicast() { + foundAddrs++ + } + } + Expect(foundAddrs).To(Equal(len(expCIDRsV6))) + + // Ensure the default route(s) + routes, err := netlink.RouteList(link, 0) + Expect(err).NotTo(HaveOccurred()) + + var defaultRouteFound4, defaultRouteFound6 bool + for _, cidr := range tc.expGWCIDRs { + gwIP, _, err := net.ParseCIDR(cidr) + Expect(err).NotTo(HaveOccurred()) + var found *bool + if ipVersion(gwIP) == "4" { + found = &defaultRouteFound4 + } else { + found = &defaultRouteFound6 + } + if *found == true { + continue + } + for _, route := range routes { + *found = (route.Dst == nil && route.Src == nil && route.Gw.Equal(gwIP)) + if *found { + break + } + } + Expect(*found).To(Equal(true)) + } + + return nil + }) + Expect(err).NotTo(HaveOccurred()) + return result, nil +} + +func (tester *testerV03x) cmdCheckTest(tc testCase, conf *Net, dataDir string) { + return +} + +func (tester *testerV03x) cmdDelTest(tc testCase, dataDir string) { err := tester.testNS.Do(func(ns.NetNS) error { defer GinkgoRecover() @@ -458,7 +858,7 @@ func (tester *testerV01xOr02x) setNS(testNS ns.NetNS, targetNS ns.NetNS) { tester.targetNS = targetNS } -func (tester *testerV01xOr02x) cmdAddTest(tc testCase, dataDir string) { +func (tester *testerV01xOr02x) cmdAddTest(tc testCase, dataDir string) (*current.Result, error) { // Generate network config and calculate gateway addresses tester.args = tc.createCmdArgs(tester.targetNS, dataDir) @@ -549,9 +949,14 @@ func (tester *testerV01xOr02x) cmdAddTest(tc testCase, dataDir string) { return nil }) Expect(err).NotTo(HaveOccurred()) + return nil, nil } -func (tester *testerV01xOr02x) cmdDelTest(tc testCase) { +func (tester *testerV01xOr02x) cmdCheckTest(tc testCase, conf *Net, dataDir string) { + return +} + +func (tester *testerV01xOr02x) cmdDelTest(tc testCase, dataDir string) { err := tester.testNS.Do(func(ns.NetNS) error { defer GinkgoRecover() @@ -586,15 +991,107 @@ func cmdAddDelTest(testNS ns.NetNS, tc testCase, dataDir string) { tester.setNS(testNS, targetNS) // Test IP allocation - tester.cmdAddTest(tc, dataDir) + result, err := tester.cmdAddTest(tc, dataDir) + Expect(err).NotTo(HaveOccurred()) + + if strings.HasPrefix(tc.cniVersion, "0.3.") { + Expect(result).NotTo(BeNil()) + } else { + Expect(result).To(BeNil()) + } // Test IP Release - tester.cmdDelTest(tc) + tester.cmdDelTest(tc, dataDir) // Clean up bridge addresses for next test case delBridgeAddrs(testNS) } +func buildOneConfig(name, cniVersion string, orig *Net, prevResult types.Result) (*Net, error) { + var err error + + inject := map[string]interface{}{ + "name": name, + "cniVersion": cniVersion, + } + // Add previous plugin result + if prevResult != nil { + inject["prevResult"] = prevResult + } + + // Ensure every config uses the same name and version + config := make(map[string]interface{}) + confBytes, err := json.Marshal(orig) + if err != nil { + return nil, err + } + + err = json.Unmarshal(confBytes, &config) + if err != nil { + return nil, fmt.Errorf("unmarshal existing network bytes: %s", err) + } + + for key, value := range inject { + config[key] = value + } + + newBytes, err := json.Marshal(config) + if err != nil { + return nil, err + } + + conf := &Net{} + if err := json.Unmarshal(newBytes, &conf); err != nil { + return nil, fmt.Errorf("error parsing configuration: %s", err) + } + + return conf, nil + +} + +func cmdAddDelCheckTest(testNS ns.NetNS, tc testCase, dataDir string) { + Expect(tc.cniVersion).To(Equal("0.4.0")) + + // Get a Add/Del tester based on test case version + tester := testerByVersion(tc.cniVersion) + + targetNS, err := testutils.NewNS() + Expect(err).NotTo(HaveOccurred()) + defer targetNS.Close() + tester.setNS(testNS, targetNS) + + // Test IP allocation + prevResult, err := tester.cmdAddTest(tc, dataDir) + Expect(err).NotTo(HaveOccurred()) + + Expect(prevResult).NotTo(BeNil()) + + confString := tc.netConfJSON(dataDir) + + conf := &Net{} + err = json.Unmarshal([]byte(confString), &conf) + Expect(err).NotTo(HaveOccurred()) + + conf.IPAM, _, err = allocator.LoadIPAMConfig([]byte(confString), "") + Expect(err).NotTo(HaveOccurred()) + + newConf, err := buildOneConfig("testConfig", tc.cniVersion, conf, prevResult) + Expect(err).NotTo(HaveOccurred()) + + // Test CHECK + tester.cmdCheckTest(tc, newConf, dataDir) + + // Test IP Release + tester.cmdDelTest(tc, dataDir) + + // Clean up bridge addresses for next test case + delBridgeAddrs(testNS) + + if tc.vlan != 0 && !tc.isLayer2 { + delVlanAddrs(testNS, tc.vlan) + } +} + var _ = Describe("bridge Operations", func() { var originalNS ns.NetNS var dataDir string @@ -722,6 +1219,63 @@ var _ = Describe("bridge Operations", func() { cmdAddDelTest(originalNS, tc, dataDir) }) + It("configures and deconfigures a l2 bridge with vlan id 100 using ADD/DEL for 0.3.1 config", func() { + tc := testCase{cniVersion: "0.3.0", isLayer2: true, vlan: 100} + cmdAddDelTest(originalNS, tc, dataDir) + }) + + It("configures and deconfigures a l2 bridge with vlan id 100 using ADD/DEL for 0.3.1 config", func() { + tc := testCase{cniVersion: "0.3.1", isLayer2: true, vlan: 100} + cmdAddDelTest(originalNS, tc, dataDir) + }) + + It("configures and deconfigures a bridge, veth with default route and vlanID 100 with ADD/DEL for 0.3.0 config", func() { + testCases := []testCase{ + { + // IPv4 only + subnet: "10.1.2.0/24", + expGWCIDRs: []string{"10.1.2.1/24"}, + vlan: 100, + }, + { + // IPv6 only + subnet: "2001:db8::0/64", + expGWCIDRs: []string{"2001:db8::1/64"}, + vlan: 100, + }, + { + // Dual-Stack + ranges: []rangeInfo{ + {subnet: "192.168.0.0/24"}, + {subnet: "fd00::0/64"}, + }, + expGWCIDRs: []string{ + "192.168.0.1/24", + "fd00::1/64", + }, + vlan: 100, + }, + { + // 3 Subnets (1 IPv4 and 2 IPv6 subnets) + ranges: []rangeInfo{ + {subnet: "192.168.0.0/24"}, + {subnet: "fd00::0/64"}, + {subnet: "2001:db8::0/64"}, + }, + expGWCIDRs: []string{ + "192.168.0.1/24", + "fd00::1/64", + "2001:db8::1/64", + }, + vlan: 100, + }, + } + for _, tc := range testCases { + tc.cniVersion = "0.3.0" + cmdAddDelTest(originalNS, tc, dataDir) + } + }) + It("configures and deconfigures a bridge and veth with default route with ADD/DEL for 0.3.1 config", func() { testCases := []testCase{ { @@ -767,7 +1321,7 @@ var _ = Describe("bridge Operations", func() { tester.args = tc.createCmdArgs(targetNS, dataDir) // Execute cmdDEL on the plugin, expect no errors - tester.cmdDelTest(tc) + tester.cmdDelTest(tc, dataDir) }) It("configures and deconfigures a bridge and veth with default route with ADD/DEL for 0.1.0 config", func() { @@ -870,13 +1424,13 @@ var _ = Describe("bridge Operations", func() { Expect(conf.ForceAddress).To(Equal(false)) // Set first address on bridge - err = ensureBridgeAddr(bridge, family, &gwnFirst, conf.ForceAddress) + err = ensureAddr(bridge, family, &gwnFirst, conf.ForceAddress) Expect(err).NotTo(HaveOccurred()) checkBridgeIPs(tc.gwCIDRFirst, "") // Attempt to set the second address on the bridge // with ForceAddress set to false. - err = ensureBridgeAddr(bridge, family, &gwnSecond, false) + err = ensureAddr(bridge, family, &gwnSecond, false) if family == netlink.FAMILY_V4 || subnetsOverlap { // IPv4 or overlapping IPv6 subnets: // Expect an error, and address should remain the same @@ -892,7 +1446,7 @@ var _ = Describe("bridge Operations", func() { // Set the second address on the bridge // with ForceAddress set to true. - err = ensureBridgeAddr(bridge, family, &gwnSecond, true) + err = ensureAddr(bridge, family, &gwnSecond, true) Expect(err).NotTo(HaveOccurred()) if family == netlink.FAMILY_V4 || subnetsOverlap { // IPv4 or overlapping IPv6 subnets: @@ -1007,4 +1561,38 @@ var _ = Describe("bridge Operations", func() { }) Expect(err).NotTo(HaveOccurred()) }) + + It("configures and deconfigures a bridge and veth with default route with ADD/DEL/CHECK for 0.4.0 config", func() { + testCases := []testCase{ + { + // IPv4 only + ranges: []rangeInfo{{ + subnet: "10.1.2.0/24", + }}, + expGWCIDRs: []string{"10.1.2.1/24"}, + }, + { + // IPv6 only + ranges: []rangeInfo{{ + subnet: "2001:db8::0/64", + }}, + expGWCIDRs: []string{"2001:db8::1/64"}, + }, + { + // Dual-Stack + ranges: []rangeInfo{ + {subnet: "192.168.0.0/24"}, + {subnet: "fd00::0/64"}, + }, + expGWCIDRs: []string{ + "192.168.0.1/24", + "fd00::1/64", + }, + }, + } + for _, tc := range testCases { + tc.cniVersion = "0.4.0" + cmdAddDelCheckTest(originalNS, tc, dataDir) + } + }) }) diff --git a/plugins/main/host-device/host-device.go b/plugins/main/host-device/host-device.go index 437e2440..f82e2967 100644 --- a/plugins/main/host-device/host-device.go +++ b/plugins/main/host-device/host-device.go @@ -25,13 +25,17 @@ import ( "runtime" "strings" + "github.com/vishvananda/netlink" + "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/ip" "github.com/containernetworking/plugins/pkg/ipam" "github.com/containernetworking/plugins/pkg/ns" - "github.com/vishvananda/netlink" + bv "github.com/containernetworking/plugins/pkg/utils/buildversion" ) //NetConf for host-device config, look the README to learn how to use those parameters @@ -81,6 +85,7 @@ func cmdAdd(args *skel.CmdArgs) error { return fmt.Errorf("failed to move link %v", err) } + var result *current.Result // run the IPAM plugin and get back the config to apply if cfg.IPAM.Type != "" { r, err := ipam.ExecAdd(cfg.IPAM.Type, args.StdinData) @@ -96,7 +101,7 @@ func cmdAdd(args *skel.CmdArgs) error { }() // Convert whatever the IPAM result was into the current Result type - result, err := current.NewResultFromResult(r) + result, err = current.NewResultFromResult(r) if err != nil { return err } @@ -124,6 +129,10 @@ func cmdAdd(args *skel.CmdArgs) error { if err != nil { return err } + + result.DNS = cfg.DNS + + return types.PrintResult(result, cfg.CNIVersion) } return printLink(contDev, cfg.CNIVersion, containerNs) @@ -275,11 +284,109 @@ func getLink(devname, hwaddr, kernelpath string) (netlink.Link, error) { } func main() { - // TODO: implement plugin version - skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.All, "TODO") + skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("host-device")) } -func cmdGet(args *skel.CmdArgs) error { - // TODO: implement - return fmt.Errorf("not implemented") +func cmdCheck(args *skel.CmdArgs) error { + + cfg, err := loadConf(args.StdinData) + if err != nil { + return err + } + netns, err := ns.GetNS(args.Netns) + if err != nil { + return fmt.Errorf("failed to open netns %q: %v", args.Netns, err) + } + defer netns.Close() + + // run the IPAM plugin and get back the config to apply + if cfg.IPAM.Type != "" { + err = ipam.ExecCheck(cfg.IPAM.Type, args.StdinData) + if err != nil { + return err + } + } + + // Parse previous result. + if cfg.NetConf.RawPrevResult == nil { + return fmt.Errorf("Required prevResult missing") + } + + if err := version.ParsePrevResult(&cfg.NetConf); err != nil { + return err + } + + result, err := current.NewResultFromResult(cfg.PrevResult) + if err != nil { + return err + } + + var contMap current.Interface + // Find interfaces for name we know, that of host-device inside container + for _, intf := range result.Interfaces { + if args.IfName == intf.Name { + if args.Netns == intf.Sandbox { + contMap = *intf + continue + } + } + } + + // The namespace must be the same as what was configured + if args.Netns != contMap.Sandbox { + return fmt.Errorf("Sandbox in prevResult %s doesn't match configured netns: %s", + contMap.Sandbox, args.Netns) + } + + // + // Check prevResults for ips, routes and dns against values found in the container + if err := netns.Do(func(_ ns.NetNS) error { + + // Check interface against values found in the container + err := validateCniContainerInterface(contMap) + if err != nil { + return err + } + + err = ip.ValidateExpectedInterfaceIPs(args.IfName, result.IPs) + if err != nil { + return err + } + + err = ip.ValidateExpectedRoute(result.Routes) + if err != nil { + return err + } + return nil + }); err != nil { + return err + } + + // + return nil +} + +func validateCniContainerInterface(intf current.Interface) error { + + var link netlink.Link + var err error + + if intf.Name == "" { + return fmt.Errorf("Container interface name missing in prevResult: %v", intf.Name) + } + link, err = netlink.LinkByName(intf.Name) + if err != nil { + return fmt.Errorf("Container Interface name in prevResult: %s not found", intf.Name) + } + if intf.Sandbox == "" { + return fmt.Errorf("Error: Container interface %s should not be in host namespace", link.Attrs().Name) + } + + if intf.Mac != "" { + if intf.Mac != link.Attrs().HardwareAddr.String() { + return fmt.Errorf("Interface %s Mac %s doesn't match container Mac: %s", intf.Name, intf.Mac, link.Attrs().HardwareAddr) + } + } + + return nil } diff --git a/plugins/main/host-device/host-device_test.go b/plugins/main/host-device/host-device_test.go index a894b4b7..6a230044 100644 --- a/plugins/main/host-device/host-device_test.go +++ b/plugins/main/host-device/host-device_test.go @@ -15,19 +15,210 @@ package main import ( + "encoding/json" "fmt" "math/rand" + "net" + "strings" "github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/types" + types020 "github.com/containernetworking/cni/pkg/types/020" "github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/plugins/pkg/ns" "github.com/containernetworking/plugins/pkg/testutils" + . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/vishvananda/netlink" ) +type Net struct { + Name string `json:"name"` + CNIVersion string `json:"cniVersion"` + Type string `json:"type,omitempty"` + Device string `json:"device"` // Device-Name, something like eth0 or can0 etc. + HWAddr string `json:"hwaddr"` // MAC Address of target network interface + KernelPath string `json:"kernelpath"` // Kernelpath of the device + IPAM *IPAMConfig `json:"ipam,omitempty"` + DNS types.DNS `json:"dns"` + RawPrevResult map[string]interface{} `json:"prevResult,omitempty"` + PrevResult current.Result `json:"-"` +} + +type IPAMConfig struct { + Name string + Type string `json:"type"` + Routes []*types.Route `json:"routes"` + Addresses []Address `json:"addresses,omitempty"` + DNS types.DNS `json:"dns"` +} + +type IPAMEnvArgs struct { + types.CommonArgs + IP types.UnmarshallableString `json:"ip,omitempty"` + GATEWAY types.UnmarshallableString `json:"gateway,omitempty"` +} + +type Address struct { + AddressStr string `json:"address"` + Gateway net.IP `json:"gateway,omitempty"` + Address net.IPNet + Version string +} + +// canonicalizeIP makes sure a provided ip is in standard form +func canonicalizeIP(ip *net.IP) error { + if ip.To4() != nil { + *ip = ip.To4() + return nil + } else if ip.To16() != nil { + *ip = ip.To16() + return nil + } + return fmt.Errorf("IP %s not v4 nor v6", *ip) +} + +// LoadIPAMConfig creates IPAMConfig using json encoded configuration provided +// as `bytes`. At the moment values provided in envArgs are ignored so there +// is no possibility to overload the json configuration using envArgs +func LoadIPAMConfig(bytes []byte, envArgs string) (*IPAMConfig, string, error) { + n := Net{} + if err := json.Unmarshal(bytes, &n); err != nil { + return nil, "", err + } + + if n.IPAM == nil { + return nil, "", fmt.Errorf("IPAM config missing 'ipam' key") + } + + // Validate all ranges + numV4 := 0 + numV6 := 0 + + for i := range n.IPAM.Addresses { + ip, addr, err := net.ParseCIDR(n.IPAM.Addresses[i].AddressStr) + if err != nil { + return nil, "", fmt.Errorf("invalid CIDR %s: %s", n.IPAM.Addresses[i].AddressStr, err) + } + n.IPAM.Addresses[i].Address = *addr + n.IPAM.Addresses[i].Address.IP = ip + + if err := canonicalizeIP(&n.IPAM.Addresses[i].Address.IP); err != nil { + return nil, "", fmt.Errorf("invalid address %d: %s", i, err) + } + + if n.IPAM.Addresses[i].Address.IP.To4() != nil { + n.IPAM.Addresses[i].Version = "4" + numV4++ + } else { + n.IPAM.Addresses[i].Version = "6" + numV6++ + } + } + + if envArgs != "" { + e := IPAMEnvArgs{} + err := types.LoadArgs(envArgs, &e) + if err != nil { + return nil, "", err + } + + if e.IP != "" { + for _, item := range strings.Split(string(e.IP), ",") { + ipstr := strings.TrimSpace(item) + + ip, subnet, err := net.ParseCIDR(ipstr) + if err != nil { + return nil, "", fmt.Errorf("invalid CIDR %s: %s", ipstr, err) + } + + addr := Address{Address: net.IPNet{IP: ip, Mask: subnet.Mask}} + if addr.Address.IP.To4() != nil { + addr.Version = "4" + numV4++ + } else { + addr.Version = "6" + numV6++ + } + n.IPAM.Addresses = append(n.IPAM.Addresses, addr) + } + } + + if e.GATEWAY != "" { + for _, item := range strings.Split(string(e.GATEWAY), ",") { + gwip := net.ParseIP(strings.TrimSpace(item)) + if gwip == nil { + return nil, "", fmt.Errorf("invalid gateway address: %s", item) + } + + for i := range n.IPAM.Addresses { + if n.IPAM.Addresses[i].Address.Contains(gwip) { + n.IPAM.Addresses[i].Gateway = gwip + } + } + } + } + } + + // CNI spec 0.2.0 and below supported only one v4 and v6 address + if numV4 > 1 || numV6 > 1 { + for _, v := range types020.SupportedVersions { + if n.CNIVersion == v { + return nil, "", fmt.Errorf("CNI version %v does not support more than 1 address per family", n.CNIVersion) + } + } + } + + // Copy net name into IPAM so not to drag Net struct around + n.IPAM.Name = n.Name + + return n.IPAM, n.CNIVersion, nil +} + +func buildOneConfig(name, cniVersion string, orig *Net, prevResult types.Result) (*Net, error) { + var err error + + inject := map[string]interface{}{ + "name": name, + "cniVersion": cniVersion, + } + // Add previous plugin result + if prevResult != nil { + inject["prevResult"] = prevResult + } + + // Ensure every config uses the same name and version + config := make(map[string]interface{}) + + confBytes, err := json.Marshal(orig) + if err != nil { + return nil, err + } + + err = json.Unmarshal(confBytes, &config) + if err != nil { + return nil, fmt.Errorf("unmarshal existing network bytes: %s", err) + } + + for key, value := range inject { + config[key] = value + } + + newBytes, err := json.Marshal(config) + if err != nil { + return nil, err + } + + conf := &Net{} + if err := json.Unmarshal(newBytes, &conf); err != nil { + return nil, fmt.Errorf("error parsing configuration: %s", err) + } + + return conf, nil + +} + var _ = Describe("base functionality", func() { var originalNS ns.NetNS var ifname string @@ -262,4 +453,255 @@ var _ = Describe("base functionality", func() { }) + It("Works with a valid 0.4.0 config without IPAM", func() { + var origLink netlink.Link + + // prepare ifname in original namespace + err := originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + err := netlink.LinkAdd(&netlink.Dummy{ + LinkAttrs: netlink.LinkAttrs{ + Name: ifname, + }, + }) + Expect(err).NotTo(HaveOccurred()) + origLink, err = netlink.LinkByName(ifname) + Expect(err).NotTo(HaveOccurred()) + err = netlink.LinkSetUp(origLink) + Expect(err).NotTo(HaveOccurred()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + // call CmdAdd + targetNS, err := testutils.NewNS() + Expect(err).NotTo(HaveOccurred()) + + cniName := "eth0" + conf := fmt.Sprintf(`{ + "cniVersion": "0.4.0", + "name": "cni-plugin-host-device-test", + "type": "host-device", + "device": %q + }`, ifname) + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: targetNS.Path(), + IfName: cniName, + StdinData: []byte(conf), + } + var resI types.Result + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + var err error + resI, _, err = testutils.CmdAddWithArgs(args, func() error { return cmdAdd(args) }) + return err + }) + Expect(err).NotTo(HaveOccurred()) + + // check that the result was sane + res, err := current.NewResultFromResult(resI) + Expect(err).NotTo(HaveOccurred()) + Expect(res.Interfaces).To(Equal([]*current.Interface{ + { + Name: cniName, + Mac: origLink.Attrs().HardwareAddr.String(), + Sandbox: targetNS.Path(), + }, + })) + + // assert that dummy0 is now in the target namespace + err = targetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + link, err := netlink.LinkByName(cniName) + Expect(err).NotTo(HaveOccurred()) + Expect(link.Attrs().HardwareAddr).To(Equal(origLink.Attrs().HardwareAddr)) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + // assert that dummy0 is now NOT in the original namespace anymore + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + _, err := netlink.LinkByName(ifname) + Expect(err).To(HaveOccurred()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + // call CmdCheck + n := &Net{} + err = json.Unmarshal([]byte(conf), &n) + Expect(err).NotTo(HaveOccurred()) + + cniVersion := "0.4.0" + newConf, err := buildOneConfig("testConfig", cniVersion, n, res) + Expect(err).NotTo(HaveOccurred()) + + confString, err := json.Marshal(newConf) + Expect(err).NotTo(HaveOccurred()) + + args.StdinData = confString + + // CNI Check host-device in the target namespace + + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + var err error + err = testutils.CmdCheckWithArgs(args, func() error { return cmdCheck(args) }) + return err + }) + Expect(err).NotTo(HaveOccurred()) + + // Check that deleting the device moves it back and restores the name + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + err = testutils.CmdDelWithArgs(args, func() error { + return cmdDel(args) + }) + Expect(err).NotTo(HaveOccurred()) + + _, err := netlink.LinkByName(ifname) + Expect(err).NotTo(HaveOccurred()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + }) + + It("Works with a valid 0.4.0 config with IPAM", func() { + var origLink netlink.Link + + // prepare ifname in original namespace + err := originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + err := netlink.LinkAdd(&netlink.Dummy{ + LinkAttrs: netlink.LinkAttrs{ + Name: ifname, + }, + }) + Expect(err).NotTo(HaveOccurred()) + origLink, err = netlink.LinkByName(ifname) + Expect(err).NotTo(HaveOccurred()) + err = netlink.LinkSetUp(origLink) + Expect(err).NotTo(HaveOccurred()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + // call CmdAdd + targetNS, err := testutils.NewNS() + Expect(err).NotTo(HaveOccurred()) + targetIP := "10.10.0.1/24" + cniName := "eth0" + conf := fmt.Sprintf(`{ + "cniVersion": "0.4.0", + "name": "cni-plugin-host-device-test", + "type": "host-device", + "ipam": { + "type": "static", + "addresses": [ + { + "address":"`+targetIP+`", + "gateway": "10.10.0.254" + }] + }, + "device": %q + }`, ifname) + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: targetNS.Path(), + IfName: cniName, + StdinData: []byte(conf), + } + var resI types.Result + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + var err error + resI, _, err = testutils.CmdAddWithArgs(args, func() error { return cmdAdd(args) }) + return err + }) + Expect(err).NotTo(HaveOccurred()) + + // check that the result was sane + res, err := current.NewResultFromResult(resI) + Expect(err).NotTo(HaveOccurred()) + Expect(res.Interfaces).To(Equal([]*current.Interface{ + { + Name: cniName, + Mac: origLink.Attrs().HardwareAddr.String(), + Sandbox: targetNS.Path(), + }, + })) + + // assert that dummy0 is now in the target namespace + err = targetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + link, err := netlink.LinkByName(cniName) + Expect(err).NotTo(HaveOccurred()) + Expect(link.Attrs().HardwareAddr).To(Equal(origLink.Attrs().HardwareAddr)) + + //get the IP address of the interface in the target namespace + addrs, err := netlink.AddrList(link, netlink.FAMILY_V4) + Expect(err).NotTo(HaveOccurred()) + addr := addrs[0].IPNet.String() + //assert that IP address is what we set + Expect(addr).To(Equal(targetIP)) + + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + // assert that dummy0 is now NOT in the original namespace anymore + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + _, err := netlink.LinkByName(ifname) + Expect(err).To(HaveOccurred()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + // call CmdCheck + n := &Net{} + err = json.Unmarshal([]byte(conf), &n) + Expect(err).NotTo(HaveOccurred()) + + n.IPAM, _, err = LoadIPAMConfig([]byte(conf), "") + Expect(err).NotTo(HaveOccurred()) + + cniVersion := "0.4.0" + newConf, err := buildOneConfig("testConfig", cniVersion, n, res) + Expect(err).NotTo(HaveOccurred()) + + confString, err := json.Marshal(newConf) + Expect(err).NotTo(HaveOccurred()) + + args.StdinData = confString + + // CNI Check host-device in the target namespace + + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + var err error + err = testutils.CmdCheckWithArgs(args, func() error { return cmdCheck(args) }) + return err + }) + Expect(err).NotTo(HaveOccurred()) + + // Check that deleting the device moves it back and restores the name + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + err = testutils.CmdDelWithArgs(args, func() error { + return cmdDel(args) + }) + Expect(err).NotTo(HaveOccurred()) + + _, err := netlink.LinkByName(ifname) + Expect(err).NotTo(HaveOccurred()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + }) + }) diff --git a/plugins/main/ipvlan/ipvlan.go b/plugins/main/ipvlan/ipvlan.go index fa83e1bc..162edca0 100644 --- a/plugins/main/ipvlan/ipvlan.go +++ b/plugins/main/ipvlan/ipvlan.go @@ -20,24 +20,21 @@ import ( "fmt" "runtime" + "github.com/vishvananda/netlink" + "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/ip" "github.com/containernetworking/plugins/pkg/ipam" "github.com/containernetworking/plugins/pkg/ns" - "github.com/vishvananda/netlink" + bv "github.com/containernetworking/plugins/pkg/utils/buildversion" ) type NetConf struct { types.NetConf - - // support chaining for master interface and IP decisions - // occurring prior to running ipvlan plugin - RawPrevResult *map[string]interface{} `json:"prevResult"` - PrevResult *current.Result `json:"-"` - Master string `json:"master"` Mode string `json:"mode"` MTU int `json:"mtu"` @@ -50,33 +47,35 @@ func init() { runtime.LockOSThread() } -func loadConf(bytes []byte) (*NetConf, string, error) { +func loadConf(bytes []byte, cmdCheck bool) (*NetConf, string, error) { n := &NetConf{} if err := json.Unmarshal(bytes, n); err != nil { return nil, "", fmt.Errorf("failed to load netconf: %v", err) } + + if cmdCheck { + return n, n.CNIVersion, nil + } + + var result *current.Result + var err error // Parse previous result - if n.RawPrevResult != nil { - resultBytes, err := json.Marshal(n.RawPrevResult) - if err != nil { - return nil, "", fmt.Errorf("could not serialize prevResult: %v", err) - } - res, err := version.NewResult(n.CNIVersion, resultBytes) - if err != nil { + if n.NetConf.RawPrevResult != nil { + if err = version.ParsePrevResult(&n.NetConf); err != nil { return nil, "", fmt.Errorf("could not parse prevResult: %v", err) } - n.RawPrevResult = nil - n.PrevResult, err = current.NewResultFromResult(res) + + result, err = current.NewResultFromResult(n.PrevResult) if err != nil { return nil, "", fmt.Errorf("could not convert result to current version: %v", err) } } if n.Master == "" { - if n.PrevResult == nil { + if result == nil { return nil, "", fmt.Errorf(`"master" field is required. It specifies the host interface name to virtualize`) } - if len(n.PrevResult.Interfaces) == 1 && n.PrevResult.Interfaces[0].Name != "" { - n.Master = n.PrevResult.Interfaces[0].Name + if len(result.Interfaces) == 1 && result.Interfaces[0].Name != "" { + n.Master = result.Interfaces[0].Name } else { return nil, "", fmt.Errorf("chained master failure. PrevResult lacks a single named interface") } @@ -97,6 +96,19 @@ func modeFromString(s string) (netlink.IPVlanMode, error) { } } +func modeToString(mode netlink.IPVlanMode) (string, error) { + switch mode { + case netlink.IPVLAN_MODE_L2: + return "l2", nil + case netlink.IPVLAN_MODE_L3: + return "l3", nil + case netlink.IPVLAN_MODE_L3S: + return "l3s", nil + default: + return "", fmt.Errorf("unknown ipvlan mode: %q", mode) + } +} + func createIpvlan(conf *NetConf, ifName string, netns ns.NetNS) (*current.Interface, error) { ipvlan := ¤t.Interface{} @@ -156,7 +168,7 @@ func createIpvlan(conf *NetConf, ifName string, netns ns.NetNS) (*current.Interf } func cmdAdd(args *skel.CmdArgs) error { - n, cniVersion, err := loadConf(args.StdinData) + n, cniVersion, err := loadConf(args.StdinData, false) if err != nil { return err } @@ -175,9 +187,17 @@ func cmdAdd(args *skel.CmdArgs) error { var result *current.Result // Configure iface from PrevResult if we have IPs and an IPAM // block has not been configured - if n.IPAM.Type == "" && n.PrevResult != nil && len(n.PrevResult.IPs) > 0 { - result = n.PrevResult - } else { + haveResult := false + if n.IPAM.Type == "" && n.PrevResult != nil { + result, err = current.NewResultFromResult(n.PrevResult) + if err != nil { + return err + } + if len(result.IPs) > 0 { + haveResult = true + } + } + if !haveResult { // run the IPAM plugin and get back the config to apply r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData) if err != nil { @@ -213,7 +233,7 @@ func cmdAdd(args *skel.CmdArgs) error { } func cmdDel(args *skel.CmdArgs) error { - n, _, err := loadConf(args.StdinData) + n, _, err := loadConf(args.StdinData, false) if err != nil { return err } @@ -245,11 +265,130 @@ func cmdDel(args *skel.CmdArgs) error { } func main() { - // TODO: implement plugin version - skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.All, "TODO") + skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("ipvlan")) } -func cmdGet(args *skel.CmdArgs) error { - // TODO: implement - return fmt.Errorf("not implemented") +func cmdCheck(args *skel.CmdArgs) error { + + n, _, err := loadConf(args.StdinData, true) + if err != nil { + return err + } + netns, err := ns.GetNS(args.Netns) + if err != nil { + return fmt.Errorf("failed to open netns %q: %v", args.Netns, err) + } + defer netns.Close() + + if n.IPAM.Type != "" { + // run the IPAM plugin and get back the config to apply + err = ipam.ExecCheck(n.IPAM.Type, args.StdinData) + if err != nil { + return err + } + } + + // Parse previous result. + if n.NetConf.RawPrevResult == nil { + return fmt.Errorf("Required prevResult missing") + } + + if err := version.ParsePrevResult(&n.NetConf); err != nil { + return err + } + + result, err := current.NewResultFromResult(n.PrevResult) + if err != nil { + return err + } + + var contMap current.Interface + // Find interfaces for names whe know, ipvlan inside container + for _, intf := range result.Interfaces { + if args.IfName == intf.Name { + if args.Netns == intf.Sandbox { + contMap = *intf + continue + } + } + } + + // The namespace must be the same as what was configured + if args.Netns != contMap.Sandbox { + return fmt.Errorf("Sandbox in prevResult %s doesn't match configured netns: %s", + contMap.Sandbox, args.Netns) + } + + m, err := netlink.LinkByName(n.Master) + if err != nil { + return fmt.Errorf("failed to lookup master %q: %v", n.Master, err) + } + + // Check prevResults for ips, routes and dns against values found in the container + if err := netns.Do(func(_ ns.NetNS) error { + + // Check interface against values found in the container + err := validateCniContainerInterface(contMap, m.Attrs().Index, n.Mode) + if err != nil { + return err + } + + err = ip.ValidateExpectedInterfaceIPs(args.IfName, result.IPs) + if err != nil { + return err + } + + err = ip.ValidateExpectedRoute(result.Routes) + if err != nil { + return err + } + return nil + }); err != nil { + return err + } + + return nil +} + +func validateCniContainerInterface(intf current.Interface, masterIndex int, modeExpected string) error { + + var link netlink.Link + var err error + + if intf.Name == "" { + return fmt.Errorf("Container interface name missing in prevResult: %v", intf.Name) + } + link, err = netlink.LinkByName(intf.Name) + if err != nil { + return fmt.Errorf("Container Interface name in prevResult: %s not found", intf.Name) + } + if intf.Sandbox == "" { + return fmt.Errorf("Error: Container interface %s should not be in host namespace", link.Attrs().Name) + } + + ipv, isIPVlan := link.(*netlink.IPVlan) + if !isIPVlan { + return fmt.Errorf("Error: Container interface %s not of type ipvlan", link.Attrs().Name) + } + + mode, err := modeFromString(modeExpected) + if ipv.Mode != mode { + currString, err := modeToString(ipv.Mode) + if err != nil { + return err + } + confString, err := modeToString(mode) + if err != nil { + return err + } + return fmt.Errorf("Container IPVlan mode %s does not match expected value: %s", currString, confString) + } + + if intf.Mac != "" { + if intf.Mac != link.Attrs().HardwareAddr.String() { + return fmt.Errorf("Interface %s Mac %s doesn't match container Mac: %s", intf.Name, intf.Mac, link.Attrs().HardwareAddr) + } + } + + return nil } diff --git a/plugins/main/ipvlan/ipvlan_test.go b/plugins/main/ipvlan/ipvlan_test.go index 9fb110ad..64503d2e 100644 --- a/plugins/main/ipvlan/ipvlan_test.go +++ b/plugins/main/ipvlan/ipvlan_test.go @@ -15,6 +15,7 @@ package main import ( + "encoding/json" "fmt" "net" "syscall" @@ -27,12 +28,71 @@ import ( "github.com/vishvananda/netlink" + "github.com/containernetworking/plugins/plugins/ipam/host-local/backend/allocator" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) const MASTER_NAME = "eth0" +type Net struct { + Name string `json:"name"` + CNIVersion string `json:"cniVersion"` + Type string `json:"type,omitempty"` + Master string `json:"master"` + Mode string `json:"mode"` + IPAM *allocator.IPAMConfig `json:"ipam"` + DNS types.DNS `json:"dns"` + RawPrevResult map[string]interface{} `json:"prevResult,omitempty"` + PrevResult current.Result `json:"-"` +} + +func buildOneConfig(netName string, cniVersion string, master string, orig *Net, prevResult types.Result) (*Net, error) { + var err error + + inject := map[string]interface{}{ + "name": netName, + "cniVersion": cniVersion, + } + // Add previous plugin result + if prevResult != nil { + inject["prevResult"] = prevResult + } + if orig.IPAM == nil { + inject["master"] = master + } + + // Ensure every config uses the same name and version + config := make(map[string]interface{}) + + confBytes, err := json.Marshal(orig) + if err != nil { + return nil, err + } + + err = json.Unmarshal(confBytes, &config) + if err != nil { + return nil, fmt.Errorf("unmarshal existing network bytes: %s", err) + } + + for key, value := range inject { + config[key] = value + } + + newBytes, err := json.Marshal(config) + if err != nil { + return nil, err + } + + conf := &Net{} + if err := json.Unmarshal(newBytes, &conf); err != nil { + return nil, fmt.Errorf("error parsing configuration: %s", err) + } + + return conf, nil + +} + func ipvlanAddDelTest(conf, IFNAME string, originalNS ns.NetNS) { targetNs, err := testutils.NewNS() Expect(err).NotTo(HaveOccurred()) @@ -106,6 +166,109 @@ func ipvlanAddDelTest(conf, IFNAME string, originalNS ns.NetNS) { Expect(err).NotTo(HaveOccurred()) } +func ipvlanAddCheckDelTest(conf string, netName string, IFNAME string, originalNS ns.NetNS) { + targetNs, err := testutils.NewNS() + Expect(err).NotTo(HaveOccurred()) + defer targetNs.Close() + + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: targetNs.Path(), + IfName: IFNAME, + StdinData: []byte(conf), + } + + var result *current.Result + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + r, _, err := testutils.CmdAddWithArgs(args, func() error { + return cmdAdd(args) + }) + Expect(err).NotTo(HaveOccurred()) + + result, err = current.GetResult(r) + Expect(err).NotTo(HaveOccurred()) + + Expect(len(result.Interfaces)).To(Equal(1)) + Expect(result.Interfaces[0].Name).To(Equal(IFNAME)) + Expect(len(result.IPs)).To(Equal(1)) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + // Make sure ipvlan link exists in the target namespace + err = targetNs.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + link, err := netlink.LinkByName(IFNAME) + Expect(err).NotTo(HaveOccurred()) + Expect(link.Attrs().Name).To(Equal(IFNAME)) + + hwaddr, err := net.ParseMAC(result.Interfaces[0].Mac) + Expect(err).NotTo(HaveOccurred()) + Expect(link.Attrs().HardwareAddr).To(Equal(hwaddr)) + + addrs, err := netlink.AddrList(link, syscall.AF_INET) + Expect(err).NotTo(HaveOccurred()) + Expect(len(addrs)).To(Equal(1)) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + n := &Net{} + err = json.Unmarshal([]byte(conf), &n) + Expect(err).NotTo(HaveOccurred()) + + if n.IPAM != nil { + n.IPAM, _, err = allocator.LoadIPAMConfig([]byte(conf), "") + Expect(err).NotTo(HaveOccurred()) + } + + cniVersion := "0.4.0" + newConf, err := buildOneConfig(netName, cniVersion, MASTER_NAME, n, result) + Expect(err).NotTo(HaveOccurred()) + + confString, err := json.Marshal(newConf) + Expect(err).NotTo(HaveOccurred()) + + args.StdinData = confString + + // CNI Check on macvlan in the target namespace + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + err := testutils.CmdCheckWithArgs(args, func() error { + return cmdCheck(args) + }) + Expect(err).NotTo(HaveOccurred()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + err = testutils.CmdDelWithArgs(args, func() error { + return cmdDel(args) + }) + Expect(err).NotTo(HaveOccurred()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + // Make sure ipvlan link has been deleted + err = targetNs.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + link, err := netlink.LinkByName(IFNAME) + Expect(err).To(HaveOccurred()) + Expect(link).To(BeNil()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) +} + var _ = Describe("ipvlan Operations", func() { var originalNS ns.NetNS @@ -256,4 +419,49 @@ var _ = Describe("ipvlan Operations", func() { }) Expect(err).NotTo(HaveOccurred()) }) + + It("configures and deconfigures a cniVersion 0.4.0 iplvan link with ADD/CHECK/DEL", func() { + const IFNAME = "ipvl0" + + conf := fmt.Sprintf(`{ + "cniVersion": "0.4.0", + "name": "ipvlanTest1", + "type": "ipvlan", + "master": "%s", + "ipam": { + "type": "host-local", + "subnet": "10.1.2.0/24" + } +}`, MASTER_NAME) + + ipvlanAddCheckDelTest(conf, "ipvlanTest1", IFNAME, originalNS) + }) + + It("configures and deconfigures a cniVersion 0.4.0 iplvan link with ADD/CHECK/DEL when chained", func() { + const IFNAME = "ipvl0" + + conf := fmt.Sprintf(`{ + "cniVersion": "0.4.0", + "name": "ipvlanTest2", + "type": "ipvlan", + "prevResult": { + "interfaces": [ + { + "name": "%s" + } + ], + "ips": [ + { + "version": "4", + "address": "10.1.2.2/24", + "gateway": "10.1.2.1", + "interface": 0 + } + ], + "routes": [] + } + }`, MASTER_NAME) + + ipvlanAddCheckDelTest(conf, "ipvlanTest2", IFNAME, originalNS) + }) }) diff --git a/plugins/main/loopback/loopback.go b/plugins/main/loopback/loopback.go index 454c905f..db5e9964 100644 --- a/plugins/main/loopback/loopback.go +++ b/plugins/main/loopback/loopback.go @@ -15,13 +15,14 @@ package main import ( - "fmt" + "github.com/vishvananda/netlink" "github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/cni/pkg/version" + "github.com/containernetworking/plugins/pkg/ns" - "github.com/vishvananda/netlink" + bv "github.com/containernetworking/plugins/pkg/utils/buildversion" ) func cmdAdd(args *skel.CmdArgs) error { @@ -73,11 +74,10 @@ func cmdDel(args *skel.CmdArgs) error { } func main() { - // TODO: implement plugin version - skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.All, "TODO") + skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("loopback")) } -func cmdGet(args *skel.CmdArgs) error { +func cmdCheck(args *skel.CmdArgs) error { // TODO: implement - return fmt.Errorf("not implemented") + return nil } diff --git a/plugins/main/macvlan/macvlan.go b/plugins/main/macvlan/macvlan.go index dba60978..a0521a84 100644 --- a/plugins/main/macvlan/macvlan.go +++ b/plugins/main/macvlan/macvlan.go @@ -21,16 +21,19 @@ import ( "net" "runtime" + "github.com/j-keck/arping" + "github.com/vishvananda/netlink" + "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/ip" "github.com/containernetworking/plugins/pkg/ipam" "github.com/containernetworking/plugins/pkg/ns" + bv "github.com/containernetworking/plugins/pkg/utils/buildversion" "github.com/containernetworking/plugins/pkg/utils/sysctl" - "github.com/j-keck/arping" - "github.com/vishvananda/netlink" ) const ( @@ -77,6 +80,21 @@ func modeFromString(s string) (netlink.MacvlanMode, error) { } } +func modeToString(mode netlink.MacvlanMode) (string, error) { + switch mode { + case netlink.MACVLAN_MODE_BRIDGE: + return "bridge", nil + case netlink.MACVLAN_MODE_PRIVATE: + return "private", nil + case netlink.MACVLAN_MODE_VEPA: + return "vepa", nil + case netlink.MACVLAN_MODE_PASSTHRU: + return "passthru", nil + default: + return "", fmt.Errorf("unknown macvlan mode: %q", mode) + } +} + func createMacvlan(conf *NetConf, ifName string, netns ns.NetNS) (*current.Interface, error) { macvlan := ¤t.Interface{} @@ -255,11 +273,128 @@ func cmdDel(args *skel.CmdArgs) error { } func main() { - // TODO: implement plugin version - skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.All, "TODO") + skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("macvlan")) } -func cmdGet(args *skel.CmdArgs) error { - // TODO: implement - return fmt.Errorf("not implemented") +func cmdCheck(args *skel.CmdArgs) error { + + n, _, err := loadConf(args.StdinData) + if err != nil { + return err + } + netns, err := ns.GetNS(args.Netns) + if err != nil { + return fmt.Errorf("failed to open netns %q: %v", args.Netns, err) + } + defer netns.Close() + + // run the IPAM plugin and get back the config to apply + err = ipam.ExecCheck(n.IPAM.Type, args.StdinData) + if err != nil { + return err + } + + // Parse previous result. + if n.NetConf.RawPrevResult == nil { + return fmt.Errorf("Required prevResult missing") + } + + if err := version.ParsePrevResult(&n.NetConf); err != nil { + return err + } + + result, err := current.NewResultFromResult(n.PrevResult) + if err != nil { + return err + } + + var contMap current.Interface + // Find interfaces for names whe know, macvlan device name inside container + for _, intf := range result.Interfaces { + if args.IfName == intf.Name { + if args.Netns == intf.Sandbox { + contMap = *intf + continue + } + } + } + + // The namespace must be the same as what was configured + if args.Netns != contMap.Sandbox { + return fmt.Errorf("Sandbox in prevResult %s doesn't match configured netns: %s", + contMap.Sandbox, args.Netns) + } + + m, err := netlink.LinkByName(n.Master) + if err != nil { + return fmt.Errorf("failed to lookup master %q: %v", n.Master, err) + } + + // Check prevResults for ips, routes and dns against values found in the container + if err := netns.Do(func(_ ns.NetNS) error { + + // Check interface against values found in the container + err := validateCniContainerInterface(contMap, m.Attrs().Index, n.Mode) + if err != nil { + return err + } + + err = ip.ValidateExpectedInterfaceIPs(args.IfName, result.IPs) + if err != nil { + return err + } + + err = ip.ValidateExpectedRoute(result.Routes) + if err != nil { + return err + } + return nil + }); err != nil { + return err + } + + return nil +} + +func validateCniContainerInterface(intf current.Interface, parentIndex int, modeExpected string) error { + + var link netlink.Link + var err error + + if intf.Name == "" { + return fmt.Errorf("Container interface name missing in prevResult: %v", intf.Name) + } + link, err = netlink.LinkByName(intf.Name) + if err != nil { + return fmt.Errorf("Container Interface name in prevResult: %s not found", intf.Name) + } + if intf.Sandbox == "" { + return fmt.Errorf("Error: Container interface %s should not be in host namespace", link.Attrs().Name) + } + + macv, isMacvlan := link.(*netlink.Macvlan) + if !isMacvlan { + return fmt.Errorf("Error: Container interface %s not of type macvlan", link.Attrs().Name) + } + + mode, err := modeFromString(modeExpected) + if macv.Mode != mode { + currString, err := modeToString(macv.Mode) + if err != nil { + return err + } + confString, err := modeToString(mode) + if err != nil { + return err + } + return fmt.Errorf("Container macvlan mode %s does not match expected value: %s", currString, confString) + } + + if intf.Mac != "" { + if intf.Mac != link.Attrs().HardwareAddr.String() { + return fmt.Errorf("Interface %s Mac %s doesn't match container Mac: %s", intf.Name, intf.Mac, link.Attrs().HardwareAddr) + } + } + + return nil } diff --git a/plugins/main/macvlan/macvlan_test.go b/plugins/main/macvlan/macvlan_test.go index 0f7fef9f..d76540ee 100644 --- a/plugins/main/macvlan/macvlan_test.go +++ b/plugins/main/macvlan/macvlan_test.go @@ -15,6 +15,7 @@ package main import ( + "encoding/json" "fmt" "net" "syscall" @@ -27,12 +28,73 @@ import ( "github.com/vishvananda/netlink" + "github.com/containernetworking/plugins/plugins/ipam/host-local/backend/allocator" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) const MASTER_NAME = "eth0" +type Net struct { + Name string `json:"name"` + CNIVersion string `json:"cniVersion"` + Type string `json:"type,omitempty"` + Master string `json:"master"` + Mode string `json:"mode"` + IPAM *allocator.IPAMConfig `json:"ipam"` + //RuntimeConfig struct { // The capability arg + // IPRanges []RangeSet `json:"ipRanges,omitempty"` + //} `json:"runtimeConfig,omitempty"` + //Args *struct { + // A *IPAMArgs `json:"cni"` + DNS types.DNS `json:"dns"` + RawPrevResult map[string]interface{} `json:"prevResult,omitempty"` + PrevResult current.Result `json:"-"` +} + +func buildOneConfig(netName string, cniVersion string, orig *Net, prevResult types.Result) (*Net, error) { + var err error + + inject := map[string]interface{}{ + "name": netName, + "cniVersion": cniVersion, + } + // Add previous plugin result + if prevResult != nil { + inject["prevResult"] = prevResult + } + + // Ensure every config uses the same name and version + config := make(map[string]interface{}) + + confBytes, err := json.Marshal(orig) + if err != nil { + return nil, err + } + + err = json.Unmarshal(confBytes, &config) + if err != nil { + return nil, fmt.Errorf("unmarshal existing network bytes: %s", err) + } + + for key, value := range inject { + config[key] = value + } + + newBytes, err := json.Marshal(config) + if err != nil { + return nil, err + } + + conf := &Net{} + if err := json.Unmarshal(newBytes, &conf); err != nil { + return nil, fmt.Errorf("error parsing configuration: %s", err) + } + + return conf, nil + +} + var _ = Describe("macvlan Operations", func() { var originalNS ns.NetNS @@ -223,4 +285,118 @@ var _ = Describe("macvlan Operations", func() { Expect(err).NotTo(HaveOccurred()) }) + + It("configures and deconfigures a cniVersion 0.4.0 macvlan link with ADD/DEL", func() { + const IFNAME = "macvl0" + + conf := fmt.Sprintf(`{ + "cniVersion": "0.4.0", + "name": "macvlanTestv4", + "type": "macvlan", + "master": "%s", + "ipam": { + "type": "host-local", + "ranges": [[ {"subnet": "10.1.2.0/24", "gateway": "10.1.2.1"} ]] + } +}`, MASTER_NAME) + + targetNs, err := testutils.NewNS() + Expect(err).NotTo(HaveOccurred()) + defer targetNs.Close() + + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: targetNs.Path(), + IfName: IFNAME, + StdinData: []byte(conf), + } + + var result *current.Result + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + r, _, err := testutils.CmdAddWithArgs(args, func() error { + return cmdAdd(args) + }) + Expect(err).NotTo(HaveOccurred()) + + result, err = current.GetResult(r) + Expect(err).NotTo(HaveOccurred()) + + Expect(len(result.Interfaces)).To(Equal(1)) + Expect(result.Interfaces[0].Name).To(Equal(IFNAME)) + Expect(len(result.IPs)).To(Equal(1)) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + // Make sure macvlan link exists in the target namespace + err = targetNs.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + link, err := netlink.LinkByName(IFNAME) + Expect(err).NotTo(HaveOccurred()) + Expect(link.Attrs().Name).To(Equal(IFNAME)) + + hwaddr, err := net.ParseMAC(result.Interfaces[0].Mac) + Expect(err).NotTo(HaveOccurred()) + Expect(link.Attrs().HardwareAddr).To(Equal(hwaddr)) + + addrs, err := netlink.AddrList(link, syscall.AF_INET) + Expect(err).NotTo(HaveOccurred()) + Expect(len(addrs)).To(Equal(1)) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + n := &Net{} + err = json.Unmarshal([]byte(conf), &n) + Expect(err).NotTo(HaveOccurred()) + + n.IPAM, _, err = allocator.LoadIPAMConfig([]byte(conf), "") + Expect(err).NotTo(HaveOccurred()) + + cniVersion := "0.4.0" + newConf, err := buildOneConfig("macvlanTestv4", cniVersion, n, result) + Expect(err).NotTo(HaveOccurred()) + + confString, err := json.Marshal(newConf) + Expect(err).NotTo(HaveOccurred()) + + args.StdinData = confString + + // CNI Check on macvlan in the target namespace + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + err := testutils.CmdCheckWithArgs(args, func() error { + return cmdCheck(args) + }) + Expect(err).NotTo(HaveOccurred()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + err := testutils.CmdDelWithArgs(args, func() error { + return cmdDel(args) + }) + Expect(err).NotTo(HaveOccurred()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + // Make sure macvlan link has been deleted + err = targetNs.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + link, err := netlink.LinkByName(IFNAME) + Expect(err).To(HaveOccurred()) + Expect(link).To(BeNil()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + }) }) diff --git a/plugins/main/ptp/ptp.go b/plugins/main/ptp/ptp.go index a1b6183d..1f6def22 100644 --- a/plugins/main/ptp/ptp.go +++ b/plugins/main/ptp/ptp.go @@ -22,16 +22,19 @@ import ( "os" "runtime" + "github.com/j-keck/arping" + "github.com/vishvananda/netlink" + "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/ip" "github.com/containernetworking/plugins/pkg/ipam" "github.com/containernetworking/plugins/pkg/ns" "github.com/containernetworking/plugins/pkg/utils" - "github.com/j-keck/arping" - "github.com/vishvananda/netlink" + bv "github.com/containernetworking/plugins/pkg/utils/buildversion" ) func init() { @@ -110,7 +113,7 @@ func setupContainerVeth(netns ns.NetNS, ifName string, mtu int, pr *current.Resu } for _, r := range []netlink.Route{ - netlink.Route{ + { LinkIndex: contVeth.Index, Dst: &net.IPNet{ IP: ipc.Gateway, @@ -119,7 +122,7 @@ func setupContainerVeth(netns ns.NetNS, ifName string, mtu int, pr *current.Resu Scope: netlink.SCOPE_LINK, Src: ipc.Address.IP, }, - netlink.Route{ + { LinkIndex: contVeth.Index, Dst: &net.IPNet{ IP: ipc.Address.IP.Mask(ipc.Address.Mask), @@ -285,11 +288,108 @@ func cmdDel(args *skel.CmdArgs) error { } func main() { - // TODO: implement plugin version - skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.All, "TODO") + skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("ptp")) } -func cmdGet(args *skel.CmdArgs) error { - // TODO: implement - return fmt.Errorf("not implemented") +func cmdCheck(args *skel.CmdArgs) error { + conf := NetConf{} + if err := json.Unmarshal(args.StdinData, &conf); err != nil { + return fmt.Errorf("failed to load netconf: %v", err) + } + + netns, err := ns.GetNS(args.Netns) + if err != nil { + return fmt.Errorf("failed to open netns %q: %v", args.Netns, err) + } + defer netns.Close() + + // run the IPAM plugin and get back the config to apply + err = ipam.ExecCheck(conf.IPAM.Type, args.StdinData) + if err != nil { + return err + } + if conf.NetConf.RawPrevResult == nil { + return fmt.Errorf("ptp: Required prevResult missing") + } + if err := version.ParsePrevResult(&conf.NetConf); err != nil { + return err + } + // Convert whatever the IPAM result was into the current Result type + result, err := current.NewResultFromResult(conf.PrevResult) + if err != nil { + return err + } + + var contMap current.Interface + // Find interfaces for name whe know, that of host-device inside container + for _, intf := range result.Interfaces { + if args.IfName == intf.Name { + if args.Netns == intf.Sandbox { + contMap = *intf + continue + } + } + } + + // The namespace must be the same as what was configured + if args.Netns != contMap.Sandbox { + return fmt.Errorf("Sandbox in prevResult %s doesn't match configured netns: %s", + contMap.Sandbox, args.Netns) + } + + // + // Check prevResults for ips, routes and dns against values found in the container + if err := netns.Do(func(_ ns.NetNS) error { + + // Check interface against values found in the container + err := validateCniContainerInterface(contMap) + if err != nil { + return err + } + + err = ip.ValidateExpectedInterfaceIPs(args.IfName, result.IPs) + if err != nil { + return err + } + + err = ip.ValidateExpectedRoute(result.Routes) + if err != nil { + return err + } + return nil + }); err != nil { + return err + } + + return nil +} + +func validateCniContainerInterface(intf current.Interface) error { + + var link netlink.Link + var err error + + if intf.Name == "" { + return fmt.Errorf("Container interface name missing in prevResult: %v", intf.Name) + } + link, err = netlink.LinkByName(intf.Name) + if err != nil { + return fmt.Errorf("ptp: Container Interface name in prevResult: %s not found", intf.Name) + } + if intf.Sandbox == "" { + return fmt.Errorf("ptp: Error: Container interface %s should not be in host namespace", link.Attrs().Name) + } + + _, isVeth := link.(*netlink.Veth) + if !isVeth { + return fmt.Errorf("Error: Container interface %s not of type veth/p2p", link.Attrs().Name) + } + + if intf.Mac != "" { + if intf.Mac != link.Attrs().HardwareAddr.String() { + return fmt.Errorf("ptp: Interface %s Mac %s doesn't match container Mac: %s", intf.Name, intf.Mac, link.Attrs().HardwareAddr) + } + } + + return nil } diff --git a/plugins/main/ptp/ptp_test.go b/plugins/main/ptp/ptp_test.go index b5d16f89..6efdc098 100644 --- a/plugins/main/ptp/ptp_test.go +++ b/plugins/main/ptp/ptp_test.go @@ -15,6 +15,7 @@ package main import ( + "encoding/json" "fmt" "github.com/containernetworking/cni/pkg/skel" @@ -25,10 +26,66 @@ import ( "github.com/vishvananda/netlink" + "github.com/containernetworking/plugins/plugins/ipam/host-local/backend/allocator" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) +type Net struct { + Name string `json:"name"` + CNIVersion string `json:"cniVersion"` + Type string `json:"type,omitempty"` + IPMasq bool `json:"ipMasq"` + MTU int `json:"mtu"` + IPAM *allocator.IPAMConfig `json:"ipam"` + DNS types.DNS `json:"dns"` + RawPrevResult map[string]interface{} `json:"prevResult,omitempty"` + PrevResult current.Result `json:"-"` +} + +func buildOneConfig(netName string, cniVersion string, orig *Net, prevResult types.Result) (*Net, error) { + var err error + + inject := map[string]interface{}{ + "name": netName, + "cniVersion": cniVersion, + } + // Add previous plugin result + if prevResult != nil { + inject["prevResult"] = prevResult + } + + // Ensure every config uses the same name and version + config := make(map[string]interface{}) + + confBytes, err := json.Marshal(orig) + if err != nil { + return nil, err + } + + err = json.Unmarshal(confBytes, &config) + if err != nil { + return nil, fmt.Errorf("unmarshal existing network bytes: %s", err) + } + + for key, value := range inject { + config[key] = value + } + + newBytes, err := json.Marshal(config) + if err != nil { + return nil, err + } + + conf := &Net{} + if err := json.Unmarshal(newBytes, &conf); err != nil { + return nil, fmt.Errorf("error parsing configuration: %s", err) + } + + return conf, nil + +} + var _ = Describe("ptp Operations", func() { var originalNS ns.NetNS @@ -142,6 +199,133 @@ var _ = Describe("ptp Operations", func() { Expect(err).NotTo(HaveOccurred()) } + doTestv4 := func(conf string, netName string, numIPs int) { + const IFNAME = "ptp0" + + targetNs, err := testutils.NewNS() + Expect(err).NotTo(HaveOccurred()) + defer targetNs.Close() + + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: targetNs.Path(), + IfName: IFNAME, + StdinData: []byte(conf), + } + + var resI types.Result + var res *current.Result + + // Execute the plugin with the ADD command, creating the veth endpoints + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + resI, _, err = testutils.CmdAddWithArgs(args, func() error { + return cmdAdd(args) + }) + Expect(err).NotTo(HaveOccurred()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + res, err = current.NewResultFromResult(resI) + Expect(err).NotTo(HaveOccurred()) + + // Make sure ptp link exists in the target namespace + // Then, ping the gateway + seenIPs := 0 + + wantMac := "" + err = targetNs.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + link, err := netlink.LinkByName(IFNAME) + Expect(err).NotTo(HaveOccurred()) + wantMac = link.Attrs().HardwareAddr.String() + + for _, ipc := range res.IPs { + if *ipc.Interface != 1 { + continue + } + seenIPs += 1 + saddr := ipc.Address.IP.String() + daddr := ipc.Gateway.String() + fmt.Fprintln(GinkgoWriter, "ping", saddr, "->", daddr) + + if err := testutils.Ping(saddr, daddr, (ipc.Version == "6"), 30); err != nil { + return fmt.Errorf("ping %s -> %s failed: %s", saddr, daddr, err) + } + } + + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + Expect(seenIPs).To(Equal(numIPs)) + + // make sure the interfaces are correct + Expect(res.Interfaces).To(HaveLen(2)) + + Expect(res.Interfaces[0].Name).To(HavePrefix("veth")) + Expect(res.Interfaces[0].Mac).To(HaveLen(17)) + Expect(res.Interfaces[0].Sandbox).To(BeEmpty()) + + Expect(res.Interfaces[1].Name).To(Equal(IFNAME)) + Expect(res.Interfaces[1].Mac).To(Equal(wantMac)) + Expect(res.Interfaces[1].Sandbox).To(Equal(targetNs.Path())) + + // call CmdCheck + n := &Net{} + err = json.Unmarshal([]byte(conf), &n) + Expect(err).NotTo(HaveOccurred()) + + n.IPAM, _, err = allocator.LoadIPAMConfig([]byte(conf), "") + Expect(err).NotTo(HaveOccurred()) + + cniVersion := "0.4.0" + newConf, err := buildOneConfig(netName, cniVersion, n, res) + Expect(err).NotTo(HaveOccurred()) + + confString, err := json.Marshal(newConf) + Expect(err).NotTo(HaveOccurred()) + + args.StdinData = confString + + // CNI Check host-device in the target namespace + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + var err error + err = testutils.CmdCheckWithArgs(args, func() error { return cmdCheck(args) }) + return err + }) + Expect(err).NotTo(HaveOccurred()) + + args.StdinData = []byte(conf) + + // Call the plugins with the DEL command, deleting the veth endpoints + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + err := testutils.CmdDelWithArgs(args, func() error { + return cmdDel(args) + }) + Expect(err).NotTo(HaveOccurred()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + // Make sure ptp link has been deleted + err = targetNs.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + link, err := netlink.LinkByName(IFNAME) + Expect(err).To(HaveOccurred()) + Expect(link).To(BeNil()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + } + It("configures and deconfigures a ptp link with ADD/DEL", func() { conf := `{ "cniVersion": "0.3.1", @@ -215,4 +399,39 @@ var _ = Describe("ptp Operations", func() { }) Expect(err).NotTo(HaveOccurred()) }) + + It("configures and deconfigures a CNI V4 ptp link with ADD/DEL", func() { + conf := `{ + "cniVersion": "0.4.0", + "name": "ptpNetv4", + "type": "ptp", + "ipMasq": true, + "mtu": 5000, + "ipam": { + "type": "host-local", + "subnet": "10.1.2.0/24" + } +}` + + doTestv4(conf, "ptpNetv4", 1) + }) + + It("configures and deconfigures a CNI V4 dual-stack ptp link with ADD/DEL", func() { + conf := `{ + "cniVersion": "0.4.0", + "name": "ptpNetv4ds", + "type": "ptp", + "ipMasq": true, + "mtu": 5000, + "ipam": { + "type": "host-local", + "ranges": [ + [{ "subnet": "10.1.2.0/24"}], + [{ "subnet": "2001:db8:1::0/66"}] + ] + } +}` + + doTestv4(conf, "ptpNetv4ds", 2) + }) }) diff --git a/plugins/main/vlan/vlan.go b/plugins/main/vlan/vlan.go index c34a752e..6e69221b 100644 --- a/plugins/main/vlan/vlan.go +++ b/plugins/main/vlan/vlan.go @@ -20,14 +20,17 @@ import ( "fmt" "runtime" + "github.com/vishvananda/netlink" + "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/ip" "github.com/containernetworking/plugins/pkg/ipam" "github.com/containernetworking/plugins/pkg/ns" - "github.com/vishvananda/netlink" + bv "github.com/containernetworking/plugins/pkg/utils/buildversion" ) type NetConf struct { @@ -192,11 +195,130 @@ func cmdDel(args *skel.CmdArgs) error { } func main() { - // TODO: implement plugin version - skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.All, "TODO") + skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("vlan")) } -func cmdGet(args *skel.CmdArgs) error { - // TODO: implement - return fmt.Errorf("not implemented") +func cmdCheck(args *skel.CmdArgs) error { + conf := NetConf{} + if err := json.Unmarshal(args.StdinData, &conf); err != nil { + return fmt.Errorf("failed to load netconf: %v", err) + } + + netns, err := ns.GetNS(args.Netns) + if err != nil { + return fmt.Errorf("failed to open netns %q: %v", args.Netns, err) + } + defer netns.Close() + + // run the IPAM plugin and get back the config to apply + err = ipam.ExecCheck(conf.IPAM.Type, args.StdinData) + if err != nil { + return err + } + if conf.NetConf.RawPrevResult == nil { + return fmt.Errorf("ptp: Required prevResult missing") + } + if err := version.ParsePrevResult(&conf.NetConf); err != nil { + return err + } + // Convert whatever the IPAM result was into the current Result type + result, err := current.NewResultFromResult(conf.PrevResult) + if err != nil { + return err + } + + var contMap current.Interface + // Find interfaces for name whe know, that of host-device inside container + for _, intf := range result.Interfaces { + if args.IfName == intf.Name { + if args.Netns == intf.Sandbox { + contMap = *intf + continue + } + } + } + + // The namespace must be the same as what was configured + if args.Netns != contMap.Sandbox { + return fmt.Errorf("Sandbox in prevResult %s doesn't match configured netns: %s", + contMap.Sandbox, args.Netns) + } + + m, err := netlink.LinkByName(conf.Master) + if err != nil { + return fmt.Errorf("failed to lookup master %q: %v", conf.Master, err) + } + + // + // Check prevResults for ips, routes and dns against values found in the container + if err := netns.Do(func(_ ns.NetNS) error { + + // Check interface against values found in the container + err := validateCniContainerInterface(contMap, m.Attrs().Index, conf.VlanId, conf.MTU) + if err != nil { + return err + } + + err = ip.ValidateExpectedInterfaceIPs(args.IfName, result.IPs) + if err != nil { + return err + } + + err = ip.ValidateExpectedRoute(result.Routes) + if err != nil { + return err + } + return nil + }); err != nil { + return err + } + + return nil +} + +func validateCniContainerInterface(intf current.Interface, masterIndex int, vlanId int, mtu int) error { + + var link netlink.Link + var err error + + if intf.Name == "" { + return fmt.Errorf("Container interface name missing in prevResult: %v", intf.Name) + } + link, err = netlink.LinkByName(intf.Name) + if err != nil { + return fmt.Errorf("ptp: Container Interface name in prevResult: %s not found", intf.Name) + } + if intf.Sandbox == "" { + return fmt.Errorf("ptp: Error: Container interface %s should not be in host namespace", link.Attrs().Name) + } + + vlan, isVlan := link.(*netlink.Vlan) + if !isVlan { + return fmt.Errorf("Error: Container interface %s not of type vlan", link.Attrs().Name) + } + + // TODO This works when unit testing via cnitool; fails with ./test.sh + //if masterIndex != vlan.Attrs().ParentIndex { + // return fmt.Errorf("Container vlan Master %d does not match expected value: %d", vlan.Attrs().ParentIndex, masterIndex) + //} + + if intf.Mac != "" { + if intf.Mac != link.Attrs().HardwareAddr.String() { + return fmt.Errorf("vlan: Interface %s Mac %s doesn't match container Mac: %s", intf.Name, intf.Mac, link.Attrs().HardwareAddr) + } + } + + if vlanId != vlan.VlanId { + return fmt.Errorf("Error: Tuning link %s configured promisc is %v, current value is %d", + intf.Name, vlanId, vlan.VlanId) + } + + if mtu != 0 { + if mtu != link.Attrs().MTU { + return fmt.Errorf("Error: Tuning configured MTU of %s is %d, current value is %d", + intf.Name, mtu, link.Attrs().MTU) + } + } + + return nil } diff --git a/plugins/main/vlan/vlan_test.go b/plugins/main/vlan/vlan_test.go index d2cdf821..e845a84c 100644 --- a/plugins/main/vlan/vlan_test.go +++ b/plugins/main/vlan/vlan_test.go @@ -15,6 +15,7 @@ package main import ( + "encoding/json" "fmt" "net" "syscall" @@ -27,12 +28,69 @@ import ( "github.com/vishvananda/netlink" + "github.com/containernetworking/plugins/plugins/ipam/host-local/backend/allocator" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) const MASTER_NAME = "eth0" +type Net struct { + Name string `json:"name"` + CNIVersion string `json:"cniVersion"` + Type string `json:"type,omitempty"` + Master string `json:"master"` + VlanId int `json:"vlanId"` + MTU int `json:"mtu"` + IPAM *allocator.IPAMConfig `json:"ipam"` + DNS types.DNS `json:"dns"` + RawPrevResult map[string]interface{} `json:"prevResult,omitempty"` + PrevResult current.Result `json:"-"` +} + +func buildOneConfig(netName string, cniVersion string, orig *Net, prevResult types.Result) (*Net, error) { + var err error + + inject := map[string]interface{}{ + "name": netName, + "cniVersion": cniVersion, + } + // Add previous plugin result + if prevResult != nil { + inject["prevResult"] = prevResult + } + + // Ensure every config uses the same name and version + config := make(map[string]interface{}) + + confBytes, err := json.Marshal(orig) + if err != nil { + return nil, err + } + + err = json.Unmarshal(confBytes, &config) + if err != nil { + return nil, fmt.Errorf("unmarshal existing network bytes: %s", err) + } + + for key, value := range inject { + config[key] = value + } + + newBytes, err := json.Marshal(config) + if err != nil { + return nil, err + } + + conf := &Net{} + if err := json.Unmarshal(newBytes, &conf); err != nil { + return nil, fmt.Errorf("error parsing configuration: %s", err) + } + + return conf, nil + +} + var _ = Describe("vlan Operations", func() { var originalNS ns.NetNS @@ -234,4 +292,119 @@ var _ = Describe("vlan Operations", func() { }) Expect(err).NotTo(HaveOccurred()) }) + + It("configures and deconfigures an CNI V4 vlan link with ADD/CHECK/DEL", func() { + const IFNAME = "eth0" + + conf := fmt.Sprintf(`{ + "cniVersion": "0.4.0", + "name": "vlanTestv4", + "type": "vlan", + "master": "%s", + "vlanId": 1234, + "ipam": { + "type": "host-local", + "subnet": "10.1.2.0/24" + } +}`, MASTER_NAME) + + targetNs, err := testutils.NewNS() + Expect(err).NotTo(HaveOccurred()) + defer targetNs.Close() + + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: targetNs.Path(), + IfName: IFNAME, + StdinData: []byte(conf), + } + + var result *current.Result + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + r, _, err := testutils.CmdAddWithArgs(args, func() error { + return cmdAdd(args) + }) + Expect(err).NotTo(HaveOccurred()) + + result, err = current.GetResult(r) + Expect(err).NotTo(HaveOccurred()) + + Expect(len(result.Interfaces)).To(Equal(1)) + Expect(result.Interfaces[0].Name).To(Equal(IFNAME)) + Expect(len(result.IPs)).To(Equal(1)) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + // Make sure vlan link exists in the target namespace + err = targetNs.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + link, err := netlink.LinkByName(IFNAME) + Expect(err).NotTo(HaveOccurred()) + Expect(link.Attrs().Name).To(Equal(IFNAME)) + + hwaddr, err := net.ParseMAC(result.Interfaces[0].Mac) + Expect(err).NotTo(HaveOccurred()) + Expect(link.Attrs().HardwareAddr).To(Equal(hwaddr)) + + addrs, err := netlink.AddrList(link, syscall.AF_INET) + Expect(err).NotTo(HaveOccurred()) + Expect(len(addrs)).To(Equal(1)) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + // call CmdCheck + n := &Net{} + err = json.Unmarshal([]byte(conf), &n) + Expect(err).NotTo(HaveOccurred()) + + n.IPAM, _, err = allocator.LoadIPAMConfig([]byte(conf), "") + Expect(err).NotTo(HaveOccurred()) + + cniVersion := "0.4.0" + newConf, err := buildOneConfig("vlanTestv4", cniVersion, n, result) + Expect(err).NotTo(HaveOccurred()) + + confString, err := json.Marshal(newConf) + Expect(err).NotTo(HaveOccurred()) + + args.StdinData = confString + + // CNI Check host-device in the target namespace + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + var err error + err = testutils.CmdCheckWithArgs(args, func() error { return cmdCheck(args) }) + return err + }) + Expect(err).NotTo(HaveOccurred()) + + args.StdinData = []byte(conf) + + err = originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + err = testutils.CmdDelWithArgs(args, func() error { + return cmdDel(args) + }) + Expect(err).NotTo(HaveOccurred()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + // Make sure vlan link has been deleted + err = targetNs.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + link, err := netlink.LinkByName(IFNAME) + Expect(err).To(HaveOccurred()) + Expect(link).To(BeNil()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + }) }) diff --git a/plugins/main/windows/win-bridge/win-bridge_windows.go b/plugins/main/windows/win-bridge/win-bridge_windows.go index b36ae0ca..2f08ee84 100644 --- a/plugins/main/windows/win-bridge/win-bridge_windows.go +++ b/plugins/main/windows/win-bridge/win-bridge_windows.go @@ -21,22 +21,25 @@ import ( "strings" "os" - "github.com/juju/errors" "github.com/Microsoft/hcsshim" "github.com/Microsoft/hcsshim/hcn" + "github.com/juju/errors" + "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/hns" "github.com/containernetworking/plugins/pkg/ipam" + bv "github.com/containernetworking/plugins/pkg/utils/buildversion" ) type NetConf struct { hns.NetConf - IPMasqNetwork string `json:"ipMasqNetwork,omitempty"` - ApiVersion int `json:"ApiVersion"` + IPMasqNetwork string `json:"ipMasqNetwork,omitempty"` + ApiVersion int `json:"ApiVersion"` } func init() { @@ -64,18 +67,18 @@ func ProcessEndpointArgs(args *skel.CmdArgs, n *NetConf) (*hns.EndpointInfo, err if err != nil { return nil, errors.Annotatef(err, "error while ipam.ExecAdd") } - + // Convert whatever the IPAM result was into the current Result type result, err := current.NewResultFromResult(r) if err != nil { return nil, errors.Annotatef(err, "error while NewResultFromResult") - } else { + } else { if len(result.IPs) == 0 { return nil, errors.New("IPAM plugin return is missing IP config") } epInfo.IpAddress = result.IPs[0].Address.IP epInfo.Gateway = result.IPs[0].Address.IP.Mask(result.IPs[0].Address.Mask) - + // Calculate gateway for bridge network (needs to be x.2) epInfo.Gateway[len(epInfo.Gateway)-1] += 2 } @@ -149,7 +152,7 @@ func cmdHcnAdd(args *skel.CmdArgs, n *NetConf) (*current.Result, error) { epName := hns.ConstructEndpointName(args.ContainerID, args.Netns, n.Name) - hcnEndpoint, err := hns.AddHcnEndpoint(epName, hcnNetwork.Id, args.Netns, func () (*hcn.HostComputeEndpoint, error) { + hcnEndpoint, err := hns.AddHcnEndpoint(epName, hcnNetwork.Id, args.Netns, func() (*hcn.HostComputeEndpoint, error) { epInfo, err := ProcessEndpointArgs(args, n) if err != nil { return nil, errors.Annotatef(err, "error while ProcessEndpointArgs") @@ -212,7 +215,7 @@ func cmdDel(args *skel.CmdArgs) error { } } epName := hns.ConstructEndpointName(args.ContainerID, args.Netns, n.Name) - + if n.ApiVersion == 2 { return hns.RemoveHcnEndpoint(epName) } else { @@ -226,5 +229,5 @@ func cmdGet(_ *skel.CmdArgs) error { } func main() { - skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.PluginSupports("0.1.0", "0.2.0", "0.3.0"), "TODO") + skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.PluginSupports("0.1.0", "0.2.0", "0.3.0"), bv.BuildString("win-bridge")) } diff --git a/plugins/main/windows/win-overlay/win-overlay_windows.go b/plugins/main/windows/win-overlay/win-overlay_windows.go index 580bd16a..634c34c4 100644 --- a/plugins/main/windows/win-overlay/win-overlay_windows.go +++ b/plugins/main/windows/win-overlay/win-overlay_windows.go @@ -21,15 +21,17 @@ import ( "strings" "os" + "github.com/Microsoft/hcsshim" "github.com/juju/errors" - "github.com/Microsoft/hcsshim" "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/hns" "github.com/containernetworking/plugins/pkg/ipam" + bv "github.com/containernetworking/plugins/pkg/utils/buildversion" ) type NetConf struct { @@ -172,5 +174,5 @@ func cmdGet(_ *skel.CmdArgs) error { } func main() { - skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.PluginSupports("0.1.0", "0.2.0", "0.3.0"), "TODO") + skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.PluginSupports("0.1.0", "0.2.0", "0.3.0"), bv.BuildString("win-overlay")) } diff --git a/plugins/meta/bandwidth/bandwidth_linux_test.go b/plugins/meta/bandwidth/bandwidth_linux_test.go index c36b31e1..f09980fa 100644 --- a/plugins/meta/bandwidth/bandwidth_linux_test.go +++ b/plugins/meta/bandwidth/bandwidth_linux_test.go @@ -35,6 +35,49 @@ import ( "github.com/onsi/gomega/gexec" ) +func buildOneConfig(name, cniVersion string, orig *PluginConf, prevResult types.Result) (*PluginConf, []byte, error) { + var err error + + inject := map[string]interface{}{ + "name": name, + "cniVersion": cniVersion, + } + // Add previous plugin result + if prevResult != nil { + inject["prevResult"] = prevResult + } + + // Ensure every config uses the same name and version + config := make(map[string]interface{}) + + confBytes, err := json.Marshal(orig) + if err != nil { + return nil, nil, err + } + + err = json.Unmarshal(confBytes, &config) + if err != nil { + return nil, nil, fmt.Errorf("unmarshal existing network bytes: %s", err) + } + + for key, value := range inject { + config[key] = value + } + + newBytes, err := json.Marshal(config) + if err != nil { + return nil, nil, err + } + + conf := &PluginConf{} + if err := json.Unmarshal(newBytes, &conf); err != nil { + return nil, nil, fmt.Errorf("error parsing configuration: %s", err) + } + + return conf, newBytes, nil + +} + var _ = Describe("bandwidth test", func() { var ( hostNs ns.NetNS @@ -643,7 +686,6 @@ var _ = Describe("bandwidth test", func() { containerWithTbfResult, err := current.GetResult(containerWithTbfRes) Expect(err).NotTo(HaveOccurred()) - tbfPluginConf := PluginConf{} tbfPluginConf.RuntimeConfig.Bandwidth = &BandwidthEntry{ IngressBurst: burstInBits, @@ -654,7 +696,7 @@ var _ = Describe("bandwidth test", func() { tbfPluginConf.Name = "mynet" tbfPluginConf.CNIVersion = "0.3.0" tbfPluginConf.Type = "bandwidth" - tbfPluginConf.RawPrevResult = &map[string]interface{}{ + tbfPluginConf.RawPrevResult = map[string]interface{}{ "ips": containerWithTbfResult.IPs, "interfaces": containerWithTbfResult.Interfaces, } @@ -663,7 +705,6 @@ var _ = Describe("bandwidth test", func() { IPs: containerWithTbfResult.IPs, Interfaces: containerWithTbfResult.Interfaces, } - conf, err := json.Marshal(tbfPluginConf) Expect(err).NotTo(HaveOccurred()) @@ -725,4 +766,169 @@ var _ = Describe("bandwidth test", func() { }, 1) }) + Context("when chaining bandwidth plugin with PTP using 0.4.0 config", func() { + var ptpConf string + var rateInBits int + var burstInBits int + var packetInBytes int + var containerWithoutTbfNS ns.NetNS + var containerWithTbfNS ns.NetNS + var portServerWithTbf int + var portServerWithoutTbf int + + var containerWithTbfRes types.Result + var containerWithoutTbfRes types.Result + var echoServerWithTbf *gexec.Session + var echoServerWithoutTbf *gexec.Session + + BeforeEach(func() { + rateInBytes := 1000 + rateInBits = rateInBytes * 8 + burstInBits = rateInBits * 2 + packetInBytes = rateInBytes * 25 + + ptpConf = `{ + "cniVersion": "0.4.0", + "name": "myBWnet", + "type": "ptp", + "ipMasq": true, + "mtu": 512, + "ipam": { + "type": "host-local", + "subnet": "10.1.2.0/24" + } +}` + + containerWithTbfIFName := "ptp0" + containerWithoutTbfIFName := "ptp1" + + var err error + containerWithTbfNS, err = testutils.NewNS() + Expect(err).NotTo(HaveOccurred()) + + containerWithoutTbfNS, err = testutils.NewNS() + Expect(err).NotTo(HaveOccurred()) + + By("create two containers, and use the bandwidth plugin on one of them") + Expect(hostNs.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + containerWithTbfRes, _, err = testutils.CmdAdd(containerWithTbfNS.Path(), "dummy", containerWithTbfIFName, []byte(ptpConf), func() error { + r, err := invoke.DelegateAdd(context.TODO(), "ptp", []byte(ptpConf), nil) + Expect(r.Print()).To(Succeed()) + + return err + }) + Expect(err).NotTo(HaveOccurred()) + + containerWithoutTbfRes, _, err = testutils.CmdAdd(containerWithoutTbfNS.Path(), "dummy2", containerWithoutTbfIFName, []byte(ptpConf), func() error { + r, err := invoke.DelegateAdd(context.TODO(), "ptp", []byte(ptpConf), nil) + Expect(r.Print()).To(Succeed()) + + return err + }) + Expect(err).NotTo(HaveOccurred()) + + containerWithTbfResult, err := current.GetResult(containerWithTbfRes) + Expect(err).NotTo(HaveOccurred()) + + tbfPluginConf := &PluginConf{} + err = json.Unmarshal([]byte(ptpConf), &tbfPluginConf) + Expect(err).NotTo(HaveOccurred()) + + tbfPluginConf.RuntimeConfig.Bandwidth = &BandwidthEntry{ + IngressBurst: burstInBits, + IngressRate: rateInBits, + EgressBurst: burstInBits, + EgressRate: rateInBits, + } + tbfPluginConf.Type = "bandwidth" + cniVersion := "0.4.0" + _, newConfBytes, err := buildOneConfig("myBWnet", cniVersion, tbfPluginConf, containerWithTbfResult) + Expect(err).NotTo(HaveOccurred()) + + args := &skel.CmdArgs{ + ContainerID: "dummy3", + Netns: containerWithTbfNS.Path(), + IfName: containerWithTbfIFName, + StdinData: newConfBytes, + } + + result, out, err := testutils.CmdAdd(containerWithTbfNS.Path(), args.ContainerID, "", newConfBytes, func() error { return cmdAdd(args) }) + Expect(err).NotTo(HaveOccurred(), string(out)) + + // Do CNI Check + checkConf := &PluginConf{} + err = json.Unmarshal([]byte(ptpConf), &checkConf) + Expect(err).NotTo(HaveOccurred()) + + checkConf.RuntimeConfig.Bandwidth = &BandwidthEntry{ + IngressBurst: burstInBits, + IngressRate: rateInBits, + EgressBurst: burstInBits, + EgressRate: rateInBits, + } + checkConf.Type = "bandwidth" + + _, newCheckBytes, err := buildOneConfig("myBWnet", cniVersion, checkConf, result) + Expect(err).NotTo(HaveOccurred()) + + args = &skel.CmdArgs{ + ContainerID: "dummy3", + Netns: containerWithTbfNS.Path(), + IfName: containerWithTbfIFName, + StdinData: newCheckBytes, + } + + err = testutils.CmdCheck(containerWithTbfNS.Path(), args.ContainerID, "", newCheckBytes, func() error { return cmdCheck(args) }) + Expect(err).NotTo(HaveOccurred()) + + return nil + })).To(Succeed()) + + By("starting a tcp server on both containers") + portServerWithTbf, echoServerWithTbf, err = startEchoServerInNamespace(containerWithTbfNS) + Expect(err).NotTo(HaveOccurred()) + portServerWithoutTbf, echoServerWithoutTbf, err = startEchoServerInNamespace(containerWithoutTbfNS) + Expect(err).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + containerWithTbfNS.Close() + containerWithoutTbfNS.Close() + if echoServerWithoutTbf != nil { + echoServerWithoutTbf.Kill() + } + if echoServerWithTbf != nil { + echoServerWithTbf.Kill() + } + }) + + Measure("limits ingress traffic on veth device", func(b Benchmarker) { + var runtimeWithLimit time.Duration + var runtimeWithoutLimit time.Duration + + By("gather timing statistics about both containers") + By("sending tcp traffic to the container that has traffic shaped", func() { + runtimeWithLimit = b.Time("with tbf", func() { + result, err := current.GetResult(containerWithTbfRes) + Expect(err).NotTo(HaveOccurred()) + + makeTcpClientInNS(hostNs.Path(), result.IPs[0].Address.IP.String(), portServerWithTbf, packetInBytes) + }) + }) + + By("sending tcp traffic to the container that does not have traffic shaped", func() { + runtimeWithoutLimit = b.Time("without tbf", func() { + result, err := current.GetResult(containerWithoutTbfRes) + Expect(err).NotTo(HaveOccurred()) + + makeTcpClientInNS(hostNs.Path(), result.IPs[0].Address.IP.String(), portServerWithoutTbf, packetInBytes) + }) + }) + + Expect(runtimeWithLimit).To(BeNumerically(">", runtimeWithoutLimit+1000*time.Millisecond)) + }, 1) + }) + }) diff --git a/plugins/meta/bandwidth/main.go b/plugins/meta/bandwidth/main.go index 5a2e5630..6d800589 100644 --- a/plugins/meta/bandwidth/main.go +++ b/plugins/meta/bandwidth/main.go @@ -20,13 +20,15 @@ import ( "errors" "fmt" + "github.com/vishvananda/netlink" + "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/ip" - "github.com/vishvananda/netlink" + "github.com/containernetworking/plugins/pkg/ip" + bv "github.com/containernetworking/plugins/pkg/utils/buildversion" ) // BandwidthEntry corresponds to a single entry in the bandwidth argument, @@ -50,10 +52,6 @@ type PluginConf struct { Bandwidth *BandwidthEntry `json:"bandwidth,omitempty"` } `json:"runtimeConfig,omitempty"` - // RuntimeConfig *struct{} `json:"runtimeConfig"` - - RawPrevResult *map[string]interface{} `json:"prevResult"` - PrevResult *current.Result `json:"-"` *BandwidthEntry } @@ -65,21 +63,6 @@ func parseConfig(stdin []byte) (*PluginConf, error) { return nil, fmt.Errorf("failed to parse network configuration: %v", err) } - 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) - } - } bandwidth := getBandwidth(&conf) if bandwidth != nil { err := validateRateAndBurst(bandwidth.IngressRate, bandwidth.IngressBurst) @@ -92,6 +75,18 @@ func parseConfig(stdin []byte) (*PluginConf, error) { } } + if conf.RawPrevResult != nil { + var err error + if err = version.ParsePrevResult(&conf.NetConf); err != nil { + return nil, fmt.Errorf("could not parse prevResult: %v", err) + } + + _, err = current.NewResultFromResult(conf.PrevResult) + if err != nil { + return nil, fmt.Errorf("could not convert result to current version: %v", err) + } + } + return &conf, nil } @@ -167,7 +162,11 @@ func cmdAdd(args *skel.CmdArgs) error { return fmt.Errorf("must be called as chained plugin") } - hostInterface, err := getHostInterface(conf.PrevResult.Interfaces) + result, err := current.NewResultFromResult(conf.PrevResult) + if err != nil { + return fmt.Errorf("could not convert result to current version: %v", err) + } + hostInterface, err := getHostInterface(result.Interfaces) if err != nil { return err } @@ -200,7 +199,7 @@ func cmdAdd(args *skel.CmdArgs) error { return err } - conf.PrevResult.Interfaces = append(conf.PrevResult.Interfaces, ¤t.Interface{ + result.Interfaces = append(result.Interfaces, ¤t.Interface{ Name: ifbDeviceName, Mac: ifbDevice.Attrs().HardwareAddr.String(), }) @@ -210,7 +209,7 @@ func cmdAdd(args *skel.CmdArgs) error { } } - return types.PrintResult(conf.PrevResult, conf.CNIVersion) + return types.PrintResult(result, conf.CNIVersion) } func cmdDel(args *skel.CmdArgs) error { @@ -232,11 +231,125 @@ func cmdDel(args *skel.CmdArgs) error { } func main() { - // TODO: implement plugin version - skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.PluginSupports("0.3.0", "0.3.1", version.Current()), "TODO") + skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.PluginSupports("0.3.0", "0.3.1", version.Current()), bv.BuildString("bandwidth")) } -func cmdGet(args *skel.CmdArgs) error { - // TODO: implement - return fmt.Errorf("not implemented") +func SafeQdiscList(link netlink.Link) ([]netlink.Qdisc, error) { + qdiscs, err := netlink.QdiscList(link) + if err != nil { + return nil, err + } + result := []netlink.Qdisc{} + for _, qdisc := range qdiscs { + // filter out pfifo_fast qdiscs because + // older kernels don't return them + _, pfifo := qdisc.(*netlink.PfifoFast) + if !pfifo { + result = append(result, qdisc) + } + } + return result, nil +} + +func cmdCheck(args *skel.CmdArgs) error { + bwConf, err := parseConfig(args.StdinData) + if err != nil { + return err + } + + if bwConf.PrevResult == nil { + return fmt.Errorf("must be called as a chained plugin") + } + + result, err := current.NewResultFromResult(bwConf.PrevResult) + if err != nil { + return fmt.Errorf("could not convert result to current version: %v", err) + } + + hostInterface, err := getHostInterface(result.Interfaces) + if err != nil { + return err + } + link, err := netlink.LinkByName(hostInterface.Name) + if err != nil { + return err + } + + bandwidth := getBandwidth(bwConf) + + if bandwidth.IngressRate > 0 && bandwidth.IngressBurst > 0 { + rateInBytes := bandwidth.IngressRate / 8 + burstInBytes := bandwidth.IngressBurst / 8 + bufferInBytes := buffer(uint64(rateInBytes), uint32(burstInBytes)) + latency := latencyInUsec(latencyInMillis) + limitInBytes := limit(uint64(rateInBytes), latency, uint32(burstInBytes)) + + qdiscs, err := SafeQdiscList(link) + if err != nil { + return err + } + if len(qdiscs) == 0 { + return fmt.Errorf("Failed to find qdisc") + } + + for _, qdisc := range qdiscs { + tbf, isTbf := qdisc.(*netlink.Tbf) + if !isTbf { + break + } + if tbf.Rate != uint64(rateInBytes) { + return fmt.Errorf("Rate doesn't match") + } + if tbf.Limit != uint32(limitInBytes) { + return fmt.Errorf("Limit doesn't match") + } + if tbf.Buffer != uint32(bufferInBytes) { + return fmt.Errorf("Buffer doesn't match") + } + } + } + + if bandwidth.EgressRate > 0 && bandwidth.EgressBurst > 0 { + rateInBytes := bandwidth.EgressRate / 8 + burstInBytes := bandwidth.EgressBurst / 8 + bufferInBytes := buffer(uint64(rateInBytes), uint32(burstInBytes)) + latency := latencyInUsec(latencyInMillis) + limitInBytes := limit(uint64(rateInBytes), latency, uint32(burstInBytes)) + + ifbDeviceName, err := getIfbDeviceName(bwConf.Name, args.ContainerID) + if err != nil { + return err + } + + ifbDevice, err := netlink.LinkByName(ifbDeviceName) + if err != nil { + return fmt.Errorf("get ifb device: %s", err) + } + + qdiscs, err := SafeQdiscList(ifbDevice) + if err != nil { + return err + } + if len(qdiscs) == 0 { + return fmt.Errorf("Failed to find qdisc") + } + + for _, qdisc := range qdiscs { + tbf, isTbf := qdisc.(*netlink.Tbf) + if !isTbf { + break + } + if tbf.Rate != uint64(rateInBytes) { + return fmt.Errorf("Rate doesn't match") + } + if tbf.Limit != uint32(limitInBytes) { + return fmt.Errorf("Limit doesn't match") + } + if tbf.Buffer != uint32(bufferInBytes) { + return fmt.Errorf("Buffer doesn't match") + } + } + } + + return nil } diff --git a/plugins/meta/firewall/README.md b/plugins/meta/firewall/README.md new file mode 100644 index 00000000..f73a4121 --- /dev/null +++ b/plugins/meta/firewall/README.md @@ -0,0 +1,135 @@ +# firewall plugin + +## Overview + +This plugin creates firewall rules to allow traffic to/from container IP address via the host network . +It does not create any network interfaces and therefore does not set up connectivity by itself. +It is intended to be used as a chained plugins. + +## Operation +The following network configuration file + +```json +{ + "cniVersion": "0.3.1", + "name": "bridge-firewalld", + "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": "firewall", + } + ] +} +``` + +will allow any IP addresses configured by earlier plugins to send/receive traffic via the host. + +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 previous result. + +## Backends + +This plugin supports multiple firewall backends that implement the desired functionality. +Available backends include `iptables` and `firewalld` and may be selected with the `backend` key. +If no `backend` key is given, the plugin will use firewalld if the service exists on the D-Bus system bus. +If no firewalld service is found, it will fall back to iptables. + +## firewalld backend rule structure +When the `firewalld` backend is used, this example will place the IPAM allocated address for the container (e.g. 10.88.0.2) into firewalld's `trusted` zone, allowing it to send/receive traffic. + + +A sample standalone config list (with the file extension .conflist) using firewalld backend might +look like: + +```json +{ + "cniVersion": "0.3.1", + "name": "bridge-firewalld", + "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": "firewall", + "backend": "firewalld" + } + ] +} +``` + + +`FORWARD_IN_ZONES_SOURCE` chain: +- `-d 10.88.0.2 -j FWDI_trusted` + +`CNI_FORWARD_OUT_ZONES_SOURCE` chain: +- `-s 10.88.0.2 -j FWDO_trusted` + + +## iptables backend rule structure + +A sample standalone config list (with the file extension .conflist) using iptables backend might +look like: + +```json +{ + "cniVersion": "0.3.1", + "name": "bridge-firewalld", + "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": "firewall", + "backend": "iptables" + } + ] +} +``` + +When the `iptables` backend is used, the above example will create two new iptables chains in the `filter` table and add rules that allow the given interface to send/receive traffic. + +### FORWARD +A new chain, CNI-FORWARD is added to the FORWARD chain. CNI-FORWARD is the chain where rules will be added +when containers are created and from where rules will be removed when containers terminate. + +`FORWARD` chain: +- `-j CNI-FORWARD` + +CNI-FORWARD will have a pair of rules added, one for each direction, using the IPAM assigned IP address +of the container as shown: + +`CNI_FORWARD` chain: +- `-s 10.88.0.2 -m conntrack --ctstate RELATED,ESTABLISHED -j CNI-FORWARD` +- `-d 10.88.0.2 -j CNI-FORWARD` + diff --git a/plugins/meta/firewall/firewall.go b/plugins/meta/firewall/firewall.go new file mode 100644 index 00000000..c2eef93a --- /dev/null +++ b/plugins/meta/firewall/firewall.go @@ -0,0 +1,184 @@ +// 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" + "net" + + "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" + bv "github.com/containernetworking/plugins/pkg/utils/buildversion" +) + +// FirewallNetConf represents the firewall configuration. +type FirewallNetConf struct { + types.NetConf + + // Backend is the firewall type to add rules to. Allowed values are + // 'iptables' and 'firewalld'. + Backend string `json:"backend"` + + // IptablesAdminChainName is an optional name to use instead of the default + // admin rules override chain name that includes the interface name. + IptablesAdminChainName string `json:"iptablesAdminChainName,omitempty"` + + // 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 `json:"firewalldZone,omitempty"` +} + +type FirewallBackend interface { + Add(*FirewallNetConf, *current.Result) error + Del(*FirewallNetConf, *current.Result) error + Check(*FirewallNetConf, *current.Result) error +} + +func ipString(ip net.IPNet) string { + if ip.IP.To4() == nil { + return ip.IP.String() + "/128" + } + return ip.IP.String() + "/32" +} + +func parseConf(data []byte) (*FirewallNetConf, *current.Result, error) { + conf := FirewallNetConf{} + if err := json.Unmarshal(data, &conf); err != nil { + return nil, nil, fmt.Errorf("failed to load netconf: %v", err) + } + + // Parse previous result. + if conf.RawPrevResult == nil { + return nil, nil, fmt.Errorf("missing prevResult from earlier plugin") + } + + // Parse previous result. + var result *current.Result + var err error + if err = version.ParsePrevResult(&conf.NetConf); err != nil { + return nil, nil, fmt.Errorf("could not parse prevResult: %v", err) + } + + result, err = current.NewResultFromResult(conf.PrevResult) + if err != nil { + return nil, nil, fmt.Errorf("could not convert result to current version: %v", err) + } + + // Default the firewalld zone to trusted + if conf.FirewalldZone == "" { + conf.FirewalldZone = "trusted" + } + + return &conf, result, nil +} + +func getBackend(conf *FirewallNetConf) (FirewallBackend, error) { + switch conf.Backend { + case "iptables": + return newIptablesBackend(conf) + case "firewalld": + return newFirewalldBackend(conf) + } + + // Default to firewalld if it's running + if isFirewalldRunning() { + return newFirewalldBackend(conf) + } + + // Otherwise iptables + return newIptablesBackend(conf) +} + +func cmdAdd(args *skel.CmdArgs) error { + conf, result, err := parseConf(args.StdinData) + if err != nil { + return err + } + + backend, err := getBackend(conf) + if err != nil { + return err + } + + if err := backend.Add(conf, result); err != nil { + return err + } + + if result == nil { + result = ¤t.Result{} + } + return types.PrintResult(result, conf.CNIVersion) +} + +func cmdDel(args *skel.CmdArgs) error { + conf, result, err := parseConf(args.StdinData) + if err != nil { + return err + } + + backend, err := getBackend(conf) + 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 + if err := backend.Del(conf, result); err != nil { + return err + } + + return nil +} + +func main() { + skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.PluginSupports("0.4.0"), bv.BuildString("firewall")) +} + +func cmdCheck(args *skel.CmdArgs) error { + conf, result, err := parseConf(args.StdinData) + if err != nil { + return err + } + + // Ensure we have previous result. + if result == nil { + return fmt.Errorf("Required prevResult missing") + } + + backend, err := getBackend(conf) + if err != nil { + return err + } + + if err := backend.Check(conf, result); err != nil { + return err + } + + return nil +} diff --git a/plugins/meta/firewall/firewall_firewalld_test.go b/plugins/meta/firewall/firewall_firewalld_test.go new file mode 100644 index 00000000..68a00793 --- /dev/null +++ b/plugins/meta/firewall/firewall_firewalld_test.go @@ -0,0 +1,343 @@ +// 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 ( + "bufio" + "fmt" + "os/exec" + "strings" + "sync" + "syscall" + + "github.com/containernetworking/cni/pkg/invoke" + "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/godbus/dbus" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +const ( + confTmpl = `{ + "cniVersion": "0.3.1", + "name": "firewalld-test", + "type": "firewall", + "backend": "firewalld", + "zone": "trusted", + "prevResult": { + "cniVersion": "0.3.0", + "interfaces": [ + {"name": "%s", "sandbox": "%s"} + ], + "ips": [ + { + "version": "4", + "address": "10.0.0.2/24", + "gateway": "10.0.0.1", + "interface": 0 + } + ] + } +}` + ifname = "eth0" +) + +type fakeFirewalld struct { + zone string + source string +} + +func (f *fakeFirewalld) clear() { + f.zone = "" + f.source = "" +} + +func (f *fakeFirewalld) AddSource(zone, source string) (string, *dbus.Error) { + f.zone = zone + f.source = source + return "", nil +} + +func (f *fakeFirewalld) RemoveSource(zone, source string) (string, *dbus.Error) { + f.zone = zone + f.source = source + return "", nil +} + +func (f *fakeFirewalld) QuerySource(zone, source string) (bool, *dbus.Error) { + if f.zone != zone { + return false, nil + } + if f.source != source { + return false, nil + } + return true, nil +} + +func spawnSessionDbus(wg *sync.WaitGroup) (string, *exec.Cmd) { + // Start a private D-Bus session bus + path, err := invoke.FindInPath("dbus-daemon", []string{ + "/bin", "/sbin", "/usr/bin", "/usr/sbin", + }) + Expect(err).NotTo(HaveOccurred()) + cmd := exec.Command(path, "--session", "--print-address", "--nofork", "--nopidfile") + stdout, err := cmd.StdoutPipe() + Expect(err).NotTo(HaveOccurred()) + err = cmd.Start() + Expect(err).NotTo(HaveOccurred()) + + // Wait for dbus-daemon to print the bus address + bytes, err := bufio.NewReader(stdout).ReadString('\n') + Expect(err).NotTo(HaveOccurred()) + busAddr := strings.TrimSpace(string(bytes)) + Expect(strings.HasPrefix(busAddr, "unix:abstract")).To(BeTrue()) + + var startWg sync.WaitGroup + wg.Add(1) + startWg.Add(1) + go func() { + defer GinkgoRecover() + + startWg.Done() + err = cmd.Wait() + Expect(err).NotTo(HaveOccurred()) + wg.Done() + }() + + startWg.Wait() + return busAddr, cmd +} + +var _ = Describe("firewalld test", func() { + var ( + targetNs ns.NetNS + cmd *exec.Cmd + conn *dbus.Conn + wg sync.WaitGroup + fwd *fakeFirewalld + busAddr string + ) + + BeforeEach(func() { + var err error + targetNs, err = testutils.NewNS() + Expect(err).NotTo(HaveOccurred()) + + // Start a private D-Bus session bus + busAddr, cmd = spawnSessionDbus(&wg) + conn, err = dbus.Dial(busAddr) + Expect(err).NotTo(HaveOccurred()) + err = conn.Auth(nil) + Expect(err).NotTo(HaveOccurred()) + err = conn.Hello() + Expect(err).NotTo(HaveOccurred()) + + // Start our fake firewalld + reply, err := conn.RequestName(firewalldName, dbus.NameFlagDoNotQueue) + Expect(err).NotTo(HaveOccurred()) + Expect(reply).To(Equal(dbus.RequestNameReplyPrimaryOwner)) + + fwd = &fakeFirewalld{} + // Because firewalld D-Bus methods start with lower-case, and + // because in Go lower-case methods are private, we need to remap + // Go public methods to the D-Bus name + methods := map[string]string{ + "AddSource": firewalldAddSourceMethod, + "QuerySource": firewalldQuerySourceMethod, + "RemoveSource": firewalldRemoveSourceMethod, + } + conn.ExportWithMap(fwd, methods, firewalldPath, firewalldZoneInterface) + + // Make sure the plugin uses our private session bus + testConn = conn + }) + + AfterEach(func() { + _, err := conn.ReleaseName(firewalldName) + Expect(err).NotTo(HaveOccurred()) + + err = cmd.Process.Signal(syscall.SIGTERM) + Expect(err).NotTo(HaveOccurred()) + + wg.Wait() + }) + + It("works with a 0.3.1 config", func() { + Expect(isFirewalldRunning()).To(BeTrue()) + + conf := fmt.Sprintf(confTmpl, ifname, targetNs.Path()) + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: targetNs.Path(), + IfName: ifname, + StdinData: []byte(conf), + } + _, _, err := testutils.CmdAdd(targetNs.Path(), args.ContainerID, ifname, []byte(conf), func() error { + return cmdAdd(args) + }) + Expect(err).NotTo(HaveOccurred()) + Expect(fwd.zone).To(Equal("trusted")) + Expect(fwd.source).To(Equal("10.0.0.2/32")) + fwd.clear() + + err = testutils.CmdDel(targetNs.Path(), args.ContainerID, ifname, func() error { + return cmdDel(args) + }) + Expect(err).NotTo(HaveOccurred()) + Expect(fwd.zone).To(Equal("trusted")) + Expect(fwd.source).To(Equal("10.0.0.2/32")) + }) + + It("defaults to the firewalld backend", func() { + conf := `{ + "cniVersion": "0.3.1", + "name": "firewalld-test", + "type": "firewall", + "zone": "trusted", + "prevResult": { + "cniVersion": "0.3.0", + "interfaces": [ + {"name": "eth0", "sandbox": "/foobar"} + ], + "ips": [ + { + "version": "4", + "address": "10.0.0.2/24", + "gateway": "10.0.0.1", + "interface": 0 + } + ] + } + }` + + Expect(isFirewalldRunning()).To(BeTrue()) + + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: targetNs.Path(), + IfName: ifname, + StdinData: []byte(conf), + } + _, _, err := testutils.CmdAdd(targetNs.Path(), args.ContainerID, ifname, []byte(conf), func() error { + return cmdAdd(args) + }) + Expect(err).NotTo(HaveOccurred()) + Expect(fwd.zone).To(Equal("trusted")) + Expect(fwd.source).To(Equal("10.0.0.2/32")) + }) + + It("passes through the prevResult", func() { + conf := `{ + "cniVersion": "0.3.1", + "name": "firewalld-test", + "type": "firewall", + "zone": "trusted", + "prevResult": { + "cniVersion": "0.3.0", + "interfaces": [ + {"name": "eth0", "sandbox": "/foobar"} + ], + "ips": [ + { + "version": "4", + "address": "10.0.0.2/24", + "gateway": "10.0.0.1", + "interface": 0 + } + ] + } + }` + + Expect(isFirewalldRunning()).To(BeTrue()) + + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: targetNs.Path(), + IfName: ifname, + StdinData: []byte(conf), + } + r, _, err := testutils.CmdAdd(targetNs.Path(), args.ContainerID, ifname, []byte(conf), func() error { + return cmdAdd(args) + }) + Expect(err).NotTo(HaveOccurred()) + + result, err := current.GetResult(r) + Expect(err).NotTo(HaveOccurred()) + + Expect(len(result.Interfaces)).To(Equal(1)) + Expect(result.Interfaces[0].Name).To(Equal("eth0")) + Expect(len(result.IPs)).To(Equal(1)) + Expect(result.IPs[0].Address.String()).To(Equal("10.0.0.2/24")) + }) + + It("works with a 0.4.0 config, including Check", func() { + Expect(isFirewalldRunning()).To(BeTrue()) + + conf := `{ + "cniVersion": "0.4.0", + "name": "firewalld-test", + "type": "firewall", + "backend": "firewalld", + "zone": "trusted", + "prevResult": { + "cniVersion": "0.4.0", + "interfaces": [ + {"name": "eth0", "sandbox": "/foobar"} + ], + "ips": [ + { + "version": "4", + "address": "10.0.0.2/24", + "gateway": "10.0.0.1", + "interface": 0 + } + ] + } + }` + + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: targetNs.Path(), + IfName: ifname, + StdinData: []byte(conf), + } + r, _, err := testutils.CmdAddWithArgs(args, func() error { + return cmdAdd(args) + }) + Expect(err).NotTo(HaveOccurred()) + Expect(fwd.zone).To(Equal("trusted")) + Expect(fwd.source).To(Equal("10.0.0.2/32")) + + _, err = current.GetResult(r) + Expect(err).NotTo(HaveOccurred()) + + err = testutils.CmdCheckWithArgs(args, func() error { + return cmdCheck(args) + }) + Expect(err).NotTo(HaveOccurred()) + + err = testutils.CmdDelWithArgs(args, func() error { + return cmdDel(args) + }) + Expect(err).NotTo(HaveOccurred()) + Expect(fwd.zone).To(Equal("trusted")) + Expect(fwd.source).To(Equal("10.0.0.2/32")) + }) +}) diff --git a/plugins/meta/firewall/firewall_iptables_test.go b/plugins/meta/firewall/firewall_iptables_test.go new file mode 100644 index 00000000..6c7358f4 --- /dev/null +++ b/plugins/meta/firewall/firewall_iptables_test.go @@ -0,0 +1,512 @@ +// 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 ( + "encoding/json" + "fmt" + "strings" + + "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/containernetworking/plugins/pkg/testutils" + + "github.com/vishvananda/netlink" + + "github.com/coreos/go-iptables/iptables" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func findChains(chains []string) (bool, bool) { + var foundAdmin, foundPriv bool + for _, ch := range chains { + if ch == "CNI-ADMIN" { + foundAdmin = true + } else if ch == "CNI-FORWARD" { + foundPriv = true + } + } + return foundAdmin, foundPriv +} + +func findForwardJumpRules(rules []string) (bool, bool) { + var foundAdmin, foundPriv bool + for _, rule := range rules { + if strings.Contains(rule, "-j CNI-ADMIN") { + foundAdmin = true + } else if strings.Contains(rule, "-j CNI-FORWARD") { + foundPriv = true + } + } + return foundAdmin, foundPriv +} + +func findForwardAllowRules(rules []string, ip string) (bool, bool) { + var foundOne, foundTwo bool + for _, rule := range rules { + if !strings.HasSuffix(rule, "-j ACCEPT") { + continue + } + if strings.Contains(rule, fmt.Sprintf(" -s %s ", ip)) { + foundOne = true + } else if strings.Contains(rule, fmt.Sprintf(" -d %s ", ip)) && strings.Contains(rule, "RELATED,ESTABLISHED") { + foundTwo = true + } + } + return foundOne, foundTwo +} + +func getPrevResult(bytes []byte) *current.Result { + type TmpConf struct { + types.NetConf + RawPrevResult map[string]interface{} `json:"prevResult,omitempty"` + PrevResult *current.Result `json:"-"` + } + + conf := &TmpConf{} + err := json.Unmarshal(bytes, conf) + Expect(err).NotTo(HaveOccurred()) + if conf.RawPrevResult == nil { + return nil + } + + resultBytes, err := json.Marshal(conf.RawPrevResult) + Expect(err).NotTo(HaveOccurred()) + res, err := version.NewResult(conf.CNIVersion, resultBytes) + Expect(err).NotTo(HaveOccurred()) + prevResult, err := current.NewResultFromResult(res) + Expect(err).NotTo(HaveOccurred()) + + return prevResult +} + +func validateFullRuleset(bytes []byte) { + prevResult := getPrevResult(bytes) + + for _, ip := range prevResult.IPs { + 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)) + } +} + +func validateCleanedUp(bytes []byte) { + prevResult := getPrevResult(bytes) + + for _, ip := range prevResult.IPs { + ipt, err := iptables.NewWithProtocol(protoForIP(ip.Address)) + Expect(err).NotTo(HaveOccurred()) + + // Our private and admin chains don't get cleaned up + 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()) + _, 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()) + foundAdmin, _ = findForwardJumpRules(rules) + Expect(foundAdmin).To(Equal(true)) + + // Expect no IP address rules for this IP + foundOne, foundTwo := findForwardAllowRules(rules, ipString(ip.Address)) + Expect(foundOne).To(Equal(false)) + Expect(foundTwo).To(Equal(false)) + } +} + +var _ = Describe("firewall plugin iptables backend", func() { + var originalNS, targetNS ns.NetNS + const IFNAME string = "dummy0" + + fullConf := []byte(`{ + "name": "test", + "type": "firewall", + "backend": "iptables", + "ifName": "dummy0", + "cniVersion": "0.3.1", + "prevResult": { + "interfaces": [ + {"name": "dummy0"} + ], + "ips": [ + { + "version": "4", + "address": "10.0.0.2/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()) + 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("passes prevResult through unchanged", func() { + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: targetNS.Path(), + IfName: IFNAME, + StdinData: fullConf, + } + + err := originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + r, _, err := testutils.CmdAdd(targetNS.Path(), args.ContainerID, IFNAME, fullConf, func() error { + return cmdAdd(args) + }) + Expect(err).NotTo(HaveOccurred()) + + result, err := current.GetResult(r) + Expect(err).NotTo(HaveOccurred()) + + Expect(len(result.Interfaces)).To(Equal(1)) + Expect(result.Interfaces[0].Name).To(Equal(IFNAME)) + Expect(len(result.IPs)).To(Equal(2)) + Expect(result.IPs[0].Address.String()).To(Equal("10.0.0.2/24")) + Expect(result.IPs[1].Address.String()).To(Equal("2001:db8:1:2::1/64")) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("installs the right iptables rules on the host", func() { + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: targetNS.Path(), + IfName: IFNAME, + StdinData: fullConf, + } + + err := originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + _, _, err := testutils.CmdAdd(targetNS.Path(), args.ContainerID, IFNAME, fullConf, func() error { + return cmdAdd(args) + }) + Expect(err).NotTo(HaveOccurred()) + + validateFullRuleset(fullConf) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("correctly handles a custom IptablesAdminChainName", func() { + conf := []byte(`{ + "name": "test", + "type": "firewall", + "backend": "iptables", + "ifName": "dummy0", + "cniVersion": "0.3.1", + "iptablesAdminChainName": "CNI-foobar", + "prevResult": { + "interfaces": [ + {"name": "dummy0"} + ], + "ips": [ + { + "version": "4", + "address": "10.0.0.2/24", + "interface": 0 + }, + { + "version": "6", + "address": "2001:db8:1:2::1/64", + "interface": 0 + } + ] + } +}`) + + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: targetNS.Path(), + IfName: IFNAME, + StdinData: conf, + } + + err := originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + _, _, err := testutils.CmdAdd(targetNS.Path(), args.ContainerID, IFNAME, conf, func() error { + return cmdAdd(args) + }) + Expect(err).NotTo(HaveOccurred()) + + var ipt *iptables.IPTables + for _, proto := range []iptables.Protocol{iptables.ProtocolIPv4, iptables.ProtocolIPv6} { + ipt, err = iptables.NewWithProtocol(proto) + Expect(err).NotTo(HaveOccurred()) + + // Ensure custom admin chain name + chains, err := ipt.ListChains("filter") + Expect(err).NotTo(HaveOccurred()) + var foundAdmin bool + for _, ch := range chains { + if ch == "CNI-foobar" { + foundAdmin = true + } + } + Expect(foundAdmin).To(Equal(true)) + } + + return nil + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("cleans up on delete", func() { + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: targetNS.Path(), + IfName: IFNAME, + StdinData: fullConf, + } + + err := originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + _, _, err := testutils.CmdAdd(targetNS.Path(), args.ContainerID, IFNAME, fullConf, func() error { + return cmdAdd(args) + }) + Expect(err).NotTo(HaveOccurred()) + validateFullRuleset(fullConf) + + err = testutils.CmdDel(targetNS.Path(), args.ContainerID, IFNAME, func() error { + return cmdDel(args) + }) + Expect(err).NotTo(HaveOccurred()) + validateCleanedUp(fullConf) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("installs the right iptables rules on the host v4.0.x and check is successful", func() { + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: targetNS.Path(), + IfName: IFNAME, + StdinData: fullConf, + } + + err := originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + _, _, err := testutils.CmdAdd(targetNS.Path(), args.ContainerID, IFNAME, fullConf, func() error { + return cmdAdd(args) + }) + Expect(err).NotTo(HaveOccurred()) + + validateFullRuleset(fullConf) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("cleans up on delete v4.0.x", func() { + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: targetNS.Path(), + IfName: IFNAME, + StdinData: fullConf, + } + + err := originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + _, _, err := testutils.CmdAdd(targetNS.Path(), args.ContainerID, IFNAME, fullConf, func() error { + return cmdAdd(args) + }) + Expect(err).NotTo(HaveOccurred()) + validateFullRuleset(fullConf) + + err = testutils.CmdDel(targetNS.Path(), args.ContainerID, IFNAME, func() error { + return cmdDel(args) + }) + Expect(err).NotTo(HaveOccurred()) + validateCleanedUp(fullConf) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + }) +}) + +var _ = Describe("firewall plugin iptables backend v0.4.x", func() { + var originalNS, targetNS ns.NetNS + const IFNAME string = "dummy0" + + fullConf := []byte(`{ + "name": "test", + "type": "firewall", + "backend": "iptables", + "ifName": "dummy0", + "cniVersion": "0.4.0", + "prevResult": { + "interfaces": [ + {"name": "dummy0"} + ], + "ips": [ + { + "version": "4", + "address": "10.0.0.2/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()) + 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 iptables rules, Check rules then cleans up on delete using v4.0.x", func() { + args := &skel.CmdArgs{ + ContainerID: "dummy", + 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()) + validateFullRuleset(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/firewall_suite_test.go b/plugins/meta/firewall/firewall_suite_test.go new file mode 100644 index 00000000..d3b10e21 --- /dev/null +++ b/plugins/meta/firewall/firewall_suite_test.go @@ -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 TestFirewall(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "firewall Suite") +} diff --git a/plugins/meta/firewall/firewalld.go b/plugins/meta/firewall/firewalld.go new file mode 100644 index 00000000..ac9328c3 --- /dev/null +++ b/plugins/meta/firewall/firewalld.go @@ -0,0 +1,122 @@ +// 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" + "strings" + + "github.com/containernetworking/cni/pkg/types/current" + "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" + firewalldQuerySourceMethod = "querySource" + + 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(conf *FirewallNetConf) (FirewallBackend, error) { + conn, err := getConn() + if err != nil { + return nil, err + } + + backend := &fwdBackend{ + conn: conn, + } + return backend, nil +} + +func (fb *fwdBackend) Add(conf *FirewallNetConf, result *current.Result) error { + for _, ip := range result.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, conf.FirewalldZone, 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, conf.FirewalldZone, err) + } + } + } + return nil +} + +func (fb *fwdBackend) Del(conf *FirewallNetConf, result *current.Result) error { + for _, ip := range result.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, conf.FirewalldZone, ipStr).Store(&res) + } + return nil +} + +func (fb *fwdBackend) Check(conf *FirewallNetConf, result *current.Result) error { + for _, ip := range result.IPs { + ipStr := ipString(ip.Address) + // Check for a firewalld rule for the given source IP to the given zone + firewalldObj := fb.conn.Object(firewalldName, firewalldPath) + var res bool + if err := firewalldObj.Call(firewalldZoneInterface+"."+firewalldQuerySourceMethod, 0, conf.FirewalldZone, ipStr).Store(&res); err != nil { + return fmt.Errorf("failed to find the address %v in %v zone", ipStr, conf.FirewalldZone) + } + } + return nil +} diff --git a/plugins/meta/firewall/iptables.go b/plugins/meta/firewall/iptables.go new file mode 100644 index 00000000..faae35c6 --- /dev/null +++ b/plugins/meta/firewall/iptables.go @@ -0,0 +1,279 @@ +// 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 ( + "fmt" + "net" + + "github.com/containernetworking/cni/pkg/types/current" + "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, result *current.Result, ipt *iptables.IPTables, proto iptables.Protocol) error { + rules := make([][]string, 0) + for _, ip := range result.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, result *current.Result, ipt *iptables.IPTables, proto iptables.Protocol) error { + rules := make([][]string, 0) + for _, ip := range result.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 (ib *iptablesBackend) checkRules(conf *FirewallNetConf, result *current.Result, ipt *iptables.IPTables, proto iptables.Protocol) error { + rules := make([][]string, 0) + for _, ip := range result.IPs { + if protoForIP(ip.Address) == proto { + rules = append(rules, getPrivChainRules(ipString(ip.Address))...) + } + } + + if len(rules) <= 0 { + return nil + } + + // 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 + privRule := generateFilterRule(ib.privChainName) + privExists, err := ipt.Exists("filter", "FORWARD", privRule...) + if err != nil { + return err + } + if !privExists { + return fmt.Errorf("expected %v rule %v not found", "FORWARD", privRule) + } + + // Ensure our admin override chain rule exists in our private chain + adminRule := generateFilterRule(ib.adminChainName) + adminExists, err := ipt.Exists("filter", ib.privChainName, adminRule...) + if err != nil { + return err + } + if !adminExists { + return fmt.Errorf("expected %v rule %v not found", ib.privChainName, adminRule) + } + + // ensure rules for this IP address exist + for _, rule := range rules { + // Ensure our rule exists in our private chain + exists, err := ipt.Exists("filter", ib.privChainName, rule...) + if err != nil { + return err + } + if !exists { + return fmt.Errorf("expected rule %v not found", rule) + } + } + + 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{} + result, _ := current.NewResultFromResult(conf.PrevResult) + for _, addr := range result.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(conf *FirewallNetConf) (FirewallBackend, error) { + adminChainName := conf.IptablesAdminChainName + if adminChainName == "" { + 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, result *current.Result) error { + for proto, ipt := range ib.protos { + if err := ib.addRules(conf, result, ipt, proto); err != nil { + return err + } + } + return nil +} + +func (ib *iptablesBackend) Del(conf *FirewallNetConf, result *current.Result) error { + for proto, ipt := range ib.protos { + ib.delRules(conf, result, ipt, proto) + } + return nil +} + +func (ib *iptablesBackend) Check(conf *FirewallNetConf, result *current.Result) error { + for proto, ipt := range ib.protos { + if err := ib.checkRules(conf, result, ipt, proto); err != nil { + return err + } + } + return nil +} diff --git a/plugins/meta/flannel/flannel.go b/plugins/meta/flannel/flannel.go index d0004237..876d7171 100644 --- a/plugins/meta/flannel/flannel.go +++ b/plugins/meta/flannel/flannel.go @@ -34,6 +34,8 @@ import ( "github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/version" + + bv "github.com/containernetworking/plugins/pkg/utils/buildversion" ) const ( @@ -217,7 +219,7 @@ func cmdDel(args *skel.CmdArgs) error { } func main() { - skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.All, "TODO") + skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.All, bv.BuildString("flannel")) } func cmdGet(args *skel.CmdArgs) error { diff --git a/plugins/meta/portmap/chain.go b/plugins/meta/portmap/chain.go index 3d8dd2a1..bca8214a 100644 --- a/plugins/meta/portmap/chain.go +++ b/plugins/meta/portmap/chain.go @@ -29,6 +29,8 @@ type chain struct { entryRules [][]string // the rules that "point" to this chain rules [][]string // the rules this chain contains + + prependEntry bool // whether or not the entry rules should be prepended } // setup idempotently creates the chain. It will not error if the chain exists. @@ -45,19 +47,19 @@ func (c *chain) setup(ipt *iptables.IPTables) error { } // Add the rules to the chain - for i := len(c.rules) - 1; i >= 0; i-- { - if err := prependUnique(ipt, c.table, c.name, c.rules[i]); err != nil { + for _, rule := range c.rules { + if err := insertUnique(ipt, c.table, c.name, false, rule); err != nil { return err } } // Add the entry rules to the entry chains for _, entryChain := range c.entryChains { - for i := len(c.entryRules) - 1; i >= 0; i-- { + for _, rule := range c.entryRules { r := []string{} - r = append(r, c.entryRules[i]...) + r = append(r, rule...) r = append(r, "-j", c.name) - if err := prependUnique(ipt, c.table, entryChain, r); err != nil { + if err := insertUnique(ipt, c.table, entryChain, c.prependEntry, r); err != nil { return err } } @@ -105,8 +107,9 @@ func (c *chain) teardown(ipt *iptables.IPTables) error { return nil } -// prependUnique will prepend a rule to a chain, if it does not already exist -func prependUnique(ipt *iptables.IPTables, table, chain string, rule []string) error { +// insertUnique will add a rule to a chain if it does not already exist. +// By default the rule is appended, unless prepend is true. +func insertUnique(ipt *iptables.IPTables, table, chain string, prepend bool, rule []string) error { exists, err := ipt.Exists(table, chain, rule...) if err != nil { return err @@ -115,7 +118,11 @@ func prependUnique(ipt *iptables.IPTables, table, chain string, rule []string) e return nil } - return ipt.Insert(table, chain, 1, rule...) + if prepend { + return ipt.Insert(table, chain, 1, rule...) + } else { + return ipt.Append(table, chain, rule...) + } } func chainExists(ipt *iptables.IPTables, tableName, chainName string) (bool, error) { @@ -131,3 +138,44 @@ func chainExists(ipt *iptables.IPTables, tableName, chainName string) (bool, err } return false, nil } + +// check the chain. +func (c *chain) check(ipt *iptables.IPTables) error { + + exists, err := chainExists(ipt, c.table, c.name) + if err != nil { + return err + } + if !exists { + return fmt.Errorf("chain %s not found in iptables table %s", c.name, c.table) + } + + for i := len(c.rules) - 1; i >= 0; i-- { + match := checkRule(ipt, c.table, c.name, c.rules[i]) + if !match { + return fmt.Errorf("rule %s in chain %s not found in table %s", c.rules, c.name, c.table) + } + } + + for _, entryChain := range c.entryChains { + for i := len(c.entryRules) - 1; i >= 0; i-- { + r := []string{} + r = append(r, c.entryRules[i]...) + r = append(r, "-j", c.name) + matchEntryChain := checkRule(ipt, c.table, entryChain, r) + if !matchEntryChain { + return fmt.Errorf("rule %s in chain %s not found in table %s", c.entryRules, entryChain, c.table) + } + } + } + + return nil +} + +func checkRule(ipt *iptables.IPTables, table, chain string, rule []string) bool { + exists, err := ipt.Exists(table, chain, rule...) + if err != nil { + return false + } + return exists +} diff --git a/plugins/meta/portmap/chain_test.go b/plugins/meta/portmap/chain_test.go index c9571736..93e4be13 100644 --- a/plugins/meta/portmap/chain_test.go +++ b/plugins/meta/portmap/chain_test.go @@ -117,8 +117,8 @@ var _ = Describe("chain tests", func() { Expect(err).NotTo(HaveOccurred()) Expect(haveRules).To(Equal([]string{ "-N " + tlChainName, - "-A " + tlChainName + " -d 203.0.113.1/32 -j " + testChain.name, "-A " + tlChainName + ` -m comment --comment "canary value" -j ACCEPT`, + "-A " + tlChainName + " -d 203.0.113.1/32 -j " + testChain.name, })) // Check that the chain and rule was created diff --git a/plugins/meta/portmap/main.go b/plugins/meta/portmap/main.go index 2f44bc95..1df147d9 100644 --- a/plugins/meta/portmap/main.go +++ b/plugins/meta/portmap/main.go @@ -34,6 +34,8 @@ import ( "github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/cni/pkg/version" + + bv "github.com/containernetworking/plugins/pkg/utils/buildversion" ) // PortMapEntry corresponds to a single entry in the port_mappings argument, @@ -55,8 +57,6 @@ type PortMapConf struct { RuntimeConfig struct { PortMaps []PortMapEntry `json:"portMappings,omitempty"` } `json:"runtimeConfig,omitempty"` - RawPrevResult map[string]interface{} `json:"prevResult,omitempty"` - PrevResult *current.Result `json:"-"` // These are fields parsed out of the config or the environment; // included here for convenience @@ -70,7 +70,7 @@ type PortMapConf struct { const DefaultMarkBit = 13 func cmdAdd(args *skel.CmdArgs) error { - netConf, err := parseConfig(args.StdinData, args.IfName) + netConf, _, err := parseConfig(args.StdinData, args.IfName) if err != nil { return fmt.Errorf("failed to parse config: %v", err) } @@ -102,7 +102,7 @@ func cmdAdd(args *skel.CmdArgs) error { } func cmdDel(args *skel.CmdArgs) error { - netConf, err := parseConfig(args.StdinData, args.IfName) + netConf, _, err := parseConfig(args.StdinData, args.IfName) if err != nil { return fmt.Errorf("failed to parse config: %v", err) } @@ -118,37 +118,60 @@ func cmdDel(args *skel.CmdArgs) error { } func main() { - // TODO: implement plugin version - skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.All, "TODO") + skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("portmap")) } -func cmdGet(args *skel.CmdArgs) error { - // TODO: implement - return fmt.Errorf("not implemented") +func cmdCheck(args *skel.CmdArgs) error { + conf, result, err := parseConfig(args.StdinData, args.IfName) + if err != nil { + return err + } + + // Ensure we have previous result. + if result == nil { + return fmt.Errorf("Required prevResult missing") + } + + if len(conf.RuntimeConfig.PortMaps) == 0 { + return nil + } + + conf.ContainerID = args.ContainerID + + if conf.ContIPv4 != nil { + if err := checkPorts(conf, conf.ContIPv4); err != nil { + return err + } + } + + if conf.ContIPv6 != nil { + if err := checkPorts(conf, conf.ContIPv6); err != nil { + return err + } + } + + return nil } // parseConfig parses the supplied configuration (and prevResult) from stdin. -func parseConfig(stdin []byte, ifName string) (*PortMapConf, error) { +func parseConfig(stdin []byte, ifName string) (*PortMapConf, *current.Result, error) { conf := PortMapConf{} if err := json.Unmarshal(stdin, &conf); err != nil { - return nil, fmt.Errorf("failed to parse network configuration: %v", err) + return nil, nil, fmt.Errorf("failed to parse network configuration: %v", err) } // Parse previous result. + var result *current.Result if conf.RawPrevResult != nil { - resultBytes, err := json.Marshal(conf.RawPrevResult) - if err != nil { - return nil, fmt.Errorf("could not serialize prevResult: %v", err) + var err error + if err = version.ParsePrevResult(&conf.NetConf); err != nil { + return nil, nil, fmt.Errorf("could not parse prevResult: %v", err) } - res, err := version.NewResult(conf.CNIVersion, resultBytes) + + result, err = current.NewResultFromResult(conf.PrevResult) 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 nil, nil, fmt.Errorf("could not convert result to current version: %v", err) } } @@ -158,7 +181,7 @@ func parseConfig(stdin []byte, ifName string) (*PortMapConf, error) { } if conf.MarkMasqBit != nil && conf.ExternalSetMarkChain != nil { - return nil, fmt.Errorf("Cannot specify externalSetMarkChain and markMasqBit") + return nil, nil, fmt.Errorf("Cannot specify externalSetMarkChain and markMasqBit") } if conf.MarkMasqBit == nil { @@ -167,21 +190,21 @@ func parseConfig(stdin []byte, ifName string) (*PortMapConf, error) { } if *conf.MarkMasqBit < 0 || *conf.MarkMasqBit > 31 { - return nil, fmt.Errorf("MasqMarkBit must be between 0 and 31") + return nil, nil, fmt.Errorf("MasqMarkBit must be between 0 and 31") } // Reject invalid port numbers for _, pm := range conf.RuntimeConfig.PortMaps { if pm.ContainerPort <= 0 { - return nil, fmt.Errorf("Invalid container port number: %d", pm.ContainerPort) + return nil, nil, fmt.Errorf("Invalid container port number: %d", pm.ContainerPort) } if pm.HostPort <= 0 { - return nil, fmt.Errorf("Invalid host port number: %d", pm.HostPort) + return nil, nil, fmt.Errorf("Invalid host port number: %d", pm.HostPort) } } if conf.PrevResult != nil { - for _, ip := range conf.PrevResult.IPs { + for _, ip := range result.IPs { if ip.Version == "6" && conf.ContIPv6 != nil { continue } else if ip.Version == "4" && conf.ContIPv4 != nil { @@ -192,9 +215,9 @@ func parseConfig(stdin []byte, ifName string) (*PortMapConf, error) { if ip.Interface != nil { intIdx := *ip.Interface if intIdx >= 0 && - intIdx < len(conf.PrevResult.Interfaces) && - (conf.PrevResult.Interfaces[intIdx].Name != ifName || - conf.PrevResult.Interfaces[intIdx].Sandbox == "") { + intIdx < len(result.Interfaces) && + (result.Interfaces[intIdx].Name != ifName || + result.Interfaces[intIdx].Sandbox == "") { continue } } @@ -207,5 +230,5 @@ func parseConfig(stdin []byte, ifName string) (*PortMapConf, error) { } } - return &conf, nil + return &conf, result, nil } diff --git a/plugins/meta/portmap/portmap.go b/plugins/meta/portmap/portmap.go index 870552f6..d73dda20 100644 --- a/plugins/meta/portmap/portmap.go +++ b/plugins/meta/portmap/portmap.go @@ -111,6 +111,46 @@ func forwardPorts(config *PortMapConf, containerIP net.IP) error { return nil } +func checkPorts(config *PortMapConf, containerIP net.IP) error { + + dnatChain := genDnatChain(config.Name, config.ContainerID) + fillDnatRules(&dnatChain, config, containerIP) + + ip4t := maybeGetIptables(false) + ip6t := maybeGetIptables(true) + if ip4t == nil && ip6t == nil { + return fmt.Errorf("neither iptables nor ip6tables usable") + } + + if ip4t != nil { + exists, err := chainExists(ip4t, dnatChain.table, dnatChain.name) + if err != nil { + return err + } + if !exists { + return err + } + if err := dnatChain.check(ip4t); err != nil { + return fmt.Errorf("could not check ipv4 dnat: %v", err) + } + } + + if ip6t != nil { + exists, err := chainExists(ip6t, dnatChain.table, dnatChain.name) + if err != nil { + return err + } + if !exists { + return err + } + if err := dnatChain.check(ip6t); err != nil { + return fmt.Errorf("could not check ipv6 dnat: %v", err) + } + } + + return nil +} + // genToplevelDnatChain creates the top-level summary chain that we'll // add our chain to. This is easy, because creating chains is idempotent. // IMPORTANT: do not change this, or else upgrading plugins will require @@ -255,6 +295,10 @@ func genMarkMasqChain(markBit int) chain { table: "nat", name: MarkMasqChainName, entryChains: []string{"POSTROUTING"}, + // Only this entry chain needs to be prepended, because otherwise it is + // stomped on by the masquerading rules created by the CNI ptp and bridge + // plugins. + prependEntry: true, entryRules: [][]string{{ "-m", "comment", "--comment", "CNI portfwd requiring masquerade", diff --git a/plugins/meta/portmap/portmap_integ_test.go b/plugins/meta/portmap/portmap_integ_test.go index efdb47a6..ce4eebef 100644 --- a/plugins/meta/portmap/portmap_integ_test.go +++ b/plugins/meta/portmap/portmap_integ_test.go @@ -165,6 +165,12 @@ var _ = Describe("portmap integration tests", func() { fmt.Fprintf(GinkgoWriter, "hostIP: %s:%d, contIP: %s:%d\n", hostIP, hostPort, contIP, containerPort) + // dump iptables-save output for debugging + cmd = exec.Command("iptables-save") + cmd.Stderr = GinkgoWriter + cmd.Stdout = GinkgoWriter + Expect(cmd.Run()).To(Succeed()) + // Sanity check: verify that the container is reachable directly contOK := testEchoServer(contIP.String(), containerPort, "") diff --git a/plugins/meta/portmap/portmap_test.go b/plugins/meta/portmap/portmap_test.go index d47b483c..8b04bf78 100644 --- a/plugins/meta/portmap/portmap_test.go +++ b/plugins/meta/portmap/portmap_test.go @@ -68,7 +68,7 @@ var _ = Describe("portmapping configuration", func() { ] } }`) - c, err := parseConfig(configBytes, "container") + c, _, err := parseConfig(configBytes, "container") Expect(err).NotTo(HaveOccurred()) Expect(c.CNIVersion).To(Equal("0.3.1")) Expect(c.ConditionsV4).To(Equal(&[]string{"a", "b"})) @@ -91,7 +91,7 @@ var _ = Describe("portmapping configuration", func() { "conditionsV4": ["a", "b"], "conditionsV6": ["c", "d"] }`) - c, err := parseConfig(configBytes, "container") + c, _, err := parseConfig(configBytes, "container") Expect(err).NotTo(HaveOccurred()) Expect(c.CNIVersion).To(Equal("0.3.1")) Expect(c.ConditionsV4).To(Equal(&[]string{"a", "b"})) @@ -115,7 +115,7 @@ var _ = Describe("portmapping configuration", func() { ] } }`) - _, err := parseConfig(configBytes, "container") + _, _, err := parseConfig(configBytes, "container") Expect(err).To(MatchError("Invalid host port number: 0")) }) @@ -143,7 +143,7 @@ var _ = Describe("portmapping configuration", func() { ] } }`) - _, err := parseConfig(configBytes, "container") + _, _, err := parseConfig(configBytes, "container") Expect(err).NotTo(HaveOccurred()) }) }) @@ -175,7 +175,7 @@ var _ = Describe("portmapping configuration", func() { "conditionsV6": ["c", "d"] }`) - conf, err := parseConfig(configBytes, "foo") + conf, _, err := parseConfig(configBytes, "foo") Expect(err).NotTo(HaveOccurred()) conf.ContainerID = containerID @@ -271,7 +271,7 @@ var _ = Describe("portmapping configuration", func() { "conditionsV6": ["c", "d"] }`) - conf, err := parseConfig(configBytes, "foo") + conf, _, err := parseConfig(configBytes, "foo") Expect(err).NotTo(HaveOccurred()) conf.ContainerID = containerID @@ -323,6 +323,7 @@ var _ = Describe("portmapping configuration", func() { "--mark", "0x20/0x20", "-j", "MASQUERADE", }}, + prependEntry: true, })) }) }) diff --git a/plugins/meta/sbr/main.go b/plugins/meta/sbr/main.go index c121dadc..325229a2 100644 --- a/plugins/meta/sbr/main.go +++ b/plugins/meta/sbr/main.go @@ -22,13 +22,15 @@ import ( "net" "github.com/alexflint/go-filemutex" + "github.com/vishvananda/netlink" + "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/containernetworking/plugins/pkg/ns" + bv "github.com/containernetworking/plugins/pkg/utils/buildversion" ) const firstTableID = 100 @@ -370,7 +372,7 @@ RULE_LOOP: } func main() { - skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.All, "TODO") + skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.All, bv.BuildString("sbr")) } func cmdGet(args *skel.CmdArgs) error { diff --git a/plugins/meta/tuning/tuning.go b/plugins/meta/tuning/tuning.go index 156a4b73..f8d0f3ad 100644 --- a/plugins/meta/tuning/tuning.go +++ b/plugins/meta/tuning/tuning.go @@ -25,23 +25,24 @@ import ( "path/filepath" "strings" + "github.com/vishvananda/netlink" + "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" + bv "github.com/containernetworking/plugins/pkg/utils/buildversion" ) // TuningConf represents the network tuning configuration. type TuningConf struct { types.NetConf - SysCtl map[string]string `json:"sysctl"` - RawPrevResult map[string]interface{} `json:"prevResult,omitempty"` - PrevResult *current.Result `json:"-"` - Mac string `json:"mac,omitempty"` - Promisc bool `json:"promisc,omitempty"` - Mtu int `json:"mtu,omitempty"` + SysCtl map[string]string `json:"sysctl"` + Mac string `json:"mac,omitempty"` + Promisc bool `json:"promisc,omitempty"` + Mtu int `json:"mtu,omitempty"` } type MACEnvArgs struct { @@ -68,23 +69,6 @@ func parseConf(data []byte, envArgs string) (*TuningConf, error) { } } - // 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 } @@ -111,7 +95,15 @@ func changeMacAddr(ifName string, newMacAddr string) error { } func updateResultsMacAddr(config TuningConf, ifName string, newMacAddr string) { - for _, i := range config.PrevResult.Interfaces { + // Parse previous result. + if config.PrevResult == nil { + return + } + + version.ParsePrevResult(&config.NetConf) + result, _ := current.NewResultFromResult(config.PrevResult) + + for _, i := range result.Interfaces { if i.Name == ifName { i.Mac = newMacAddr } @@ -144,6 +136,20 @@ func cmdAdd(args *skel.CmdArgs) error { return err } + // Parse previous result. + if tuningConf.RawPrevResult == nil { + return fmt.Errorf("Required prevResult missing") + } + + if err := version.ParsePrevResult(&tuningConf.NetConf); err != nil { + return err + } + + _, err = current.NewResultFromResult(tuningConf.PrevResult) + if err != nil { + return err + } + // The directory /proc/sys/net is per network namespace. Enter in the // network namespace before writing on it. @@ -199,11 +205,80 @@ func cmdDel(args *skel.CmdArgs) error { } func main() { - // TODO: implement plugin version - skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.All, "TODO") + skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("tuning")) } -func cmdGet(args *skel.CmdArgs) error { - // TODO: implement - return fmt.Errorf("not implemented") +func cmdCheck(args *skel.CmdArgs) error { + tuningConf, err := parseConf(args.StdinData, args.Args) + if err != nil { + return err + } + + // Parse previous result. + if tuningConf.RawPrevResult == nil { + return fmt.Errorf("Required prevResult missing") + } + + if err := version.ParsePrevResult(&tuningConf.NetConf); err != nil { + return err + } + + _, err = current.NewResultFromResult(tuningConf.PrevResult) + if err != nil { + return err + } + + err = ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error { + // Check each configured value vs what's currently in the container + for key, conf_value := range tuningConf.SysCtl { + fileName := filepath.Join("/proc/sys", strings.Replace(key, ".", "/", -1)) + fileName = filepath.Clean(fileName) + + contents, err := ioutil.ReadFile(fileName) + if err != nil { + return err + } + cur_value := strings.TrimSuffix(string(contents), "\n") + if conf_value != cur_value { + return fmt.Errorf("Error: Tuning configured value of %s is %s, current value is %s", fileName, conf_value, cur_value) + } + } + + link, err := netlink.LinkByName(args.IfName) + if err != nil { + return fmt.Errorf("Cannot find container link %v", args.IfName) + } + + if tuningConf.Mac != "" { + if tuningConf.Mac != link.Attrs().HardwareAddr.String() { + return fmt.Errorf("Error: Tuning configured Ethernet of %s is %s, current value is %s", + args.IfName, tuningConf.Mac, link.Attrs().HardwareAddr) + } + } + + if tuningConf.Promisc { + if link.Attrs().Promisc == 0 { + return fmt.Errorf("Error: Tuning link %s configured promisc is %v, current value is %d", + args.IfName, tuningConf.Promisc, link.Attrs().Promisc) + } + } else { + if link.Attrs().Promisc != 0 { + return fmt.Errorf("Error: Tuning link %s configured promisc is %v, current value is %d", + args.IfName, tuningConf.Promisc, link.Attrs().Promisc) + } + } + + if tuningConf.Mtu != 0 { + if tuningConf.Mtu != link.Attrs().MTU { + return fmt.Errorf("Error: Tuning configured MTU of %s is %d, current value is %d", + args.IfName, tuningConf.Mtu, link.Attrs().MTU) + } + } + return nil + }) + if err != nil { + return err + } + + return nil } diff --git a/plugins/meta/tuning/tuning_test.go b/plugins/meta/tuning/tuning_test.go index 1b68bf15..87dd2619 100644 --- a/plugins/meta/tuning/tuning_test.go +++ b/plugins/meta/tuning/tuning_test.go @@ -15,7 +15,11 @@ package main import ( + "encoding/json" + "fmt" + "github.com/containernetworking/cni/pkg/skel" + "github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/plugins/pkg/ns" "github.com/containernetworking/plugins/pkg/testutils" @@ -27,6 +31,49 @@ import ( . "github.com/onsi/gomega" ) +func buildOneConfig(name, cniVersion string, orig *TuningConf, prevResult types.Result) (*TuningConf, []byte, error) { + var err error + + inject := map[string]interface{}{ + "name": name, + "cniVersion": cniVersion, + } + // Add previous plugin result + if prevResult != nil { + inject["prevResult"] = prevResult + } + + // Ensure every config uses the same name and version + config := make(map[string]interface{}) + + confBytes, err := json.Marshal(orig) + if err != nil { + return nil, nil, err + } + + err = json.Unmarshal(confBytes, &config) + if err != nil { + return nil, nil, fmt.Errorf("unmarshal existing network bytes: %s", err) + } + + for key, value := range inject { + config[key] = value + } + + newBytes, err := json.Marshal(config) + if err != nil { + return nil, nil, err + } + + conf := &TuningConf{} + if err := json.Unmarshal(newBytes, &conf); err != nil { + return nil, nil, fmt.Errorf("error parsing configuration: %s", err) + } + + return conf, newBytes, nil + +} + var _ = Describe("tuning plugin", func() { var originalNS ns.NetNS const IFNAME string = "dummy0" @@ -342,4 +389,296 @@ var _ = Describe("tuning plugin", func() { }) Expect(err).NotTo(HaveOccurred()) }) + + It("configures and deconfigures promiscuous mode with CNI 0.4.0 ADD/DEL", func() { + conf := []byte(`{ + "name": "test", + "type": "iplink", + "cniVersion": "0.4.0", + "promisc": true, + "prevResult": { + "interfaces": [ + {"name": "dummy0", "sandbox":"netns"} + ], + "ips": [ + { + "version": "4", + "address": "10.0.0.2/24", + "gateway": "10.0.0.1", + "interface": 0 + } + ] + } +}`) + + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: originalNS.Path(), + IfName: IFNAME, + StdinData: conf, + } + + err := originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + r, _, err := testutils.CmdAddWithArgs(args, func() error { + return cmdAdd(args) + }) + Expect(err).NotTo(HaveOccurred()) + + result, err := current.GetResult(r) + Expect(err).NotTo(HaveOccurred()) + + Expect(len(result.Interfaces)).To(Equal(1)) + Expect(result.Interfaces[0].Name).To(Equal(IFNAME)) + Expect(len(result.IPs)).To(Equal(1)) + Expect(result.IPs[0].Address.String()).To(Equal("10.0.0.2/24")) + + link, err := netlink.LinkByName(IFNAME) + Expect(err).NotTo(HaveOccurred()) + Expect(link.Attrs().Promisc).To(Equal(1)) + + n := &TuningConf{} + err = json.Unmarshal([]byte(conf), &n) + Expect(err).NotTo(HaveOccurred()) + + cniVersion := "0.4.0" + _, confString, err := buildOneConfig("testConfig", cniVersion, n, r) + Expect(err).NotTo(HaveOccurred()) + + args.StdinData = confString + + err = testutils.CmdCheckWithArgs(args, func() error { + return cmdCheck(args) + }) + Expect(err).NotTo(HaveOccurred()) + + err = testutils.CmdDel(originalNS.Path(), + args.ContainerID, "", func() error { return cmdDel(args) }) + Expect(err).NotTo(HaveOccurred()) + + return nil + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("configures and deconfigures mtu with CNI 0.4.0 ADD/DEL", func() { + conf := []byte(`{ + "name": "test", + "type": "iplink", + "cniVersion": "0.4.0", + "mtu": 1454, + "prevResult": { + "interfaces": [ + {"name": "dummy0", "sandbox":"netns"} + ], + "ips": [ + { + "version": "4", + "address": "10.0.0.2/24", + "gateway": "10.0.0.1", + "interface": 0 + } + ] + } +}`) + + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: originalNS.Path(), + IfName: IFNAME, + StdinData: conf, + } + + err := originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + r, _, err := testutils.CmdAddWithArgs(args, func() error { + return cmdAdd(args) + }) + Expect(err).NotTo(HaveOccurred()) + + result, err := current.GetResult(r) + Expect(err).NotTo(HaveOccurred()) + + Expect(len(result.Interfaces)).To(Equal(1)) + Expect(result.Interfaces[0].Name).To(Equal(IFNAME)) + Expect(len(result.IPs)).To(Equal(1)) + Expect(result.IPs[0].Address.String()).To(Equal("10.0.0.2/24")) + + link, err := netlink.LinkByName(IFNAME) + Expect(err).NotTo(HaveOccurred()) + Expect(link.Attrs().MTU).To(Equal(1454)) + + n := &TuningConf{} + err = json.Unmarshal([]byte(conf), &n) + Expect(err).NotTo(HaveOccurred()) + + cniVersion := "0.4.0" + _, confString, err := buildOneConfig("testConfig", cniVersion, n, r) + Expect(err).NotTo(HaveOccurred()) + + args.StdinData = confString + + err = testutils.CmdCheckWithArgs(args, func() error { + return cmdCheck(args) + }) + Expect(err).NotTo(HaveOccurred()) + + err = testutils.CmdDel(originalNS.Path(), + args.ContainerID, "", func() error { return cmdDel(args) }) + Expect(err).NotTo(HaveOccurred()) + + return nil + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("configures and deconfigures mac address (from conf file) with CNI v4.0 ADD/DEL", func() { + conf := []byte(`{ + "name": "test", + "type": "iplink", + "cniVersion": "0.4.0", + "mac": "c2:11:22:33:44:55", + "prevResult": { + "interfaces": [ + {"name": "dummy0", "sandbox":"netns"} + ], + "ips": [ + { + "version": "4", + "address": "10.0.0.2/24", + "gateway": "10.0.0.1", + "interface": 0 + } + ] + } +}`) + + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: originalNS.Path(), + IfName: IFNAME, + StdinData: conf, + } + + err := originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + r, _, err := testutils.CmdAddWithArgs(args, func() error { + return cmdAdd(args) + }) + Expect(err).NotTo(HaveOccurred()) + + result, err := current.GetResult(r) + Expect(err).NotTo(HaveOccurred()) + + Expect(len(result.Interfaces)).To(Equal(1)) + Expect(result.Interfaces[0].Name).To(Equal(IFNAME)) + Expect(len(result.IPs)).To(Equal(1)) + Expect(result.IPs[0].Address.String()).To(Equal("10.0.0.2/24")) + + link, err := netlink.LinkByName(IFNAME) + Expect(err).NotTo(HaveOccurred()) + hw, err := net.ParseMAC("c2:11:22:33:44:55") + Expect(err).NotTo(HaveOccurred()) + Expect(link.Attrs().HardwareAddr).To(Equal(hw)) + + n := &TuningConf{} + err = json.Unmarshal([]byte(conf), &n) + Expect(err).NotTo(HaveOccurred()) + + cniVersion := "0.4.0" + _, confString, err := buildOneConfig("testConfig", cniVersion, n, r) + Expect(err).NotTo(HaveOccurred()) + + args.StdinData = confString + + err = testutils.CmdCheckWithArgs(args, func() error { + return cmdCheck(args) + }) + Expect(err).NotTo(HaveOccurred()) + + err = testutils.CmdDel(originalNS.Path(), + args.ContainerID, "", func() error { return cmdDel(args) }) + Expect(err).NotTo(HaveOccurred()) + + return nil + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("configures and deconfigures mac address (from CNI_ARGS) with CNI v4 ADD/DEL", func() { + conf := []byte(`{ + "name": "test", + "type": "iplink", + "cniVersion": "0.4.0", + "prevResult": { + "interfaces": [ + {"name": "dummy0", "sandbox":"netns"} + ], + "ips": [ + { + "version": "4", + "address": "10.0.0.2/24", + "gateway": "10.0.0.1", + "interface": 0 + } + ] + } +}`) + + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: originalNS.Path(), + IfName: IFNAME, + StdinData: conf, + Args: "IgnoreUnknown=true;MAC=c2:11:22:33:44:66", + } + + err := originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + r, _, err := testutils.CmdAddWithArgs(args, func() error { + return cmdAdd(args) + }) + Expect(err).NotTo(HaveOccurred()) + + result, err := current.GetResult(r) + Expect(err).NotTo(HaveOccurred()) + + Expect(len(result.Interfaces)).To(Equal(1)) + Expect(result.Interfaces[0].Name).To(Equal(IFNAME)) + Expect(len(result.IPs)).To(Equal(1)) + Expect(result.IPs[0].Address.String()).To(Equal("10.0.0.2/24")) + + link, err := netlink.LinkByName(IFNAME) + Expect(err).NotTo(HaveOccurred()) + hw, err := net.ParseMAC("c2:11:22:33:44:66") + Expect(err).NotTo(HaveOccurred()) + Expect(link.Attrs().HardwareAddr).To(Equal(hw)) + + n := &TuningConf{} + err = json.Unmarshal([]byte(conf), &n) + Expect(err).NotTo(HaveOccurred()) + + cniVersion := "0.4.0" + _, confString, err := buildOneConfig("testConfig", cniVersion, n, r) + Expect(err).NotTo(HaveOccurred()) + + args.StdinData = confString + + err = testutils.CmdCheckWithArgs(args, func() error { + return cmdCheck(args) + }) + Expect(err).NotTo(HaveOccurred()) + + err = testutils.CmdDel(originalNS.Path(), + args.ContainerID, "", func() error { return cmdDel(args) }) + Expect(err).NotTo(HaveOccurred()) + + return nil + }) + Expect(err).NotTo(HaveOccurred()) + }) }) diff --git a/plugins/sample/main.go b/plugins/sample/main.go index 65676270..2fcef412 100644 --- a/plugins/sample/main.go +++ b/plugins/sample/main.go @@ -25,6 +25,8 @@ import ( "github.com/containernetworking/cni/pkg/types" "github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/cni/pkg/version" + + bv "github.com/containernetworking/plugins/pkg/utils/buildversion" ) // PluginConf is whatever you expect your configuration json to be. This is whatever @@ -92,13 +94,22 @@ func cmdAdd(args *skel.CmdArgs) error { return err } + // Remove this if this is an "originating" plugin if conf.PrevResult == nil { return fmt.Errorf("must be called as chained plugin") } + // Uncomment if this is an "originating" plugin + + //if conf.PrevResult != nil { + // return fmt.Errorf("must be called as the first plugin") + // } + // This is some sample code to generate the list of container-side IPs. // We're casting the prevResult to a 0.3.0 response, which can also include // host-side IPs (but doesn't when converted from a 0.2.0 response). + // + // You don't need this if you are writing an "originating" plugin. containerIPs := make([]net.IP, 0, len(conf.PrevResult.IPs)) if conf.CNIVersion != "0.3.0" { for _, ip := range conf.PrevResult.IPs { @@ -123,6 +134,8 @@ func cmdAdd(args *skel.CmdArgs) error { return fmt.Errorf("got no container IPs") } + // Implement your plugin here + // Pass through the result for the next plugin return types.PrintResult(conf.PrevResult, conf.CNIVersion) } @@ -141,11 +154,11 @@ func cmdDel(args *skel.CmdArgs) error { } func main() { - // TODO: implement plugin version - skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.All, "TODO") + // replace TODO with your plugin name + skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("TODO")) } -func cmdGet(args *skel.CmdArgs) error { +func cmdCheck(args *skel.CmdArgs) error { // TODO: implement return fmt.Errorf("not implemented") } diff --git a/scripts/release.sh b/scripts/release.sh index bd59ba0e..5b9d40d1 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -2,11 +2,12 @@ set -xe SRC_DIR="${SRC_DIR:-$PWD}" +DOCKER="${DOCKER:-docker}" TAG=$(git describe --tags --dirty) RELEASE_DIR=release-${TAG} -BUILDFLAGS="-ldflags '-extldflags -static -X main._buildVersion=${TAG}'" +BUILDFLAGS="-ldflags '-extldflags -static -X github.com/containernetworking/plugins/pkg/utils/buildversion.BuildVersion=${TAG}'" OUTPUT_DIR=bin @@ -15,7 +16,7 @@ rm -Rf ${SRC_DIR}/${RELEASE_DIR} mkdir -p ${SRC_DIR}/${RELEASE_DIR} mkdir -p ${OUTPUT_DIR} -docker run -v ${SRC_DIR}:/go/src/github.com/containernetworking/plugins --rm golang:1.10-alpine \ +$DOCKER run -v ${SRC_DIR}:/go/src/github.com/containernetworking/plugins --rm golang:1.10-alpine \ /bin/sh -xe -c "\ apk --no-cache add bash tar; cd /go/src/github.com/containernetworking/plugins; umask 0022; diff --git a/test_linux.sh b/test_linux.sh index c5e4d6fa..858534b3 100755 --- a/test_linux.sh +++ b/test_linux.sh @@ -6,35 +6,53 @@ # set -e +# switch into the repo root directory +cd "$(dirname $0)" + +# Build all plugins before testing source ./build_linux.sh echo "Running tests" -GINKGO_FLAGS="-p --randomizeAllSpecs --randomizeSuites --failOnPending --progress --skipPackage=pkg/hns" +function testrun { + sudo -E bash -c "umask 0; cd ${GOPATH}/src; PATH=${GOROOT}/bin:$(pwd)/bin:${PATH} go test $@" +} -# user has not provided PKG override -if [ -z "$PKG" ]; then - GINKGO_FLAGS="$GINKGO_FLAGS -r ." - LINT_TARGETS="./..." +COVERALLS=${COVERALLS:-""} -# user has provided PKG override +if [ -n "${COVERALLS}" ]; then + echo "with coverage profile generation..." else - GINKGO_FLAGS="$GINKGO_FLAGS $PKG" - LINT_TARGETS="$PKG" + echo "without coverage profile generation..." fi -sudo -E bash -c "umask 0; cd ${GOPATH}/src/${REPO_PATH}; PATH=${GOROOT}/bin:$(pwd)/bin:${PATH} ginkgo ${GINKGO_FLAGS}" +PKG=${PKG:-$(cd ${GOPATH}/src/${REPO_PATH}; go list ./... | xargs echo)} + +# coverage profile only works per-package +i=0 +for t in ${PKG}; do + if [ -n "${COVERALLS}" ]; then + COVERFLAGS="-covermode set -coverprofile ${i}.coverprofile" + fi + testrun "${COVERFLAGS:-""} ${t}" + i=$((i+1)) +done + +# Submit coverage information +if [ -n "${COVERALLS}" ]; then + gover + goveralls -service=travis-ci -coverprofile=gover.coverprofile +fi -cd ${GOPATH}/src/${REPO_PATH}; echo "Checking gofmt..." -fmtRes=$(go fmt $LINT_TARGETS) +fmtRes=$(go fmt $PKG) if [ -n "${fmtRes}" ]; then echo -e "go fmt checking failed:\n${fmtRes}" exit 255 fi echo "Checking govet..." -vetRes=$(go vet $LINT_TARGETS) +vetRes=$(go vet $PKG) if [ -n "${vetRes}" ]; then echo -e "govet checking failed:\n${vetRes}" exit 255 diff --git a/vendor/github.com/containernetworking/cni/libcni/api.go b/vendor/github.com/containernetworking/cni/libcni/api.go index dd3e1680..360733e7 100644 --- a/vendor/github.com/containernetworking/cni/libcni/api.go +++ b/vendor/github.com/containernetworking/cni/libcni/api.go @@ -126,7 +126,7 @@ func buildOneConfig(name, cniVersion string, orig *NetworkConfig, prevResult typ // These capabilities arguments are filtered through the plugin's advertised // capabilities from its config JSON, and any keys in the CapabilityArgs // matching plugin capabilities are added to the "runtimeConfig" dictionary -// sent to the plugin via JSON on stdin. For exmaple, if the plugin's +// sent to the plugin via JSON on stdin. For example, if the plugin's // capabilities include "portMappings", and the CapabilityArgs map includes a // "portMappings" key, that key and its value are added to the "runtimeConfig" // dictionary to be passed to the plugin's stdin. diff --git a/vendor/github.com/containernetworking/cni/pkg/invoke/os_unix.go b/vendor/github.com/containernetworking/cni/pkg/invoke/os_unix.go index bab5737a..9bcfb455 100644 --- a/vendor/github.com/containernetworking/cni/pkg/invoke/os_unix.go +++ b/vendor/github.com/containernetworking/cni/pkg/invoke/os_unix.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -// +build darwin dragonfly freebsd linux netbsd opensbd solaris +// +build darwin dragonfly freebsd linux netbsd openbsd solaris package invoke diff --git a/vendor/github.com/containernetworking/cni/pkg/skel/skel.go b/vendor/github.com/containernetworking/cni/pkg/skel/skel.go index d6062a11..af56b8a1 100644 --- a/vendor/github.com/containernetworking/cni/pkg/skel/skel.go +++ b/vendor/github.com/containernetworking/cni/pkg/skel/skel.go @@ -291,7 +291,7 @@ func PluginMainWithError(cmdAdd, cmdCheck, cmdDel func(_ *CmdArgs) error, versio // The caller must also specify what CNI spec versions the plugin supports. // // The caller can specify an "about" string, which is printed on stderr -// when no CNI_COMMAND is specified. The reccomended output is "CNI plugin v" +// when no CNI_COMMAND is specified. The recommended output is "CNI plugin v" // // When an error occurs in either cmdAdd, cmdCheck, or cmdDel, PluginMain will print the error // as JSON to stdout and call os.Exit(1). diff --git a/vendor/github.com/containernetworking/cni/pkg/types/020/types.go b/vendor/github.com/containernetworking/cni/pkg/types/020/types.go index 2833aba7..53256167 100644 --- a/vendor/github.com/containernetworking/cni/pkg/types/020/types.go +++ b/vendor/github.com/containernetworking/cni/pkg/types/020/types.go @@ -17,6 +17,7 @@ package types020 import ( "encoding/json" "fmt" + "io" "net" "os" @@ -73,11 +74,15 @@ func (r *Result) GetAsVersion(version string) (types.Result, error) { } func (r *Result) Print() error { + return r.PrintTo(os.Stdout) +} + +func (r *Result) PrintTo(writer io.Writer) error { data, err := json.MarshalIndent(r, "", " ") if err != nil { return err } - _, err = os.Stdout.Write(data) + _, err = writer.Write(data) return err } diff --git a/vendor/github.com/containernetworking/cni/pkg/types/current/types.go b/vendor/github.com/containernetworking/cni/pkg/types/current/types.go index 71a634fe..7267a2e6 100644 --- a/vendor/github.com/containernetworking/cni/pkg/types/current/types.go +++ b/vendor/github.com/containernetworking/cni/pkg/types/current/types.go @@ -17,6 +17,7 @@ package current import ( "encoding/json" "fmt" + "io" "net" "os" @@ -75,13 +76,9 @@ func convertFrom020(result types.Result) (*Result, error) { Gateway: oldResult.IP4.Gateway, }) for _, route := range oldResult.IP4.Routes { - gw := route.GW - if gw == nil { - gw = oldResult.IP4.Gateway - } newResult.Routes = append(newResult.Routes, &types.Route{ Dst: route.Dst, - GW: gw, + GW: route.GW, }) } } @@ -93,13 +90,9 @@ func convertFrom020(result types.Result) (*Result, error) { Gateway: oldResult.IP6.Gateway, }) for _, route := range oldResult.IP6.Routes { - gw := route.GW - if gw == nil { - gw = oldResult.IP6.Gateway - } newResult.Routes = append(newResult.Routes, &types.Route{ Dst: route.Dst, - GW: gw, + GW: route.GW, }) } } @@ -202,11 +195,15 @@ func (r *Result) GetAsVersion(version string) (types.Result, error) { } func (r *Result) Print() error { + return r.PrintTo(os.Stdout) +} + +func (r *Result) PrintTo(writer io.Writer) error { data, err := json.MarshalIndent(r, "", " ") if err != nil { return err } - _, err = os.Stdout.Write(data) + _, err = writer.Write(data) return err } diff --git a/vendor/github.com/containernetworking/cni/pkg/types/types.go b/vendor/github.com/containernetworking/cni/pkg/types/types.go index 5e49f117..d0d11006 100644 --- a/vendor/github.com/containernetworking/cni/pkg/types/types.go +++ b/vendor/github.com/containernetworking/cni/pkg/types/types.go @@ -18,6 +18,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "net" "os" ) @@ -87,7 +88,7 @@ type ResultFactoryFunc func([]byte) (Result, error) // Result is an interface that provides the result of plugin execution type Result interface { - // The highest CNI specification result verison the result supports + // The highest CNI specification result version the result supports // without having to convert Version() string @@ -98,6 +99,9 @@ type Result interface { // Prints the result in JSON format to stdout Print() error + // Prints the result in JSON format to provided writer + PrintTo(writer io.Writer) error + // Returns a JSON string representation of the result String() string } diff --git a/vendor/github.com/containernetworking/cni/pkg/version/plugin.go b/vendor/github.com/containernetworking/cni/pkg/version/plugin.go index 612335a8..1df42724 100644 --- a/vendor/github.com/containernetworking/cni/pkg/version/plugin.go +++ b/vendor/github.com/containernetworking/cni/pkg/version/plugin.go @@ -86,9 +86,13 @@ func (*PluginDecoder) Decode(jsonBytes []byte) (PluginInfo, error) { // minor, and micro numbers or returns an error func ParseVersion(version string) (int, int, int, error) { var major, minor, micro int + if version == "" { + return -1, -1, -1, fmt.Errorf("invalid version %q: the version is empty", version) + } + parts := strings.Split(version, ".") - if len(parts) == 0 || len(parts) >= 4 { - return -1, -1, -1, fmt.Errorf("invalid version %q: too many or too few parts", version) + if len(parts) >= 4 { + return -1, -1, -1, fmt.Errorf("invalid version %q: too many parts", version) } major, err := strconv.Atoi(parts[0]) @@ -114,7 +118,7 @@ func ParseVersion(version string) (int, int, int, error) { } // GreaterThanOrEqualTo takes two string versions, parses them into major/minor/micro -// nubmers, and compares them to determine whether the first version is greater +// numbers, and compares them to determine whether the first version is greater // than or equal to the second func GreaterThanOrEqualTo(version, otherVersion string) (bool, error) { firstMajor, firstMinor, firstMicro, err := ParseVersion(version) diff --git a/vendor/github.com/godbus/dbus/.travis.yml b/vendor/github.com/godbus/dbus/.travis.yml new file mode 100644 index 00000000..2e1bbb78 --- /dev/null +++ b/vendor/github.com/godbus/dbus/.travis.yml @@ -0,0 +1,40 @@ +dist: precise +language: go +go_import_path: github.com/godbus/dbus +sudo: true + +go: + - 1.6.3 + - 1.7.3 + - tip + +env: + global: + matrix: + - TARGET=amd64 + - TARGET=arm64 + - TARGET=arm + - TARGET=386 + - TARGET=ppc64le + +matrix: + fast_finish: true + allow_failures: + - go: tip + exclude: + - go: tip + env: TARGET=arm + - go: tip + env: TARGET=arm64 + - go: tip + env: TARGET=386 + - go: tip + env: TARGET=ppc64le + +addons: + apt: + packages: + - dbus + - dbus-x11 + +before_install: diff --git a/vendor/github.com/godbus/dbus/CONTRIBUTING.md b/vendor/github.com/godbus/dbus/CONTRIBUTING.md new file mode 100644 index 00000000..c88f9b2b --- /dev/null +++ b/vendor/github.com/godbus/dbus/CONTRIBUTING.md @@ -0,0 +1,50 @@ +# How to Contribute + +## Getting Started + +- Fork the repository on GitHub +- Read the [README](README.markdown) for build and test instructions +- Play with the project, submit bugs, submit patches! + +## Contribution Flow + +This is a rough outline of what a contributor's workflow looks like: + +- Create a topic branch from where you want to base your work (usually master). +- Make commits of logical units. +- Make sure your commit messages are in the proper format (see below). +- Push your changes to a topic branch in your fork of the repository. +- Make sure the tests pass, and add any new tests as appropriate. +- Submit a pull request to the original repository. + +Thanks for your contributions! + +### Format of the Commit Message + +We follow a rough convention for commit messages that is designed to answer two +questions: what changed and why. The subject line should feature the what and +the body of the commit should describe the why. + +``` +scripts: add the test-cluster command + +this uses tmux to setup a test cluster that you can easily kill and +start for debugging. + +Fixes #38 +``` + +The format can be described more formally as follows: + +``` +: + + + +