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.
--- /dev/null
+// 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
+}
--- /dev/null
+// 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)
+}
--- /dev/null
+// 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
+}
--- /dev/null
+// 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,
+ })
+}
--- /dev/null
+// 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)
+}
--- /dev/null
+// 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
+}
--- /dev/null
+// 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)
+}
--- /dev/null
+// 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)
+ }
+}