From c24708ff62bb54f209b1e8b10d2f826ceed0060b Mon Sep 17 00:00:00 2001 From: Eugene Yakubovich Date: Wed, 15 Apr 2015 15:35:02 -0700 Subject: [PATCH] Add plugin code This adds basic plugins. "main" types: veth, bridge, macvlan "ipam" type: host-local The code has been ported over from github.com/coreos/rkt project and adapted to fit the CNI spec. --- ip/cidr.go | 86 ++++++++++++++++++++++++++++++ ip/ipmasq.go | 66 +++++++++++++++++++++++ ip/link.go | 117 +++++++++++++++++++++++++++++++++++++++++ ip/route.go | 47 +++++++++++++++++ ns/ns.go | 81 ++++++++++++++++++++++++++++ plugin/ipam.go | 136 ++++++++++++++++++++++++++++++++++++++++++++++++ plugin/types.go | 106 +++++++++++++++++++++++++++++++++++++ skel/skel.go | 98 ++++++++++++++++++++++++++++++++++ 8 files changed, 737 insertions(+) create mode 100644 ip/cidr.go create mode 100644 ip/ipmasq.go create mode 100644 ip/link.go create mode 100644 ip/route.go create mode 100644 ns/ns.go create mode 100644 plugin/ipam.go create mode 100644 plugin/types.go create mode 100644 skel/skel.go diff --git a/ip/cidr.go b/ip/cidr.go new file mode 100644 index 00000000..c9633988 --- /dev/null +++ b/ip/cidr.go @@ -0,0 +1,86 @@ +// Copyright 2015 CoreOS, Inc. +// +// 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 ( + "encoding/json" + "math/big" + "net" +) + +// ParseCIDR takes a string like "10.2.3.1/24" and +// return IPNet with "10.2.3.1" and /24 mask +func ParseCIDR(s string) (*net.IPNet, error) { + ip, ipn, err := net.ParseCIDR(s) + if err != nil { + return nil, err + } + + ipn.IP = ip + return ipn, nil +} + +// NextIP returns IP incremented by 1 +func NextIP(ip net.IP) net.IP { + i := ipToInt(ip) + return intToIP(i.Add(i, big.NewInt(1))) +} + +// PrevIP returns IP decremented by 1 +func PrevIP(ip net.IP) net.IP { + i := ipToInt(ip) + return intToIP(i.Sub(i, big.NewInt(1))) +} + +func ipToInt(ip net.IP) *big.Int { + if v := ip.To4(); v != nil { + return big.NewInt(0).SetBytes(v) + } + return big.NewInt(0).SetBytes(ip.To16()) +} + +func intToIP(i *big.Int) net.IP { + return net.IP(i.Bytes()) +} + +// Network masks off the host portion of the IP +func Network(ipn *net.IPNet) *net.IPNet { + return &net.IPNet{ + IP: ipn.IP.Mask(ipn.Mask), + Mask: ipn.Mask, + } +} + +// like net.IPNet but adds JSON marshalling and unmarshalling +type IPNet net.IPNet + +func (n IPNet) MarshalJSON() ([]byte, error) { + return json.Marshal((*net.IPNet)(&n).String()) +} + +func (n *IPNet) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + + tmp, err := ParseCIDR(s) + if err != nil { + return err + } + + *n = IPNet(*tmp) + return nil +} diff --git a/ip/ipmasq.go b/ip/ipmasq.go new file mode 100644 index 00000000..665189bc --- /dev/null +++ b/ip/ipmasq.go @@ -0,0 +1,66 @@ +// Copyright 2015 CoreOS, Inc. +// +// 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/appc/cni/Godeps/_workspace/src/github.com/coreos/go-iptables/iptables" +) + +// SetupIPMasq installs iptables rules to masquerade traffic +// coming from ipn and going outside of it +func SetupIPMasq(ipn *net.IPNet, chain string) error { + ipt, err := iptables.New() + if err != nil { + return fmt.Errorf("failed to locate iptabes: %v", err) + } + + if err = ipt.NewChain("nat", chain); err != nil { + if err.(*iptables.Error).ExitStatus() != 1 { + // TODO(eyakubovich): assumes exit status 1 implies chain exists + return err + } + } + + if err = ipt.AppendUnique("nat", chain, "-d", ipn.String(), "-j", "ACCEPT"); err != nil { + return err + } + + if err = ipt.AppendUnique("nat", chain, "!", "-d", "224.0.0.0/4", "-j", "MASQUERADE"); err != nil { + return err + } + + return ipt.AppendUnique("nat", "POSTROUTING", "-s", ipn.String(), "-j", chain) +} + +// TeardownIPMasq undoes the effects of SetupIPMasq +func TeardownIPMasq(ipn *net.IPNet, chain string) error { + ipt, err := iptables.New() + if err != nil { + return fmt.Errorf("failed to locate iptabes: %v", err) + } + + if err = ipt.Delete("nat", "POSTROUTING", "-s", ipn.String(), "-j", chain); err != nil { + return err + } + + if err = ipt.ClearChain("nat", chain); err != nil { + return err + } + + return ipt.DeleteChain("nat", chain) +} diff --git a/ip/link.go b/ip/link.go new file mode 100644 index 00000000..59865cf8 --- /dev/null +++ b/ip/link.go @@ -0,0 +1,117 @@ +// Copyright 2015 CoreOS, Inc. +// +// 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 ( + "crypto/sha512" + "fmt" + "net" + "os" + + "github.com/appc/cni/Godeps/_workspace/src/github.com/vishvananda/netlink" +) + +func makeVeth(name, peer string, mtu int) (netlink.Link, error) { + veth := &netlink.Veth{ + LinkAttrs: netlink.LinkAttrs{ + Name: name, + Flags: net.FlagUp, + MTU: mtu, + }, + PeerName: peer, + } + if err := netlink.LinkAdd(veth); err != nil { + return nil, err + } + + return veth, nil +} + +// RandomVethName returns string "veth" with random prefix (hashed from entropy) +func RandomVethName(entropy string) string { + h := sha512.New() + h.Write([]byte(entropy)) + return fmt.Sprintf("veth%x", h.Sum(nil)[:5]) +} + +// SetupVeth sets up a virtual ethernet link. +// Should be in container netns. +// TODO(eyakubovich): get rid of entropy and ask kernel to pick name via pattern +func SetupVeth(entropy, contVethName string, mtu int, hostNS *os.File) (hostVeth, contVeth netlink.Link, err error) { + // NetworkManager (recent versions) will ignore veth devices that start with "veth" + hostVethName := RandomVethName(entropy) + hostVeth, err = makeVeth(hostVethName, contVethName, mtu) + if err != nil { + err = fmt.Errorf("failed to make veth pair: %v", err) + return + } + + if err = netlink.LinkSetUp(hostVeth); err != nil { + err = fmt.Errorf("failed to set %q up: %v", hostVethName, err) + return + } + + contVeth, err = netlink.LinkByName(contVethName) + if err != nil { + err = fmt.Errorf("failed to lookup %q: %v", contVethName, err) + return + } + + if err = netlink.LinkSetUp(contVeth); err != nil { + err = fmt.Errorf("failed to set %q up: %v", contVethName, err) + return + } + + if err = netlink.LinkSetNsFd(hostVeth, int(hostNS.Fd())); err != nil { + err = fmt.Errorf("failed to move veth to host netns: %v", err) + return + } + + return +} + +// DelLinkByName removes an interface link. +func DelLinkByName(ifName string) error { + iface, err := netlink.LinkByName(ifName) + if err != nil { + return fmt.Errorf("failed to lookup %q: %v", ifName, err) + } + + if err = netlink.LinkDel(iface); err != nil { + return fmt.Errorf("failed to delete %q: %v", ifName, err) + } + + return nil +} + +// DelLinkByNameAddr remove an interface returns its IP address +// of the specified family +func DelLinkByNameAddr(ifName string, family int) (*net.IPNet, error) { + iface, err := netlink.LinkByName(ifName) + if err != nil { + return nil, fmt.Errorf("failed to lookup %q: %v", ifName, err) + } + + addrs, err := netlink.AddrList(iface, family) + if err != nil || len(addrs) == 0 { + return nil, fmt.Errorf("failed to get IP addresses for %q: %v", ifName, err) + } + + if err = netlink.LinkDel(iface); err != nil { + return nil, fmt.Errorf("failed to delete %q: %v", ifName, err) + } + + return addrs[0].IPNet, nil +} diff --git a/ip/route.go b/ip/route.go new file mode 100644 index 00000000..f310f1e3 --- /dev/null +++ b/ip/route.go @@ -0,0 +1,47 @@ +// Copyright 2015 CoreOS, Inc. +// +// 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 ( + "net" + + "github.com/appc/cni/Godeps/_workspace/src/github.com/vishvananda/netlink" +) + +// AddDefaultRoute sets the default route on the given gateway. +func AddDefaultRoute(gw net.IP, dev netlink.Link) error { + _, defNet, _ := net.ParseCIDR("0.0.0.0/0") + return AddRoute(defNet, gw, dev) +} + +// AddRoute adds a universally-scoped route to a device. +func AddRoute(ipn *net.IPNet, gw net.IP, dev netlink.Link) error { + return netlink.RouteAdd(&netlink.Route{ + LinkIndex: dev.Attrs().Index, + Scope: netlink.SCOPE_UNIVERSE, + Dst: ipn, + Gw: gw, + }) +} + +// AddHostRoute adds a host-scoped route to a device. +func AddHostRoute(ipn *net.IPNet, gw net.IP, dev netlink.Link) error { + return netlink.RouteAdd(&netlink.Route{ + LinkIndex: dev.Attrs().Index, + Scope: netlink.SCOPE_HOST, + Dst: ipn, + Gw: gw, + }) +} diff --git a/ns/ns.go b/ns/ns.go new file mode 100644 index 00000000..82291f98 --- /dev/null +++ b/ns/ns.go @@ -0,0 +1,81 @@ +// Copyright 2015 CoreOS, Inc. +// +// 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 ns + +import ( + "fmt" + "os" + "runtime" + "syscall" +) + +var setNsMap = map[string]uintptr{ + "386": 346, + "amd64": 308, + "arm": 374, +} + +// SetNS sets the network namespace on a target file. +func SetNS(f *os.File, flags uintptr) error { + if runtime.GOOS != "linux" { + return fmt.Errorf("unsupported OS: %s", runtime.GOOS) + } + + trap, ok := setNsMap[runtime.GOARCH] + if !ok { + return fmt.Errorf("unsupported arch: %s", runtime.GOARCH) + } + + _, _, err := syscall.RawSyscall(trap, f.Fd(), flags, 0) + if err != 0 { + return err + } + + return nil +} + +// WithNetNSPath executes the passed closure under the given network +// namespace, restoring the original namespace afterwards. +func WithNetNSPath(nspath string, f func(*os.File) error) error { + ns, err := os.Open(nspath) + if err != nil { + return fmt.Errorf("Failed to open %v: %v", nspath, err) + } + defer ns.Close() + + return WithNetNS(ns, f) +} + +// WithNetNS executes the passed closure under the given network +// namespace, restoring the original namespace afterwards. +func WithNetNS(ns *os.File, f func(*os.File) error) error { + // save a handle to current (host) network namespace + thisNS, err := os.Open("/proc/self/ns/net") + if err != nil { + return fmt.Errorf("Failed to open /proc/self/ns/net: %v", err) + } + defer thisNS.Close() + + if err = SetNS(ns, syscall.CLONE_NEWNET); err != nil { + return fmt.Errorf("Error switching to ns %v: %v", ns.Name(), err) + } + + if err = f(thisNS); err != nil { + return err + } + + // switch back + return SetNS(thisNS, syscall.CLONE_NEWNET) +} diff --git a/plugin/ipam.go b/plugin/ipam.go new file mode 100644 index 00000000..8b59cab7 --- /dev/null +++ b/plugin/ipam.go @@ -0,0 +1,136 @@ +// Copyright 2015 CoreOS, Inc. +// +// 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 plugin + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/appc/cni/Godeps/_workspace/src/github.com/vishvananda/netlink" + "github.com/appc/cni/pkg/ip" +) + +// Find returns the full path of the plugin by searching in CNI_PATH +func Find(plugin string) string { + paths := strings.Split(os.Getenv("CNI_PATH"), ":") + + for _, p := range paths { + fullname := filepath.Join(p, plugin) + if fi, err := os.Stat(fullname); err == nil && fi.Mode().IsRegular() { + return fullname + } + } + + return "" +} + +// ExecAdd executes IPAM plugin, assuming CNI_COMMAND == ADD. +// Parses and returns resulting IPConfig +func ExecAdd(plugin string, netconf []byte) (*Result, error) { + if os.Getenv("CNI_COMMAND") != "ADD" { + return nil, fmt.Errorf("CNI_COMMAND is not ADD") + } + + pluginPath := Find(plugin) + if pluginPath == "" { + return nil, fmt.Errorf("could not find %q plugin", plugin) + } + + stdout := &bytes.Buffer{} + + c := exec.Cmd{ + Path: pluginPath, + Args: []string{pluginPath}, + Stdin: bytes.NewBuffer(netconf), + Stdout: stdout, + Stderr: os.Stderr, + } + if err := c.Run(); err != nil { + return nil, err + } + + res := &Result{} + err := json.Unmarshal(stdout.Bytes(), res) + return res, err +} + +// ExecDel executes IPAM plugin, assuming CNI_COMMAND == DEL. +func ExecDel(plugin string, netconf []byte) error { + if os.Getenv("CNI_COMMAND") != "DEL" { + return fmt.Errorf("CNI_COMMAND is not DEL") + } + + pluginPath := Find(plugin) + if pluginPath == "" { + return fmt.Errorf("could not find %q plugin", plugin) + } + + c := exec.Cmd{ + Path: pluginPath, + Args: []string{pluginPath}, + Stdin: bytes.NewBuffer(netconf), + Stderr: os.Stderr, + } + return c.Run() +} + +// ConfigureIface takes the result of IPAM plugin and +// applies to the ifName interface +func ConfigureIface(ifName string, res *Result) error { + link, err := netlink.LinkByName(ifName) + if err != nil { + return fmt.Errorf("failed to lookup %q: %v", ifName, err) + } + + if err := netlink.LinkSetUp(link); err != nil { + return fmt.Errorf("failed too set %q UP: %v", ifName, err) + } + + // TODO(eyakubovich): IPv6 + addr := &netlink.Addr{IPNet: &res.IP4.IP, Label: ""} + if err = netlink.AddrAdd(link, addr); err != nil { + return fmt.Errorf("failed to add IP addr to %q: %v", ifName, err) + } + + for _, r := range res.IP4.Routes { + gw := r.GW + if gw == nil { + gw = res.IP4.Gateway + } + if err = ip.AddRoute(&r.Dst, gw, link); err != nil { + // we skip over duplicate routes as we assume the first one wins + if !os.IsExist(err) { + return fmt.Errorf("failed to add route '%v via %v dev %v': %v", r.Dst, gw, ifName, err) + } + } + } + + return nil +} + +// PrintResult writes out prettified Result to stdout +func PrintResult(res *Result) error { + data, err := json.MarshalIndent(res, "", " ") + if err != nil { + return err + } + _, err = os.Stdout.Write(data) + return err +} diff --git a/plugin/types.go b/plugin/types.go new file mode 100644 index 00000000..6eb6ac21 --- /dev/null +++ b/plugin/types.go @@ -0,0 +1,106 @@ +// Copyright 2015 CoreOS, Inc. +// +// 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 plugin + +import ( + "encoding/json" + "net" + + "github.com/appc/cni/pkg/ip" +) + +// NetConf describes a network. +type NetConf struct { + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + IPAM struct { + Type string `json:"type,omitempty"` + } `json:"ipam,omitempty"` +} + +// Result is what gets returned from the plugin (via stdout) to the caller +type Result struct { + IP4 *IPConfig `json:"ip4,omitempty"` + IP6 *IPConfig `json:"ip6,omitempty"` +} + +// IPConfig contains values necessary to configure an interface +type IPConfig struct { + IP net.IPNet + Gateway net.IP + Routes []Route +} + +type Route struct { + Dst net.IPNet + GW net.IP +} + +// net.IPNet is not JSON (un)marshallable so this duality is needed +// for our custom ip.IPNet type + +// JSON (un)marshallable types +type ipConfig struct { + IP ip.IPNet `json:"ip"` + Gateway net.IP `json:"gateway,omitempty"` + Routes []Route `json:"routes,omitempty"` +} + +type route struct { + Dst ip.IPNet `json:"dst"` + GW net.IP `json:"gw,omitempty"` +} + +func (c *IPConfig) MarshalJSON() ([]byte, error) { + ipc := ipConfig{ + IP: ip.IPNet(c.IP), + Gateway: c.Gateway, + Routes: c.Routes, + } + + return json.Marshal(ipc) +} + +func (c *IPConfig) UnmarshalJSON(data []byte) error { + ipc := ipConfig{} + if err := json.Unmarshal(data, &ipc); err != nil { + return err + } + + c.IP = net.IPNet(ipc.IP) + c.Gateway = ipc.Gateway + c.Routes = ipc.Routes + return nil +} + +func (r *Route) UnmarshalJSON(data []byte) error { + rt := route{} + if err := json.Unmarshal(data, &rt); err != nil { + return err + } + + r.Dst = net.IPNet(rt.Dst) + r.GW = rt.GW + return nil +} + +func (r *Route) MarshalJSON() ([]byte, error) { + rt := route{ + Dst: ip.IPNet(r.Dst), + GW: r.GW, + } + + return json.Marshal(rt) +} diff --git a/skel/skel.go b/skel/skel.go new file mode 100644 index 00000000..9f033350 --- /dev/null +++ b/skel/skel.go @@ -0,0 +1,98 @@ +// Copyright 2014 CoreOS, Inc. +// +// 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 skel provides skeleton code for a CNI plugin. +// In particular, it implements argument parsing and validation. +package skel + +import ( + "io/ioutil" + "log" + "os" +) + +// CmdArgs captures all the arguments passed in to the plugin +// via both env vars and stdin +type CmdArgs struct { + ContainerID string + Netns string + IfName string + Args string + Path string + StdinData []byte +} + +// PluginMain is the "main" for a plugin. It accepts +// two callback functions for add and del commands. +func PluginMain(cmdAdd, cmdDel func(_ *CmdArgs) error) { + var cmd, contID, netns, ifName, args, path string + + vars := []struct { + name string + val *string + req bool + }{ + {"CNI_COMMAND", &cmd, true}, + {"CNI_CONTAINERID", &contID, false}, + {"CNI_NETNS", &netns, true}, + {"CNI_IFNAME", &ifName, true}, + {"CNI_ARGS", &args, false}, + {"CNI_PATH", &path, true}, + } + + argsMissing := false + for _, v := range vars { + *v.val = os.Getenv(v.name) + if v.req && *v.val == "" { + log.Printf("%v env variable missing", v.name) + argsMissing = true + } + } + + if argsMissing { + os.Exit(1) + } + + stdinData, err := ioutil.ReadAll(os.Stdin) + if err != nil { + log.Printf("Error reading from stdin: %v", err) + os.Exit(1) + } + + cmdArgs := &CmdArgs{ + ContainerID: contID, + Netns: netns, + IfName: ifName, + Args: args, + Path: path, + StdinData: stdinData, + } + + switch cmd { + case "ADD": + err = cmdAdd(cmdArgs) + + case "DEL": + err = cmdDel(cmdArgs) + + default: + log.Printf("Unknown CNI_COMMAND: %v", cmd) + os.Exit(1) + } + + if err != nil { + log.Printf("%v: %v", cmd, err) + os.Exit(1) + } +}