--- /dev/null
+# vz-local IP address management plugin (stolen from host-local; needed until Mesos supports chained plugins)
+
+host-local IPAM allocates IPv4 and IPv6 addresses out of a specified address range. Optionally,
+it can include a DNS configuration from a `resolv.conf` file on the host.
+
+## Overview
+
+host-local IPAM plugin allocates ip addresses out of a set of address ranges.
+It stores the state locally on the host filesystem, therefore ensuring uniqueness of IP addresses on a single host.
+
+## Example configurations
+
+```json
+{
+ "ipam": {
+ "type": "host-local",
+ "ranges": [
+ {
+ "subnet": "10.10.0.0/16",
+ "rangeStart": "10.10.1.20",
+ "rangeEnd": "10.10.3.50",
+ "gateway": "10.10.0.254"
+ },
+ {
+ "subnet": "3ffe:ffff:0:01ff::/64",
+ "rangeStart": "3ffe:ffff:0:01ff::0010",
+ "rangeEnd": "3ffe:ffff:0:01ff::0020"
+ }
+ ],
+ "routes": [
+ { "dst": "0.0.0.0/0" },
+ { "dst": "192.168.0.0/16", "gw": "10.10.5.1" },
+ { "dst": "3ffe:ffff:0:01ff::1/64" }
+ ],
+ "dataDir": "/run/my-orchestrator/container-ipam-state"
+ }
+}
+```
+
+Previous versions of the `host-local` allocator did not support the `ranges`
+property, and instead expected a single range on the top level. This is
+deprecated but still supported.
+```json
+{
+ "ipam": {
+ "type": "host-local",
+ "subnet": "3ffe:ffff:0:01ff::/64",
+ "rangeStart": "3ffe:ffff:0:01ff::0010",
+ "rangeEnd": "3ffe:ffff:0:01ff::0020",
+ "routes": [
+ { "dst": "3ffe:ffff:0:01ff::1/64" }
+ ],
+ "resolvConf": "/etc/resolv.conf"
+ }
+}
+```
+
+We can test it out on the command-line:
+
+```bash
+$ echo '{ "cniVersion": "0.3.1", "name": "examplenet", "ipam": { "type": "host-local", "ranges": [ {"subnet": "203.0.113.0/24"}, {"subnet": "2001:db8:1::/64"}], "dataDir": "/tmp/cni-example" } }' | CNI_COMMAND=ADD CNI_CONTAINERID=example CNI_NETNS=/dev/null CNI_IFNAME=dummy0 CNI_PATH=. ./host-local
+
+```
+
+```json
+{
+ "ips": [
+ {
+ "version": "4",
+ "address": "203.0.113.2/24",
+ "gateway": "203.0.113.1"
+ },
+ {
+ "version": "6",
+ "address": "2001:db8:1::2/64",
+ "gateway": "2001:db8:1::1"
+ }
+ ],
+ "dns": {}
+}
+```
+
+## Network configuration reference
+
+* `type` (string, required): "host-local".
+* `routes` (string, optional): list of routes to add to the container namespace. Each route is a dictionary with "dst" and optional "gw" fields. If "gw" is omitted, value of "gateway" will be used.
+* `resolvConf` (string, optional): Path to a `resolv.conf` on the host to parse and return as the DNS configuration
+* `dataDir` (string, optional): Path to a directory to use for maintaining state, e.g. which IPs have been allocated to which containers
+* `ranges`, (array, required, nonempty) an array of range objects:
+ * `subnet` (string, required): CIDR block to allocate out of.
+ * `rangeStart` (string, optional): IP inside of "subnet" from which to start allocating addresses. Defaults to ".2" IP inside of the "subnet" block.
+ * `rangeEnd` (string, optional): IP inside of "subnet" with which to end allocating addresses. Defaults to ".254" IP inside of the "subnet" block for ipv4, ".255" for IPv6
+ * `gateway` (string, optional): IP inside of "subnet" to designate as the gateway. Defaults to ".1" IP inside of the "subnet" block.
+
+Older versions of the `host-local` plugin did not support the `ranges` array. Instead,
+all the properties in the `range` object were top-level. This is still supported but deprecated.
+
+## Supported arguments
+The following [CNI_ARGS](https://github.com/containernetworking/cni/blob/master/SPEC.md#parameters) are supported:
+
+* `ip`: request a specific IP address from a subnet.
+
+The following [args conventions](https://github.com/containernetworking/cni/blob/master/CONVENTIONS.md) are supported:
+
+* `ips` (array of strings): A list of custom IPs to attempt to allocate
+
+### Custom IP allocation
+For every requested custom IP, the `host-local` allocator will request that IP
+if it falls within one of the `range` objects. Thus it is possible to specify
+multiple custom IPs and multiple ranges.
+
+If any requested IPs cannot be reserved, either because they are already in use
+or are not part of a specified range, the plugin will return an error.
+
+
+## Files
+
+Allocated IP addresses are stored as files in `/var/lib/cni/networks/$NETWORK_NAME`.
+The path can be customized with the `dataDir` option listed above. Environments
+where IPs are released automatically on reboot (e.g. running containers are not
+restored) may wish to specify `/var/run/cni` or another tmpfs mounted directory
+instead.
--- /dev/null
+// Copyright 2015 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 allocator
+
+import (
+ "encoding/base64"
+ "fmt"
+ "log"
+ "net"
+ "os"
+
+ "github.com/containernetworking/cni/pkg/types/current"
+ "github.com/containernetworking/plugins/pkg/ip"
+ "github.com/containernetworking/plugins/plugins/ipam/vz-local/backend"
+
+ //"github.com/vishvananda/netlink"
+ //"os"
+ //"syscall"
+)
+
+type IPAllocator struct {
+ netName string
+ ipRange Range
+ store backend.Store
+ rangeID string // Used for tracking last reserved ip
+}
+
+type RangeIter struct {
+ low net.IP
+ high net.IP
+ cur net.IP
+ start net.IP
+}
+
+func NewIPAllocator(netName string, r Range, store backend.Store) *IPAllocator {
+ // The range name (last allocated ip suffix) is just the base64
+ // encoding of the bytes of the first IP
+ rangeID := base64.URLEncoding.EncodeToString(r.RangeStart)
+
+ return &IPAllocator{
+ netName: netName,
+ ipRange: r,
+ store: store,
+ rangeID: rangeID,
+ }
+}
+
+// Get alocates an IP
+func (a *IPAllocator) Get(id string, requestedIP net.IP) (*current.IPConfig, error) {
+ a.store.Lock()
+ defer a.store.Unlock()
+
+ gw := a.ipRange.Gateway
+
+ var reservedIP net.IP
+
+ if requestedIP != nil {
+ if gw != nil && gw.Equal(requestedIP) {
+ return nil, fmt.Errorf("requested IP must differ from gateway IP")
+ }
+
+ log.Println("AdvRoute: RequestedIP - BGP Advertises /32", requestedIP)
+
+ if err := a.ipRange.IPInRange(requestedIP); err != nil {
+ return nil, err
+ }
+
+ reserved, err := a.store.Reserve(id, requestedIP, a.rangeID)
+ if err != nil {
+ return nil, err
+ }
+ if !reserved {
+ return nil, fmt.Errorf("requested IP address %q is not available in network: %s %s", requestedIP, a.netName, (*net.IPNet)(&a.ipRange.Subnet).String())
+ }
+ reservedIP = requestedIP
+
+ } else {
+ iter, err := a.GetIter()
+ if err != nil {
+ return nil, err
+ }
+ for {
+ cur := iter.Next()
+ if cur == nil {
+ break
+ }
+
+ // don't allocate gateway IP
+ if gw != nil && cur.Equal(gw) {
+ continue
+ }
+
+ reserved, err := a.store.Reserve(id, cur, a.rangeID)
+ if err != nil {
+ return nil, err
+ }
+
+ if reserved {
+ reservedIP = cur
+ break
+ }
+ }
+ }
+
+ if reservedIP == nil {
+ return nil, fmt.Errorf("no IP addresses available in network: %s %s", a.netName, (*net.IPNet)(&a.ipRange.Subnet).String())
+ }
+
+/*
+ var uplink types.UnmarshallableString
+ if a.conf.UPLINK != nil {
+ uplink = a.conf.UPLINK
+ log.Println("AdvRoute: Uplink - BGP Advertises /32", requestedIP, uplink)
+
+ dst := &net.IPNet{
+ IP: requestedIP,
+ Mask: net.CIDRMask(32, 32),
+ }
+
+ link, err := netlink.LinkByName(string(uplink))
+ if err != nil {
+ log.Println("Can't obtain link index for: ", uplink)
+ return nil, nil, err
+ }
+
+ viaIP := a.conf.VIAIP
+ route := netlink.Route{
+ Dst: dst,
+ LinkIndex: link.Attrs().Index,
+ Gw: viaIP,
+ }
+
+ if err := netlink.RouteAdd(&route); err != nil {
+ fmt.Fprintln(os.Stderr, "There was an error adding netlink route: ", err)
+ if (err == syscall.EAGAIN) {
+ log.Println("ERRNO: eagain")
+ } else if (err == syscall.EEXIST) {
+ log.Println("ERRNO: route already exists")
+ } else {
+ log.Println("ERRNO: value is: ", (int(err.(syscall.Errno))))
+ }
+ return nil, nil, err
+ }
+ }
+*/
+
+ version := "4"
+ if reservedIP.To4() == nil {
+ version = "6"
+ }
+
+ return ¤t.IPConfig{
+ Version: version,
+ Address: net.IPNet{IP: reservedIP, Mask: a.ipRange.Subnet.Mask},
+ Gateway: gw,
+ }, nil
+}
+
+// Release clears all IPs allocated for the container with given ID
+func (a *IPAllocator) Release(id string) error {
+ a.store.Lock()
+ defer a.store.Unlock()
+
+/*
+ var requestedIP net.IP
+ if a.conf.Args != nil {
+ requestedIP = a.conf.Args.IP
+ }
+
+ if requestedIP != nil {
+ //var uplink string
+ var uplink types.UnmarshallableString
+ if a.conf.Args != nil {
+ uplink = a.conf.Args.UPLINK
+ log.Println("AdvRoute: BGP Advertises /32", requestedIP, uplink)
+ dst := &net.IPNet{
+ // IP: net.IPv4(99, 44, 69, 3),
+ IP: requestedIP,
+ Mask: net.CIDRMask(32, 32),
+ }
+
+ link, err := netlink.LinkByName(string(uplink))
+ if err != nil {
+ log.Println("Can't obtain link index for: ", uplink)
+ return err1
+ }
+
+ viaIP := a.conf.Args.VIAIP
+ route := netlink.Route{
+ Dst: dst,
+ LinkIndex: link.Attrs().Index,
+ Gw: viaIP,
+ }
+
+ if err := netlink.RouteDel(&route); err != nil {
+ fmt.Fprintln(os.Stderr, "There was an error removing netlink route: ", err)
+ if (err == syscall.EAGAIN) {
+ log.Println("ERRNO: eagain")
+ } else if (err == syscall.ESRCH) {
+ log.Println("ERRNO: route doesn't exists")
+ } else {
+ log.Println("ERRNO: value is: ", (int(err.(syscall.Errno))))
+ }
+ // os.Exit(1)
+ return err
+ }
+ }
+*/
+
+ return a.store.ReleaseByID(id)
+}
+
+// GetIter encapsulates the strategy for this allocator.
+// We use a round-robin strategy, attempting to evenly use the whole subnet.
+// More specifically, a crash-looping container will not see the same IP until
+// the entire range has been run through.
+// We may wish to consider avoiding recently-released IPs in the future.
+func (a *IPAllocator) GetIter() (*RangeIter, error) {
+ i := RangeIter{
+ low: a.ipRange.RangeStart,
+ high: a.ipRange.RangeEnd,
+ }
+
+ // Round-robin by trying to allocate from the last reserved IP + 1
+ startFromLastReservedIP := false
+
+ // We might get a last reserved IP that is wrong if the range indexes changed.
+ // This is not critical, we just lose round-robin this one time.
+ lastReservedIP, err := a.store.LastReservedIP(a.rangeID)
+ if err != nil && !os.IsNotExist(err) {
+ log.Printf("Error retrieving last reserved ip: %v", err)
+ } else if lastReservedIP != nil {
+ startFromLastReservedIP = a.ipRange.IPInRange(lastReservedIP) == nil
+ }
+
+ if startFromLastReservedIP {
+ if i.high.Equal(lastReservedIP) {
+ i.start = i.low
+ } else {
+ i.start = ip.NextIP(lastReservedIP)
+ }
+ } else {
+ i.start = a.ipRange.RangeStart
+ }
+ return &i, nil
+}
+
+// Next returns the next IP in the iterator, or nil if end is reached
+func (i *RangeIter) Next() net.IP {
+ // If we're at the beginning, time to start
+ if i.cur == nil {
+ i.cur = i.start
+ return i.cur
+ }
+ // we returned .high last time, since we're inclusive
+ if i.cur.Equal(i.high) {
+ i.cur = i.low
+ } else {
+ i.cur = ip.NextIP(i.cur)
+ }
+
+ // If we've looped back to where we started, exit
+ if i.cur.Equal(i.start) {
+ return nil
+ }
+
+ return i.cur
+}
--- /dev/null
+// 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 allocator_test
+
+import (
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+
+ "testing"
+)
+
+func TestAllocator(t *testing.T) {
+ RegisterFailHandler(Fail)
+ RunSpecs(t, "Allocator Suite")
+}
--- /dev/null
+// 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 allocator
+
+import (
+ "fmt"
+ "net"
+
+ "github.com/containernetworking/cni/pkg/types"
+ "github.com/containernetworking/cni/pkg/types/current"
+ fakestore "github.com/containernetworking/plugins/plugins/ipam/host-local/backend/testing"
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+)
+
+type AllocatorTestCase struct {
+ subnet string
+ ipmap map[string]string
+ expectResult string
+ lastIP string
+}
+
+func mkalloc() IPAllocator {
+ ipnet, _ := types.ParseCIDR("192.168.1.0/24")
+
+ r := Range{
+ Subnet: types.IPNet(*ipnet),
+ }
+ r.Canonicalize()
+ store := fakestore.NewFakeStore(map[string]string{}, map[string]net.IP{})
+
+ alloc := IPAllocator{
+ netName: "netname",
+ ipRange: r,
+ store: store,
+ rangeID: "rangeid",
+ }
+
+ return alloc
+}
+
+func (t AllocatorTestCase) run(idx int) (*current.IPConfig, error) {
+ fmt.Fprintln(GinkgoWriter, "Index:", idx)
+ subnet, err := types.ParseCIDR(t.subnet)
+ if err != nil {
+ return nil, err
+ }
+
+ conf := Range{
+ Subnet: types.IPNet(*subnet),
+ }
+
+ Expect(conf.Canonicalize()).To(BeNil())
+
+ store := fakestore.NewFakeStore(t.ipmap, map[string]net.IP{"rangeid": net.ParseIP(t.lastIP)})
+
+ alloc := IPAllocator{
+ "netname",
+ conf,
+ store,
+ "rangeid",
+ }
+
+ return alloc.Get("ID", nil)
+}
+
+var _ = Describe("host-local ip allocator", func() {
+ Context("RangeIter", func() {
+ It("should loop correctly from the beginning", func() {
+ r := RangeIter{
+ start: net.IP{10, 0, 0, 0},
+ low: net.IP{10, 0, 0, 0},
+ high: net.IP{10, 0, 0, 5},
+ }
+ Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 0}))
+ Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 1}))
+ Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 2}))
+ Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 3}))
+ Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 4}))
+ Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 5}))
+ Expect(r.Next()).To(BeNil())
+ })
+
+ It("should loop correctly from the end", func() {
+ r := RangeIter{
+ start: net.IP{10, 0, 0, 5},
+ low: net.IP{10, 0, 0, 0},
+ high: net.IP{10, 0, 0, 5},
+ }
+ Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 5}))
+ Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 0}))
+ Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 1}))
+ Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 2}))
+ Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 3}))
+ Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 4}))
+ Expect(r.Next()).To(BeNil())
+ })
+
+ It("should loop correctly from the middle", func() {
+ r := RangeIter{
+ start: net.IP{10, 0, 0, 3},
+ low: net.IP{10, 0, 0, 0},
+ high: net.IP{10, 0, 0, 5},
+ }
+ Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 3}))
+ Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 4}))
+ Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 5}))
+ Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 0}))
+ Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 1}))
+ Expect(r.Next()).To(Equal(net.IP{10, 0, 0, 2}))
+ Expect(r.Next()).To(BeNil())
+ })
+
+ })
+
+ Context("when has free ip", func() {
+ It("should allocate ips in round robin", func() {
+ testCases := []AllocatorTestCase{
+ // fresh start
+ {
+ subnet: "10.0.0.0/29",
+ ipmap: map[string]string{},
+ expectResult: "10.0.0.2",
+ lastIP: "",
+ },
+ {
+ subnet: "2001:db8:1::0/64",
+ ipmap: map[string]string{},
+ expectResult: "2001:db8:1::2",
+ lastIP: "",
+ },
+ {
+ subnet: "10.0.0.0/30",
+ ipmap: map[string]string{},
+ expectResult: "10.0.0.2",
+ lastIP: "",
+ },
+ {
+ subnet: "10.0.0.0/29",
+ ipmap: map[string]string{
+ "10.0.0.2": "id",
+ },
+ expectResult: "10.0.0.3",
+ lastIP: "",
+ },
+ // next ip of last reserved ip
+ {
+ subnet: "10.0.0.0/29",
+ ipmap: map[string]string{},
+ expectResult: "10.0.0.6",
+ lastIP: "10.0.0.5",
+ },
+ {
+ subnet: "10.0.0.0/29",
+ ipmap: map[string]string{
+ "10.0.0.4": "id",
+ "10.0.0.5": "id",
+ },
+ expectResult: "10.0.0.6",
+ lastIP: "10.0.0.3",
+ },
+ // round robin to the beginning
+ {
+ subnet: "10.0.0.0/29",
+ ipmap: map[string]string{
+ "10.0.0.6": "id",
+ },
+ expectResult: "10.0.0.2",
+ lastIP: "10.0.0.5",
+ },
+ // lastIP is out of range
+ {
+ subnet: "10.0.0.0/29",
+ ipmap: map[string]string{
+ "10.0.0.2": "id",
+ },
+ expectResult: "10.0.0.3",
+ lastIP: "10.0.0.128",
+ },
+ // wrap around and reserve lastIP
+ {
+ subnet: "10.0.0.0/29",
+ ipmap: map[string]string{
+ "10.0.0.2": "id",
+ "10.0.0.4": "id",
+ "10.0.0.5": "id",
+ "10.0.0.6": "id",
+ },
+ expectResult: "10.0.0.3",
+ lastIP: "10.0.0.3",
+ },
+ }
+
+ for idx, tc := range testCases {
+ res, err := tc.run(idx)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(res.Address.IP.String()).To(Equal(tc.expectResult))
+ }
+ })
+
+ It("should not allocate the broadcast address", func() {
+ alloc := mkalloc()
+ for i := 2; i < 255; i++ {
+ res, err := alloc.Get("ID", nil)
+ Expect(err).ToNot(HaveOccurred())
+ s := fmt.Sprintf("192.168.1.%d/24", i)
+ Expect(s).To(Equal(res.Address.String()))
+ fmt.Fprintln(GinkgoWriter, "got ip", res.Address.String())
+ }
+
+ x, err := alloc.Get("ID", nil)
+ fmt.Fprintln(GinkgoWriter, "got ip", x)
+ Expect(err).To(HaveOccurred())
+ })
+
+ It("should allocate in a round-robin fashion", func() {
+ alloc := mkalloc()
+ res, err := alloc.Get("ID", nil)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(res.Address.String()).To(Equal("192.168.1.2/24"))
+
+ err = alloc.Release("ID")
+ Expect(err).ToNot(HaveOccurred())
+
+ res, err = alloc.Get("ID", nil)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(res.Address.String()).To(Equal("192.168.1.3/24"))
+
+ })
+
+ It("should allocate RangeStart first", func() {
+ alloc := mkalloc()
+ alloc.ipRange.RangeStart = net.IP{192, 168, 1, 10}
+ res, err := alloc.Get("ID", nil)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(res.Address.String()).To(Equal("192.168.1.10/24"))
+
+ res, err = alloc.Get("ID", nil)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(res.Address.String()).To(Equal("192.168.1.11/24"))
+ })
+
+ It("should allocate RangeEnd but not past RangeEnd", func() {
+ alloc := mkalloc()
+ alloc.ipRange.RangeEnd = net.IP{192, 168, 1, 5}
+
+ for i := 1; i < 5; i++ {
+ res, err := alloc.Get("ID", nil)
+ Expect(err).ToNot(HaveOccurred())
+ // i+1 because the gateway address is skipped
+ Expect(res.Address.String()).To(Equal(fmt.Sprintf("192.168.1.%d/24", i+1)))
+ }
+
+ _, err := alloc.Get("ID", nil)
+ Expect(err).To(HaveOccurred())
+ })
+
+ Context("when requesting a specific IP", func() {
+ It("must allocate the requested IP", func() {
+ alloc := mkalloc()
+ requestedIP := net.IP{192, 168, 1, 5}
+ res, err := alloc.Get("ID", requestedIP)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(res.Address.IP.String()).To(Equal(requestedIP.String()))
+ })
+
+ It("must fail when the requested IP is allocated", func() {
+ alloc := mkalloc()
+ requestedIP := net.IP{192, 168, 1, 5}
+ res, err := alloc.Get("ID", requestedIP)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(res.Address.IP.String()).To(Equal(requestedIP.String()))
+
+ _, err = alloc.Get("ID", requestedIP)
+ Expect(err).To(MatchError(`requested IP address "192.168.1.5" is not available in network: netname 192.168.1.0/24`))
+ })
+
+ It("must return an error when the requested IP is after RangeEnd", func() {
+ alloc := mkalloc()
+ alloc.ipRange.RangeEnd = net.IP{192, 168, 1, 5}
+ requestedIP := net.IP{192, 168, 1, 6}
+ _, err := alloc.Get("ID", requestedIP)
+ Expect(err).To(HaveOccurred())
+ })
+
+ It("must return an error when the requested IP is before RangeStart", func() {
+ alloc := mkalloc()
+ alloc.ipRange.RangeStart = net.IP{192, 168, 1, 6}
+ requestedIP := net.IP{192, 168, 1, 5}
+ _, err := alloc.Get("ID", requestedIP)
+ Expect(err).To(HaveOccurred())
+ })
+ })
+
+ })
+ Context("when out of ips", func() {
+ It("returns a meaningful error", func() {
+ testCases := []AllocatorTestCase{
+ {
+ subnet: "10.0.0.0/30",
+ ipmap: map[string]string{
+ "10.0.0.2": "id",
+ "10.0.0.3": "id",
+ },
+ },
+ {
+ subnet: "10.0.0.0/29",
+ ipmap: map[string]string{
+ "10.0.0.2": "id",
+ "10.0.0.3": "id",
+ "10.0.0.4": "id",
+ "10.0.0.5": "id",
+ "10.0.0.6": "id",
+ "10.0.0.7": "id",
+ },
+ },
+ }
+ for idx, tc := range testCases {
+ _, err := tc.run(idx)
+ Expect(err).To(MatchError("no IP addresses available in network: netname " + tc.subnet))
+ }
+ })
+ })
+})
--- /dev/null
+// Copyright 2015 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 allocator
+
+import (
+ "encoding/json"
+ "fmt"
+ "net"
+
+ "log"
+ "os"
+
+ "github.com/containernetworking/cni/pkg/types"
+ types020 "github.com/containernetworking/cni/pkg/types/020"
+)
+
+// IPAMConfig represents the IP related network configuration.
+// This nests Range because we initially only supported a single
+// range directly, and wish to preserve backwards compatability
+type IPAMConfig struct {
+ *Range
+ Name string
+ Type string `json:"type"`
+ Routes []*types.Route `json:"routes"`
+ DataDir string `json:"dataDir"`
+ ResolvConf string `json:"resolvConf"`
+ STATICIP net.IP `json:"staticip,omitempty"`
+ VIAIP net.IP `json:"viaip,omitempty"`
+ UPLINK types.UnmarshallableString `json:"uplink,omitempty"`
+ Ranges []Range `json:"ranges"`
+ IPArgs []net.IP `json:"-"` // Requested IPs from CNI_ARGS and args
+}
+
+type IPAMEnvArgs struct {
+ types.CommonArgs
+ IP net.IP `json:"ip,omitempty"`
+// VIAIP net.IP `json:"viaip,omitempty"`
+// UPLINK types.UnmarshallableString `json:"uplink,omitempty"`
+}
+
+type IPAMArgs struct {
+ IPs []net.IP `json:"ips"`
+// VIAIP net.IP `json:"viaip,omitempty"`
+// UPLINK types.UnmarshallableString `json:"uplink,omitempty"`
+}
+
+/*
+ * Add structs needed to parse mesos labels
+ */
+
+type Mesos struct {
+ NetworkInfo NetworkInfo `json:"network_info"`
+}
+
+type NetworkInfo struct {
+ Name string `json:"name"`
+ Labels struct {
+ Labels []struct {
+ Key string `json:"key"`
+ Value string `json:"value"`
+ } `json:"labels,omitempty"`
+ } `json:"labels,omitempty"`
+}
+
+// The top-level network config, just so we can get the IPAM block
+type Net struct {
+ Name string `json:"name"`
+ CNIVersion string `json:"cniVersion"`
+ IPAM *IPAMConfig `json:"ipam"`
+ Args *struct {
+ A *IPAMArgs `json:"cni"`
+ Mesos Mesos `json:"org.apache.mesos,omitempty"`
+ } `json:"args"`
+}
+
+type Range struct {
+ RangeStart net.IP `json:"rangeStart,omitempty"` // The first ip, inclusive
+ RangeEnd net.IP `json:"rangeEnd,omitempty"` // The last ip, inclusive
+ Subnet types.IPNet `json:"subnet"`
+ Gateway net.IP `json:"gateway,omitempty"`
+}
+
+// NewIPAMConfig creates a NetworkConfig from the given network name.
+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")
+ }
+
+ /*
+ * Get values for supplied labels
+ */
+ labels := map[string]string{}
+ if n.Args != nil {
+
+ for k, label := range n.Args.Mesos.NetworkInfo.Labels.Labels {
+ labels[label.Key] = label.Value
+ println("Map k (for)", k)
+ println("Map k (for)", k, label.Key, label.Value)
+ }
+
+ println("CNI Args NetworkInfo: Net Name: ", n.Args.Mesos.NetworkInfo.Name)
+ }
+
+ for key, value := range labels {
+ println("Key:", key, "Value:", value)
+ }
+
+ staticIP, found := labels["StaticIP"]
+ if found {
+ println("Config StaticIP is: ", staticIP)
+ log.Println("Config StaticIP is: ", staticIP)
+ n.IPAM.STATICIP = net.ParseIP(staticIP)
+ log.Println("Config IPAM args: n.IPAM.STATICIP is:", n.IPAM.STATICIP)
+ } else {
+ println("Config StaticIP label NOT set")
+ }
+
+ // Parse custom IP from both env args *and* the top-level args config
+ if envArgs != "" {
+ println("envArgs is not null")
+ e := IPAMEnvArgs{}
+ err := types.LoadArgs(envArgs, &e)
+ if err != nil {
+ return nil, "", err
+ }
+
+ if e.IP != nil {
+ n.IPAM.IPArgs = []net.IP{e.IP}
+ println("e.IP != nil")
+ println("n.IPAM.IPArgs value is", n.IPAM.IPArgs[0].String())
+ }
+ }
+
+ if envArgs == "" {
+ println("envArgs IS null")
+ }
+
+ if n.Args != nil && n.Args.A != nil && len(n.Args.A.IPs) != 0 {
+ n.IPAM.IPArgs = append(n.IPAM.IPArgs, n.Args.A.IPs...)
+ }
+
+ if found {
+ //n.IPAM.IPArgs = append(n.IPAM.IPArgs, staticIP)
+ n.IPAM.IPArgs = append(n.IPAM.IPArgs, n.IPAM.STATICIP)
+ }
+
+ for idx, _ := range n.IPAM.IPArgs {
+ if err := canonicalizeIP(&n.IPAM.IPArgs[idx]); err != nil {
+ return nil, "", fmt.Errorf("cannot understand ip: %v", err)
+ }
+ }
+
+ // If a single range (old-style config) is specified, move it to
+ // the Ranges array
+ if n.IPAM.Range != nil && n.IPAM.Range.Subnet.IP != nil {
+ n.IPAM.Ranges = append([]Range{*n.IPAM.Range}, n.IPAM.Ranges...)
+ }
+ n.IPAM.Range = nil
+
+ if len(n.IPAM.Ranges) == 0 {
+ return nil, "", fmt.Errorf("no IP ranges specified")
+ }
+
+ // Validate all ranges
+ numV4 := 0
+ numV6 := 0
+ for i, _ := range n.IPAM.Ranges {
+ if err := n.IPAM.Ranges[i].Canonicalize(); err != nil {
+ return nil, "", fmt.Errorf("Cannot understand range %d: %v", i, err)
+ }
+ if len(n.IPAM.Ranges[i].RangeStart) == 4 {
+ numV4++
+ } else {
+ numV6++
+ }
+ }
+
+ // 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 range per address family", n.CNIVersion)
+ }
+ }
+ }
+
+ // Check for overlaps
+ l := len(n.IPAM.Ranges)
+ for i, r1 := range n.IPAM.Ranges[:l-1] {
+ for j, r2 := range n.IPAM.Ranges[i+1:] {
+ if r1.Overlaps(&r2) {
+ return nil, "", fmt.Errorf("Range %d overlaps with range %d", i, (i + j + 1))
+ }
+ }
+ }
+
+ // Copy net name into IPAM so not to drag Net struct around
+ n.IPAM.Name = n.Name
+
+ /*
+ * Example of getting an environment variable supplied to the IPAM plugin
+ */
+ mccval := os.Getenv("MCCVAL")
+ println("mccval is: ", mccval)
+
+ cni_args := os.Getenv("CNI_ARGS")
+ println("bridge -- CNI_ARGS is: ", cni_args)
+
+ /*
+ * Get values for supplied labels
+ */
+/*
+ labels := map[string]string{}
+
+ if n.Args != nil {
+
+ for k, label := range n.Args.Mesos.NetworkInfo.Labels.Labels {
+ labels[label.Key] = label.Value
+ println("Map k (for)", k)
+ println("Map k (for)", k, label.Key, label.Value)
+ }
+
+ println("CNI Args NetworkInfo: Net Name: ", n.Args.Mesos.NetworkInfo.Name)
+ }
+
+ for key, value := range labels {
+ println("Key:", key, "Value:", value)
+ }
+*/
+ println("CNI IPAM Name: ", n.IPAM.Name)
+
+/*
+ staticIP, found := labels["StaticIP"]
+ if found {
+ println("StaticIP is: ", staticIP)
+ log.Println("StaticIP is: ", staticIP)
+ n.IPAM.STATICIP = net.ParseIP(staticIP)
+ log.Println("IPAM args: n.IPAM.STATICIP is:", n.IPAM.STATICIP)
+ } else {
+ println("StaticIP label NOT set")
+ }
+*/
+ viaIP, found := labels["viaIP"]
+ if found {
+ println("viaIP is: ", viaIP)
+ log.Println("viaIP is: ", viaIP)
+ n.IPAM.VIAIP = net.ParseIP(viaIP)
+ log.Println("IPAM args: n.IPAM.VIAIP is:", n.IPAM.VIAIP)
+ } else {
+ println("viaIP label NOT set")
+ }
+
+ Uplink, found := labels["Uplink"]
+ if found {
+ println("Uplink is: ", Uplink)
+ log.Println("Uplink is: ", Uplink)
+ n.IPAM.UPLINK= types.UnmarshallableString(Uplink)
+ log.Println("IPAM args: n.IPAM.UPLINK is:", n.IPAM.UPLINK)
+ } else {
+ println("Uplink label NOT set")
+ }
+
+ bull, found := labels["bull"]
+ if !found {
+ println("Hard to believe, but bull not found")
+ } else {
+ println("Found: ", bull)
+ }
+
+ return n.IPAM, n.CNIVersion, nil
+}
--- /dev/null
+// 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 allocator
+
+import (
+ "net"
+
+ "github.com/containernetworking/cni/pkg/types"
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("IPAM config", func() {
+ It("Should parse an old-style config", func() {
+ input := `{
+ "cniVersion": "0.3.1",
+ "name": "mynet",
+ "type": "ipvlan",
+ "master": "foo0",
+ "ipam": {
+ "type": "host-local",
+ "subnet": "10.1.2.0/24",
+ "rangeStart": "10.1.2.9",
+ "rangeEnd": "10.1.2.20",
+ "gateway": "10.1.2.30"
+ }
+}`
+ conf, version, err := LoadIPAMConfig([]byte(input), "")
+ Expect(err).NotTo(HaveOccurred())
+ Expect(version).Should(Equal("0.3.1"))
+
+ Expect(conf).To(Equal(&IPAMConfig{
+ Name: "mynet",
+ Type: "host-local",
+ Ranges: []Range{
+ {
+ RangeStart: net.IP{10, 1, 2, 9},
+ RangeEnd: net.IP{10, 1, 2, 20},
+ Gateway: net.IP{10, 1, 2, 30},
+ Subnet: types.IPNet{
+ IP: net.IP{10, 1, 2, 0},
+ Mask: net.CIDRMask(24, 32),
+ },
+ },
+ },
+ }))
+ })
+ It("Should parse a new-style config", func() {
+ input := `{
+ "cniVersion": "0.3.1",
+ "name": "mynet",
+ "type": "ipvlan",
+ "master": "foo0",
+ "ipam": {
+ "type": "host-local",
+ "ranges": [
+ {
+ "subnet": "10.1.2.0/24",
+ "rangeStart": "10.1.2.9",
+ "rangeEnd": "10.1.2.20",
+ "gateway": "10.1.2.30"
+ },
+ {
+ "subnet": "11.1.2.0/24",
+ "rangeStart": "11.1.2.9",
+ "rangeEnd": "11.1.2.20",
+ "gateway": "11.1.2.30"
+ }
+ ]
+ }
+}`
+ conf, version, err := LoadIPAMConfig([]byte(input), "")
+ Expect(err).NotTo(HaveOccurred())
+ Expect(version).Should(Equal("0.3.1"))
+
+ Expect(conf).To(Equal(&IPAMConfig{
+ Name: "mynet",
+ Type: "host-local",
+ Ranges: []Range{
+ {
+ RangeStart: net.IP{10, 1, 2, 9},
+ RangeEnd: net.IP{10, 1, 2, 20},
+ Gateway: net.IP{10, 1, 2, 30},
+ Subnet: types.IPNet{
+ IP: net.IP{10, 1, 2, 0},
+ Mask: net.CIDRMask(24, 32),
+ },
+ },
+ {
+ RangeStart: net.IP{11, 1, 2, 9},
+ RangeEnd: net.IP{11, 1, 2, 20},
+ Gateway: net.IP{11, 1, 2, 30},
+ Subnet: types.IPNet{
+ IP: net.IP{11, 1, 2, 0},
+ Mask: net.CIDRMask(24, 32),
+ },
+ },
+ },
+ }))
+ })
+
+ It("Should parse a mixed config", func() {
+ input := `{
+ "cniVersion": "0.3.1",
+ "name": "mynet",
+ "type": "ipvlan",
+ "master": "foo0",
+ "ipam": {
+ "type": "host-local",
+ "subnet": "10.1.2.0/24",
+ "rangeStart": "10.1.2.9",
+ "rangeEnd": "10.1.2.20",
+ "gateway": "10.1.2.30",
+ "ranges": [
+ {
+ "subnet": "11.1.2.0/24",
+ "rangeStart": "11.1.2.9",
+ "rangeEnd": "11.1.2.20",
+ "gateway": "11.1.2.30"
+ }
+ ]
+ }
+}`
+ conf, version, err := LoadIPAMConfig([]byte(input), "")
+ Expect(err).NotTo(HaveOccurred())
+ Expect(version).Should(Equal("0.3.1"))
+
+ Expect(conf).To(Equal(&IPAMConfig{
+ Name: "mynet",
+ Type: "host-local",
+ Ranges: []Range{
+ {
+ RangeStart: net.IP{10, 1, 2, 9},
+ RangeEnd: net.IP{10, 1, 2, 20},
+ Gateway: net.IP{10, 1, 2, 30},
+ Subnet: types.IPNet{
+ IP: net.IP{10, 1, 2, 0},
+ Mask: net.CIDRMask(24, 32),
+ },
+ },
+ {
+ RangeStart: net.IP{11, 1, 2, 9},
+ RangeEnd: net.IP{11, 1, 2, 20},
+ Gateway: net.IP{11, 1, 2, 30},
+ Subnet: types.IPNet{
+ IP: net.IP{11, 1, 2, 0},
+ Mask: net.CIDRMask(24, 32),
+ },
+ },
+ },
+ }))
+ })
+
+ It("Should parse CNI_ARGS env", func() {
+ input := `{
+ "cniVersion": "0.3.1",
+ "name": "mynet",
+ "type": "ipvlan",
+ "master": "foo0",
+ "ipam": {
+ "type": "host-local",
+ "ranges": [
+ {
+ "subnet": "10.1.2.0/24",
+ "rangeStart": "10.1.2.9",
+ "rangeEnd": "10.1.2.20",
+ "gateway": "10.1.2.30"
+ },
+ {
+ "subnet": "11.1.2.0/24",
+ "rangeStart": "11.1.2.9",
+ "rangeEnd": "11.1.2.20",
+ "gateway": "11.1.2.30"
+ }
+ ]
+ }
+}`
+
+ envArgs := "IP=10.1.2.10"
+
+ conf, _, err := LoadIPAMConfig([]byte(input), envArgs)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(conf.IPArgs).To(Equal([]net.IP{{10, 1, 2, 10}}))
+
+ })
+ It("Should parse config args", func() {
+ input := `{
+ "cniVersion": "0.3.1",
+ "name": "mynet",
+ "type": "ipvlan",
+ "master": "foo0",
+ "args": {
+ "cni": {
+ "ips": [ "10.1.2.11", "11.11.11.11", "2001:db8:1::11"]
+ }
+ },
+ "ipam": {
+ "type": "host-local",
+ "ranges": [
+ {
+ "subnet": "10.1.2.0/24",
+ "rangeStart": "10.1.2.9",
+ "rangeEnd": "10.1.2.20",
+ "gateway": "10.1.2.30"
+ },
+ {
+ "subnet": "11.1.2.0/24",
+ "rangeStart": "11.1.2.9",
+ "rangeEnd": "11.1.2.20",
+ "gateway": "11.1.2.30"
+ },
+ {
+ "subnet": "2001:db8:1::/64"
+ }
+ ]
+ }
+}`
+
+ envArgs := "IP=10.1.2.10"
+
+ conf, _, err := LoadIPAMConfig([]byte(input), envArgs)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(conf.IPArgs).To(Equal([]net.IP{
+ {10, 1, 2, 10},
+ {10, 1, 2, 11},
+ {11, 11, 11, 11},
+ net.ParseIP("2001:db8:1::11"),
+ }))
+ })
+ It("Should detect overlap", func() {
+ input := `{
+ "cniVersion": "0.3.1",
+ "name": "mynet",
+ "type": "ipvlan",
+ "master": "foo0",
+ "ipam": {
+ "type": "host-local",
+ "ranges": [
+ {
+ "subnet": "10.1.2.0/24",
+ "rangeEnd": "10.1.2.128"
+ },
+ {
+ "subnet": "10.1.2.0/24",
+ "rangeStart": "10.1.2.15"
+ }
+ ]
+ }
+}`
+ _, _, err := LoadIPAMConfig([]byte(input), "")
+ Expect(err).To(MatchError("Range 0 overlaps with range 1"))
+ })
+
+ It("Should should error on too many ranges", func() {
+ input := `{
+ "cniVersion": "0.2.0",
+ "name": "mynet",
+ "type": "ipvlan",
+ "master": "foo0",
+ "ipam": {
+ "type": "host-local",
+ "ranges": [
+ {
+ "subnet": "10.1.2.0/24"
+ },
+ {
+ "subnet": "11.1.2.0/24"
+ }
+ ]
+ }
+}`
+ _, _, err := LoadIPAMConfig([]byte(input), "")
+ Expect(err).To(MatchError("CNI version 0.2.0 does not support more than 1 range per address family"))
+ })
+
+ It("Should allow one v4 and v6 range for 0.2.0", func() {
+ input := `{
+ "cniVersion": "0.2.0",
+ "name": "mynet",
+ "type": "ipvlan",
+ "master": "foo0",
+ "ipam": {
+ "type": "host-local",
+ "ranges": [
+ {
+ "subnet": "10.1.2.0/24"
+ },
+ {
+ "subnet": "2001:db8:1::/24"
+ }
+ ]
+ }
+}`
+ _, _, err := LoadIPAMConfig([]byte(input), "")
+ Expect(err).NotTo(HaveOccurred())
+ })
+})
--- /dev/null
+// 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 allocator
+
+import (
+ "fmt"
+ "net"
+
+ "github.com/containernetworking/cni/pkg/types"
+ "github.com/containernetworking/plugins/pkg/ip"
+)
+
+// Canonicalize takes a given range and ensures that all information is consistent,
+// filling out Start, End, and Gateway with sane values if missing
+func (r *Range) Canonicalize() error {
+ if err := canonicalizeIP(&r.Subnet.IP); err != nil {
+ return err
+ }
+
+ // Can't create an allocator for a network with no addresses, eg
+ // a /32 or /31
+ ones, masklen := r.Subnet.Mask.Size()
+ if ones > masklen-2 {
+ return fmt.Errorf("Network %s too small to allocate from", (*net.IPNet)(&r.Subnet).String())
+ }
+
+ if len(r.Subnet.IP) != len(r.Subnet.Mask) {
+ return fmt.Errorf("IPNet IP and Mask version mismatch")
+ }
+
+ // If the gateway is nil, claim .1
+ if r.Gateway == nil {
+ r.Gateway = ip.NextIP(r.Subnet.IP)
+ } else {
+ if err := canonicalizeIP(&r.Gateway); err != nil {
+ return err
+ }
+ subnet := (net.IPNet)(r.Subnet)
+ if !subnet.Contains(r.Gateway) {
+ return fmt.Errorf("gateway %s not in network %s", r.Gateway.String(), subnet.String())
+ }
+ }
+
+ // RangeStart: If specified, make sure it's sane (inside the subnet),
+ // otherwise use the first free IP (i.e. .1) - this will conflict with the
+ // gateway but we skip it in the iterator
+ if r.RangeStart != nil {
+ if err := canonicalizeIP(&r.RangeStart); err != nil {
+ return err
+ }
+
+ if err := r.IPInRange(r.RangeStart); err != nil {
+ return err
+ }
+ } else {
+ r.RangeStart = ip.NextIP(r.Subnet.IP)
+ }
+
+ // RangeEnd: If specified, verify sanity. Otherwise, add a sensible default
+ // (e.g. for a /24: .254 if IPv4, ::255 if IPv6)
+ if r.RangeEnd != nil {
+ if err := canonicalizeIP(&r.RangeEnd); err != nil {
+ return err
+ }
+
+ if err := r.IPInRange(r.RangeEnd); err != nil {
+ return err
+ }
+ } else {
+ r.RangeEnd = lastIP(r.Subnet)
+ }
+
+ return nil
+}
+
+// IsValidIP checks if a given ip is a valid, allocatable address in a given Range
+func (r *Range) IPInRange(addr net.IP) error {
+ if err := canonicalizeIP(&addr); err != nil {
+ return err
+ }
+
+ subnet := (net.IPNet)(r.Subnet)
+
+ if len(addr) != len(r.Subnet.IP) {
+ return fmt.Errorf("IP %s is not the same protocol as subnet %s",
+ addr, subnet.String())
+ }
+
+ if !subnet.Contains(addr) {
+ return fmt.Errorf("%s not in network %s", addr, subnet.String())
+ }
+
+ // We ignore nils here so we can use this function as we initialize the range.
+ if r.RangeStart != nil {
+ if ip.Cmp(addr, r.RangeStart) < 0 {
+ return fmt.Errorf("%s is in network %s but before start %s",
+ addr, (*net.IPNet)(&r.Subnet).String(), r.RangeStart)
+ }
+ }
+
+ if r.RangeEnd != nil {
+ if ip.Cmp(addr, r.RangeEnd) > 0 {
+ return fmt.Errorf("%s is in network %s but after end %s",
+ addr, (*net.IPNet)(&r.Subnet).String(), r.RangeEnd)
+ }
+ }
+
+ return nil
+}
+
+// Overlaps returns true if there is any overlap between ranges
+func (r *Range) Overlaps(r1 *Range) bool {
+ // different familes
+ if len(r.RangeStart) != len(r1.RangeStart) {
+ return false
+ }
+
+ return r.IPInRange(r1.RangeStart) == nil ||
+ r.IPInRange(r1.RangeEnd) == nil ||
+ r1.IPInRange(r.RangeStart) == nil ||
+ r1.IPInRange(r.RangeEnd) == nil
+}
+
+// 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)
+}
+
+// Determine the last IP of a subnet, excluding the broadcast if IPv4
+func lastIP(subnet types.IPNet) net.IP {
+ var end net.IP
+ for i := 0; i < len(subnet.IP); i++ {
+ end = append(end, subnet.IP[i]|^subnet.Mask[i])
+ }
+ if subnet.IP.To4() != nil {
+ end[3]--
+ }
+
+ return end
+}
--- /dev/null
+// 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 allocator
+
+import (
+ "net"
+
+ "github.com/containernetworking/cni/pkg/types"
+
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/ginkgo/extensions/table"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("IP ranges", func() {
+ It("should generate sane defaults for ipv4", func() {
+ snstr := "192.0.2.0/24"
+ r := Range{Subnet: mustSubnet(snstr)}
+
+ err := r.Canonicalize()
+ Expect(err).NotTo(HaveOccurred())
+
+ Expect(r).To(Equal(Range{
+ Subnet: mustSubnet(snstr),
+ RangeStart: net.IP{192, 0, 2, 1},
+ RangeEnd: net.IP{192, 0, 2, 254},
+ Gateway: net.IP{192, 0, 2, 1},
+ }))
+ })
+ It("should generate sane defaults for a smaller ipv4 subnet", func() {
+ snstr := "192.0.2.0/25"
+ r := Range{Subnet: mustSubnet(snstr)}
+
+ err := r.Canonicalize()
+ Expect(err).NotTo(HaveOccurred())
+
+ Expect(r).To(Equal(Range{
+ Subnet: mustSubnet(snstr),
+ RangeStart: net.IP{192, 0, 2, 1},
+ RangeEnd: net.IP{192, 0, 2, 126},
+ Gateway: net.IP{192, 0, 2, 1},
+ }))
+ })
+ It("should generate sane defaults for ipv6", func() {
+ snstr := "2001:DB8:1::/64"
+ r := Range{Subnet: mustSubnet(snstr)}
+
+ err := r.Canonicalize()
+ Expect(err).NotTo(HaveOccurred())
+
+ Expect(r).To(Equal(Range{
+ Subnet: mustSubnet(snstr),
+ RangeStart: net.ParseIP("2001:DB8:1::1"),
+ RangeEnd: net.ParseIP("2001:DB8:1::ffff:ffff:ffff:ffff"),
+ Gateway: net.ParseIP("2001:DB8:1::1"),
+ }))
+ })
+
+ It("Should reject a network that's too small", func() {
+ r := Range{Subnet: mustSubnet("192.0.2.0/31")}
+ err := r.Canonicalize()
+ Expect(err).Should(MatchError("Network 192.0.2.0/31 too small to allocate from"))
+ })
+
+ It("should reject invalid RangeStart and RangeEnd specifications", func() {
+ r := Range{Subnet: mustSubnet("192.0.2.0/24"), RangeStart: net.ParseIP("192.0.3.0")}
+ err := r.Canonicalize()
+ Expect(err).Should(MatchError("192.0.3.0 not in network 192.0.2.0/24"))
+
+ r = Range{Subnet: mustSubnet("192.0.2.0/24"), RangeEnd: net.ParseIP("192.0.4.0")}
+ err = r.Canonicalize()
+ Expect(err).Should(MatchError("192.0.4.0 not in network 192.0.2.0/24"))
+
+ r = Range{
+ Subnet: mustSubnet("192.0.2.0/24"),
+ RangeStart: net.ParseIP("192.0.2.50"),
+ RangeEnd: net.ParseIP("192.0.2.40"),
+ }
+ err = r.Canonicalize()
+ Expect(err).Should(MatchError("192.0.2.50 is in network 192.0.2.0/24 but after end 192.0.2.40"))
+ })
+
+ It("should reject invalid gateways", func() {
+ r := Range{Subnet: mustSubnet("192.0.2.0/24"), Gateway: net.ParseIP("192.0.3.0")}
+ err := r.Canonicalize()
+ Expect(err).Should(MatchError("gateway 192.0.3.0 not in network 192.0.2.0/24"))
+ })
+
+ It("should parse all fields correctly", func() {
+ r := Range{
+ Subnet: mustSubnet("192.0.2.0/24"),
+ RangeStart: net.ParseIP("192.0.2.40"),
+ RangeEnd: net.ParseIP("192.0.2.50"),
+ Gateway: net.ParseIP("192.0.2.254"),
+ }
+ err := r.Canonicalize()
+ Expect(err).NotTo(HaveOccurred())
+
+ Expect(r).To(Equal(Range{
+ Subnet: mustSubnet("192.0.2.0/24"),
+ RangeStart: net.IP{192, 0, 2, 40},
+ RangeEnd: net.IP{192, 0, 2, 50},
+ Gateway: net.IP{192, 0, 2, 254},
+ }))
+ })
+
+ It("should accept v4 IPs in range and reject IPs out of range", func() {
+ r := Range{
+ Subnet: mustSubnet("192.0.2.0/24"),
+ RangeStart: net.ParseIP("192.0.2.40"),
+ RangeEnd: net.ParseIP("192.0.2.50"),
+ Gateway: net.ParseIP("192.0.2.254"),
+ }
+ err := r.Canonicalize()
+ Expect(err).NotTo(HaveOccurred())
+
+ Expect(r.IPInRange(net.ParseIP("192.0.3.0"))).Should(MatchError(
+ "192.0.3.0 not in network 192.0.2.0/24"))
+
+ Expect(r.IPInRange(net.ParseIP("192.0.2.39"))).Should(MatchError(
+ "192.0.2.39 is in network 192.0.2.0/24 but before start 192.0.2.40"))
+ Expect(r.IPInRange(net.ParseIP("192.0.2.40"))).Should(BeNil())
+ Expect(r.IPInRange(net.ParseIP("192.0.2.50"))).Should(BeNil())
+ Expect(r.IPInRange(net.ParseIP("192.0.2.51"))).Should(MatchError(
+ "192.0.2.51 is in network 192.0.2.0/24 but after end 192.0.2.50"))
+ })
+
+ It("should accept v6 IPs in range and reject IPs out of range", func() {
+ r := Range{
+ Subnet: mustSubnet("2001:DB8:1::/64"),
+ RangeStart: net.ParseIP("2001:db8:1::40"),
+ RangeEnd: net.ParseIP("2001:db8:1::50"),
+ }
+ err := r.Canonicalize()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(r.IPInRange(net.ParseIP("2001:db8:2::"))).Should(MatchError(
+ "2001:db8:2:: not in network 2001:db8:1::/64"))
+
+ Expect(r.IPInRange(net.ParseIP("2001:db8:1::39"))).Should(MatchError(
+ "2001:db8:1::39 is in network 2001:db8:1::/64 but before start 2001:db8:1::40"))
+ Expect(r.IPInRange(net.ParseIP("2001:db8:1::40"))).Should(BeNil())
+ Expect(r.IPInRange(net.ParseIP("2001:db8:1::50"))).Should(BeNil())
+ Expect(r.IPInRange(net.ParseIP("2001:db8:1::51"))).Should(MatchError(
+ "2001:db8:1::51 is in network 2001:db8:1::/64 but after end 2001:db8:1::50"))
+ })
+
+ DescribeTable("Detecting overlap",
+ func(r1 Range, r2 Range, expected bool) {
+ r1.Canonicalize()
+ r2.Canonicalize()
+
+ // operation should be commutative
+ Expect(r1.Overlaps(&r2)).To(Equal(expected))
+ Expect(r2.Overlaps(&r1)).To(Equal(expected))
+ },
+ Entry("non-overlapping",
+ Range{Subnet: mustSubnet("10.0.0.0/24")},
+ Range{Subnet: mustSubnet("10.0.1.0/24")},
+ false),
+ Entry("different families",
+ // Note that the bits overlap
+ Range{Subnet: mustSubnet("0.0.0.0/24")},
+ Range{Subnet: mustSubnet("::/24")},
+ false),
+ Entry("Identical",
+ Range{Subnet: mustSubnet("10.0.0.0/24")},
+ Range{Subnet: mustSubnet("10.0.0.0/24")},
+ true),
+ Entry("Containing",
+ Range{Subnet: mustSubnet("10.0.0.0/20")},
+ Range{Subnet: mustSubnet("10.0.1.0/24")},
+ true),
+ Entry("same subnet, non overlapping start + end",
+ Range{
+ Subnet: mustSubnet("10.0.0.0/24"),
+ RangeEnd: net.ParseIP("10.0.0.127"),
+ },
+ Range{
+ Subnet: mustSubnet("10.0.0.0/24"),
+ RangeStart: net.ParseIP("10.0.0.128"),
+ },
+ false),
+ Entry("same subnet, overlapping start + end",
+ Range{
+ Subnet: mustSubnet("10.0.0.0/24"),
+ RangeEnd: net.ParseIP("10.0.0.127"),
+ },
+ Range{
+ Subnet: mustSubnet("10.0.0.0/24"),
+ RangeStart: net.ParseIP("10.0.0.127"),
+ },
+ true),
+ )
+})
+
+func mustSubnet(s string) types.IPNet {
+ n, err := types.ParseCIDR(s)
+ if err != nil {
+ Fail(err.Error())
+ }
+ canonicalizeIP(&n.IP)
+ return types.IPNet(*n)
+}
--- /dev/null
+// Copyright 2015 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 disk
+
+import (
+ "io/ioutil"
+ "net"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/containernetworking/plugins/plugins/ipam/host-local/backend"
+)
+
+const lastIPFilePrefix = "last_reserved_ip."
+
+var defaultDataDir = "/var/lib/cni/networks"
+
+// Store is a simple disk-backed store that creates one file per IP
+// address in a given directory. The contents of the file are the container ID.
+type Store struct {
+ FileLock
+ dataDir string
+}
+
+// Store implements the Store interface
+var _ backend.Store = &Store{}
+
+func New(network, dataDir string) (*Store, error) {
+ if dataDir == "" {
+ dataDir = defaultDataDir
+ }
+ dir := filepath.Join(dataDir, network)
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ return nil, err
+ }
+
+ lk, err := NewFileLock(dir)
+ if err != nil {
+ return nil, err
+ }
+ return &Store{*lk, dir}, nil
+}
+
+func (s *Store) Reserve(id string, ip net.IP, rangeID string) (bool, error) {
+ fname := filepath.Join(s.dataDir, ip.String())
+ f, err := os.OpenFile(fname, os.O_RDWR|os.O_EXCL|os.O_CREATE, 0644)
+ if os.IsExist(err) {
+ return false, nil
+ }
+ if err != nil {
+ return false, err
+ }
+ if _, err := f.WriteString(strings.TrimSpace(id)); err != nil {
+ f.Close()
+ os.Remove(f.Name())
+ return false, err
+ }
+ if err := f.Close(); err != nil {
+ os.Remove(f.Name())
+ return false, err
+ }
+ // store the reserved ip in lastIPFile
+ ipfile := filepath.Join(s.dataDir, lastIPFilePrefix+rangeID)
+ err = ioutil.WriteFile(ipfile, []byte(ip.String()), 0644)
+ if err != nil {
+ return false, err
+ }
+ return true, nil
+}
+
+// LastReservedIP returns the last reserved IP if exists
+func (s *Store) LastReservedIP(rangeID string) (net.IP, error) {
+ ipfile := filepath.Join(s.dataDir, lastIPFilePrefix+rangeID)
+ data, err := ioutil.ReadFile(ipfile)
+ if err != nil {
+ return nil, err
+ }
+ return net.ParseIP(string(data)), nil
+}
+
+func (s *Store) Release(ip net.IP) error {
+ return os.Remove(filepath.Join(s.dataDir, ip.String()))
+}
+
+// N.B. This function eats errors to be tolerant and
+// release as much as possible
+func (s *Store) ReleaseByID(id string) error {
+ 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)) == strings.TrimSpace(id) {
+ if err := os.Remove(path); err != nil {
+ return nil
+ }
+ }
+ return nil
+ })
+ return err
+}
--- /dev/null
+// Copyright 2015 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 disk
+
+import (
+ "os"
+ "syscall"
+)
+
+// FileLock wraps os.File to be used as a lock using flock
+type FileLock struct {
+ f *os.File
+}
+
+// NewFileLock opens file/dir at path and returns unlocked FileLock object
+func NewFileLock(path string) (*FileLock, error) {
+ f, err := os.Open(path)
+ if err != nil {
+ return nil, err
+ }
+
+ return &FileLock{f}, nil
+}
+
+// Close closes underlying file
+func (l *FileLock) Close() error {
+ return l.f.Close()
+}
+
+// Lock acquires an exclusive lock
+func (l *FileLock) Lock() error {
+ return syscall.Flock(int(l.f.Fd()), syscall.LOCK_EX)
+}
+
+// Unlock releases the lock
+func (l *FileLock) Unlock() error {
+ return syscall.Flock(int(l.f.Fd()), syscall.LOCK_UN)
+}
--- /dev/null
+// Copyright 2015 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 backend
+
+import "net"
+
+type Store interface {
+ Lock() error
+ Unlock() error
+ Close() error
+ Reserve(id string, ip net.IP, rangeID string) (bool, error)
+ LastReservedIP(rangeID string) (net.IP, error)
+ Release(ip net.IP) error
+ ReleaseByID(id string) error
+}
--- /dev/null
+// Copyright 2015 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 testing
+
+import (
+ "net"
+ "os"
+
+ "github.com/containernetworking/plugins/plugins/ipam/host-local/backend"
+)
+
+type FakeStore struct {
+ ipMap map[string]string
+ lastReservedIP map[string]net.IP
+}
+
+// FakeStore implements the Store interface
+var _ backend.Store = &FakeStore{}
+
+func NewFakeStore(ipmap map[string]string, lastIPs map[string]net.IP) *FakeStore {
+ return &FakeStore{ipmap, lastIPs}
+}
+
+func (s *FakeStore) Lock() error {
+ return nil
+}
+
+func (s *FakeStore) Unlock() error {
+ return nil
+}
+
+func (s *FakeStore) Close() error {
+ return nil
+}
+
+func (s *FakeStore) Reserve(id string, ip net.IP, rangeID string) (bool, error) {
+ key := ip.String()
+ if _, ok := s.ipMap[key]; !ok {
+ s.ipMap[key] = id
+ s.lastReservedIP[rangeID] = ip
+ return true, nil
+ }
+ return false, nil
+}
+
+func (s *FakeStore) LastReservedIP(rangeID string) (net.IP, error) {
+ ip, ok := s.lastReservedIP[rangeID]
+ if !ok {
+ return nil, os.ErrNotExist
+ }
+ return ip, nil
+}
+
+func (s *FakeStore) Release(ip net.IP) error {
+ delete(s.ipMap, ip.String())
+ return nil
+}
+
+func (s *FakeStore) ReleaseByID(id string) error {
+ toDelete := []string{}
+ for k, v := range s.ipMap {
+ if v == id {
+ toDelete = append(toDelete, k)
+ }
+ }
+ for _, ip := range toDelete {
+ delete(s.ipMap, ip)
+ }
+ return nil
+}
+
+func (s *FakeStore) SetIPMap(m map[string]string) {
+ s.ipMap = m
+}
--- /dev/null
+// 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 main
+
+import (
+ "bufio"
+ "os"
+ "strings"
+
+ "github.com/containernetworking/cni/pkg/types"
+)
+
+// parseResolvConf parses an existing resolv.conf in to a DNS struct
+func parseResolvConf(filename string) (*types.DNS, error) {
+ fp, err := os.Open(filename)
+ if err != nil {
+ return nil, err
+ }
+
+ dns := types.DNS{}
+ scanner := bufio.NewScanner(fp)
+ for scanner.Scan() {
+ line := scanner.Text()
+ line = strings.TrimSpace(line)
+
+ // Skip comments, empty lines
+ if len(line) == 0 || line[0] == '#' || line[0] == ';' {
+ continue
+ }
+
+ fields := strings.Fields(line)
+ if len(fields) < 2 {
+ continue
+ }
+ switch fields[0] {
+ case "nameserver":
+ dns.Nameservers = append(dns.Nameservers, fields[1])
+ case "domain":
+ dns.Domain = fields[1]
+ case "search":
+ dns.Search = append(dns.Search, fields[1:]...)
+ case "options":
+ dns.Options = append(dns.Options, fields[1:]...)
+ }
+ }
+
+ if err := scanner.Err(); err != nil {
+ return nil, err
+ }
+
+ return &dns, nil
+}
--- /dev/null
+// 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 main
+
+import (
+ "io/ioutil"
+ "os"
+
+ "github.com/containernetworking/cni/pkg/types"
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("parsing resolv.conf", func() {
+ It("parses a simple resolv.conf file", func() {
+ contents := `
+ nameserver 192.0.2.0
+ nameserver 192.0.2.1
+ `
+ dns, err := parse(contents)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(*dns).Should(Equal(types.DNS{Nameservers: []string{"192.0.2.0", "192.0.2.1"}}))
+ })
+ It("ignores comments", func() {
+ dns, err := parse(`
+nameserver 192.0.2.0
+;nameserver 192.0.2.1
+`)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(*dns).Should(Equal(types.DNS{Nameservers: []string{"192.0.2.0"}}))
+ })
+ It("parses all fields", func() {
+ dns, err := parse(`
+nameserver 192.0.2.0
+nameserver 192.0.2.2
+domain example.com
+;nameserver comment
+#nameserver comment
+search example.net example.org
+search example.gov
+options one two three
+options four
+`)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(*dns).Should(Equal(types.DNS{
+ Nameservers: []string{"192.0.2.0", "192.0.2.2"},
+ Domain: "example.com",
+ Search: []string{"example.net", "example.org", "example.gov"},
+ Options: []string{"one", "two", "three", "four"},
+ }))
+ })
+})
+
+func parse(contents string) (*types.DNS, error) {
+ f, err := ioutil.TempFile("", "host_local_resolv")
+ defer f.Close()
+ defer os.Remove(f.Name())
+
+ if err != nil {
+ return nil, err
+ }
+
+ if _, err := f.WriteString(contents); err != nil {
+ return nil, err
+ }
+
+ return parseResolvConf(f.Name())
+}
--- /dev/null
+// Copyright 2015 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"
+ "net"
+ "strings"
+
+ "github.com/containernetworking/plugins/plugins/ipam/vz-local/backend/allocator"
+ "github.com/containernetworking/plugins/plugins/ipam/vz-local/backend/disk"
+
+ "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"
+
+ "log"
+ //"os"
+ //"syscall"
+ "github.com/vishvananda/netlink"
+)
+
+func main() {
+ skel.PluginMain(cmdAdd, cmdDel, version.All)
+}
+
+func cmdAdd(args *skel.CmdArgs) error {
+ ipamConf, confVersion, err := allocator.LoadIPAMConfig(args.StdinData, args.Args)
+ if err != nil {
+ return err
+ }
+
+ result := ¤t.Result{}
+
+ if ipamConf.ResolvConf != "" {
+ dns, err := parseResolvConf(ipamConf.ResolvConf)
+ if err != nil {
+ return err
+ }
+ result.DNS = *dns
+ }
+
+ store, err := disk.New(ipamConf.Name, ipamConf.DataDir)
+ if err != nil {
+ return err
+ }
+ defer store.Close()
+
+ // Keep the allocators we used, so we can release all IPs if an error
+ // occurs after we start allocating
+ allocs := []*allocator.IPAllocator{}
+
+ // Store all requested IPs in a map, so we can easily remove ones we use
+ // and error if some remain
+ requestedIPs := map[string]net.IP{} //net.IP cannot be a key
+
+ for _, ip := range ipamConf.IPArgs {
+ requestedIPs[ip.String()] = ip
+ }
+
+ for idx, ipRange := range ipamConf.Ranges {
+ allocator := allocator.NewIPAllocator(ipamConf.Name, ipRange, store)
+
+ // Check to see if there are any custom IPs requested in this range.
+ var requestedIP net.IP
+ for k, ip := range requestedIPs {
+ if ipRange.IPInRange(ip) == nil {
+ requestedIP = ip
+ delete(requestedIPs, k)
+ break
+ }
+ }
+
+ ipConf, err := allocator.Get(args.ContainerID, requestedIP)
+ if err != nil {
+ // Deallocate all already allocated IPs
+ for _, alloc := range allocs {
+ _ = alloc.Release(args.ContainerID)
+ }
+ return fmt.Errorf("failed to allocate for range %d: %v", idx, err)
+ }
+
+ allocs = append(allocs, allocator)
+
+ result.IPs = append(result.IPs, ipConf)
+ }
+
+ // If an IP was requested that wasn't fulfilled, fail
+ if len(requestedIPs) != 0 {
+ for _, alloc := range allocs {
+ _ = alloc.Release(args.ContainerID)
+ }
+ errstr := "failed to allocate all requested IPs:"
+ for _, ip := range requestedIPs {
+ errstr = errstr + " " + ip.String()
+ }
+ return fmt.Errorf(errstr)
+ }
+
+ result.Routes = ipamConf.Routes
+
+/*
+ * TODO: Move this to the loop above; run each time requestedIP is found
+ */
+ var uplink types.UnmarshallableString
+ if ipamConf.UPLINK != "" {
+ uplink = ipamConf.UPLINK
+
+ var requestedIP net.IP
+
+ if ipamConf.STATICIP != nil {
+ requestedIP = ipamConf.STATICIP
+ } else {
+ log.Println("RequestedIP was not set")
+ }
+
+ log.Println("RequestedIP is ", requestedIP)
+ log.Println("StaticIP is", ipamConf.STATICIP)
+
+ derivedIP := result.IPs[0].Address.IP.To4()
+ println("derived IP is (string):", derivedIP.String())
+
+ if derivedIP.String() == requestedIP.String() {
+ println("derived ip == requestedIP == StaticIP ", derivedIP.String(), requestedIP.String())
+ } else {
+ println("derived ip NOT = requestediP !=StaticIP ", derivedIP.String(), requestedIP.String())
+ }
+//
+ if requestedIP.String() != ipamConf.STATICIP.String() {
+ log.Println("RequestedIP is NOT equal to StaticIP", requestedIP.String(), ipamConf.STATICIP)
+ } else {
+ log.Println("RequestedIP is equal to StaticIP", requestedIP.String(), ipamConf.STATICIP)
+ }
+//
+ log.Println("AdvRoute: Uplink - BGP Advertises /32", requestedIP, uplink)
+
+ dst := &net.IPNet{
+ IP: requestedIP,
+ Mask: net.CIDRMask(32, 32),
+ }
+
+ link, err := netlink.LinkByName(string(uplink))
+ if err != nil {
+ log.Println("Can't obtain link index for: ", uplink)
+ //return nil, nil, err
+ }
+
+ viaIP := ipamConf.VIAIP
+ route := netlink.Route{
+ Dst: dst,
+ LinkIndex: link.Attrs().Index,
+ Gw: viaIP,
+ }
+
+ log.Println("Not adding route here, moved to vz-bridge", route)
+/*
+ if err := netlink.RouteAdd(&route); err != nil {
+ fmt.Fprintln(os.Stderr, "There was an error adding netlink route: ", err)
+ if (err == syscall.EAGAIN) {
+ log.Println("ERRNO: eagain")
+ } else if (err == syscall.EEXIST) {
+ log.Println("ERRNO: route already exists")
+ } else {
+ log.Println("ERRNO: value is: ", (int(err.(syscall.Errno))))
+ }
+ //return nil, nil, err
+ }
+*/
+ }
+
+ return types.PrintResult(result, confVersion)
+}
+
+func cmdDel(args *skel.CmdArgs) error {
+ ipamConf, _, err := allocator.LoadIPAMConfig(args.StdinData, args.Args)
+ if err != nil {
+ return err
+ }
+
+ store, err := disk.New(ipamConf.Name, ipamConf.DataDir)
+ if err != nil {
+ return err
+ }
+ defer store.Close()
+
+ // Loop through all ranges, releasing all IPs, even if an error occurs
+ var errors []string
+ for _, ipRange := range ipamConf.Ranges {
+ ipAllocator := allocator.NewIPAllocator(ipamConf.Name, ipRange, store)
+
+ err := ipAllocator.Release(args.ContainerID)
+ if err != nil {
+ errors = append(errors, err.Error())
+ }
+ }
+
+ if errors != nil {
+ return fmt.Errorf(strings.Join(errors, ";"))
+ }
+
+/*
+ * TODO: Move this to the loop above; run each time requestedIP is found
+ */
+ var uplink types.UnmarshallableString
+ if ipamConf.UPLINK != "" {
+ uplink = ipamConf.UPLINK
+
+ var requestedIP net.IP
+
+ if ipamConf.STATICIP != nil {
+ requestedIP = ipamConf.STATICIP
+ } else {
+ log.Println("RequestedIP was not set")
+ }
+
+ log.Println("RequestedIP is ", requestedIP)
+ log.Println("StaticIP is", ipamConf.STATICIP)
+/*
+ if requestedIP != ipamConf.STATICIP {
+ log.Println("RequestedIP is NOT equal to StaticIP", requestedIP, ipamConf.STATICIP)
+ } else {
+ log.Println("RequestedIP is equal to StaticIP", requestedIP, ipamConf.STATICIP)
+ }
+*/
+ log.Println("AdvRoute: Uplink - BGP Advertises /32", requestedIP, uplink)
+
+ dst := &net.IPNet{
+ IP: requestedIP,
+ Mask: net.CIDRMask(32, 32),
+ }
+
+ link, err := netlink.LinkByName(string(uplink))
+ if err != nil {
+ log.Println("Can't obtain link index for: ", uplink)
+ //return nil, nil, err
+ }
+
+ viaIP := ipamConf.VIAIP
+ route := netlink.Route{
+ Dst: dst,
+ LinkIndex: link.Attrs().Index,
+ Gw: viaIP,
+ }
+
+ log.Println("Not removing route here, moved to vz-bridge", route)
+/*
+ if err := netlink.RouteDel(&route); err != nil {
+ fmt.Fprintln(os.Stderr, "There was an error adding netlink route: ", err)
+ if (err == syscall.EAGAIN) {
+ log.Println("ERRNO: eagain")
+ } else if (err == syscall.EEXIST) {
+ log.Println("ERRNO: route already exists")
+ } else {
+ log.Println("ERRNO: value is: ", (int(err.(syscall.Errno))))
+ }
+ //return nil, nil, err
+ }
+*/
+ }
+
+ return nil
+}
--- /dev/null
+// 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 main
+
+import (
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+
+ "testing"
+)
+
+func TestHostLocal(t *testing.T) {
+ RegisterFailHandler(Fail)
+ RunSpecs(t, "HostLocal Suite")
+}
--- /dev/null
+// 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 main
+
+import (
+ "fmt"
+ "io/ioutil"
+ "net"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/containernetworking/cni/pkg/skel"
+ "github.com/containernetworking/cni/pkg/types"
+ "github.com/containernetworking/cni/pkg/types/020"
+ "github.com/containernetworking/cni/pkg/types/current"
+ "github.com/containernetworking/plugins/pkg/testutils"
+
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+)
+
+var _ = Describe("host-local Operations", func() {
+ It("allocates and releases addresses with ADD/DEL", func() {
+ const ifname string = "eth0"
+ const nspath string = "/some/where"
+
+ tmpDir, err := ioutil.TempDir("", "host_local_artifacts")
+ Expect(err).NotTo(HaveOccurred())
+ defer os.RemoveAll(tmpDir)
+
+ err = ioutil.WriteFile(filepath.Join(tmpDir, "resolv.conf"), []byte("nameserver 192.0.2.3"), 0644)
+ Expect(err).NotTo(HaveOccurred())
+
+ conf := fmt.Sprintf(`{
+ "cniVersion": "0.3.1",
+ "name": "mynet",
+ "type": "ipvlan",
+ "master": "foo0",
+ "ipam": {
+ "type": "host-local",
+ "dataDir": "%s",
+ "resolvConf": "%s/resolv.conf",
+ "ranges": [
+ { "subnet": "10.1.2.0/24" },
+ { "subnet": "2001:db8:1::0/64" }
+ ],
+ "routes": [
+ {"dst": "0.0.0.0/0"},
+ {"dst": "::/0"},
+ {"dst": "192.168.0.0/16", "gw": "1.1.1.1"},
+ {"dst": "2001:db8:2::0/64", "gw": "2001:db8:3::1"}
+ ]
+ }
+}`, tmpDir, tmpDir)
+
+ args := &skel.CmdArgs{
+ ContainerID: "dummy",
+ Netns: nspath,
+ IfName: ifname,
+ StdinData: []byte(conf),
+ }
+
+ // Allocate the IP
+ r, raw, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
+ return cmdAdd(args)
+ })
+ Expect(err).NotTo(HaveOccurred())
+ Expect(strings.Index(string(raw), "\"version\":")).Should(BeNumerically(">", 0))
+
+ result, err := current.GetResult(r)
+ Expect(err).NotTo(HaveOccurred())
+
+ // Gomega is cranky about slices with different caps
+ Expect(*result.IPs[0]).To(Equal(
+ current.IPConfig{
+ Version: "4",
+ Address: mustCIDR("10.1.2.2/24"),
+ Gateway: net.ParseIP("10.1.2.1"),
+ }))
+
+ Expect(*result.IPs[1]).To(Equal(
+ current.IPConfig{
+ Version: "6",
+ Address: mustCIDR("2001:db8:1::2/64"),
+ Gateway: net.ParseIP("2001:db8:1::1"),
+ },
+ ))
+ Expect(len(result.IPs)).To(Equal(2))
+
+ Expect(result.Routes).To(Equal([]*types.Route{
+ {Dst: mustCIDR("0.0.0.0/0"), GW: nil},
+ {Dst: mustCIDR("::/0"), GW: nil},
+ {Dst: mustCIDR("192.168.0.0/16"), GW: net.ParseIP("1.1.1.1")},
+ {Dst: mustCIDR("2001:db8:2::0/64"), GW: net.ParseIP("2001:db8:3::1")},
+ }))
+
+ ipFilePath1 := filepath.Join(tmpDir, "mynet", "10.1.2.2")
+ contents, err := ioutil.ReadFile(ipFilePath1)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(string(contents)).To(Equal("dummy"))
+
+ ipFilePath2 := filepath.Join(tmpDir, "mynet", "2001:db8:1::2")
+ contents, err = ioutil.ReadFile(ipFilePath2)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(string(contents)).To(Equal("dummy"))
+
+ lastFilePath1 := filepath.Join(tmpDir, "mynet", "last_reserved_ip.CgECAQ==")
+ contents, err = ioutil.ReadFile(lastFilePath1)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(string(contents)).To(Equal("10.1.2.2"))
+
+ lastFilePath2 := filepath.Join(tmpDir, "mynet", "last_reserved_ip.IAENuAABAAAAAAAAAAAAAQ==")
+ contents, err = ioutil.ReadFile(lastFilePath2)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(string(contents)).To(Equal("2001:db8:1::2"))
+ // Release the IP
+ err = testutils.CmdDelWithResult(nspath, ifname, func() error {
+ return cmdDel(args)
+ })
+ Expect(err).NotTo(HaveOccurred())
+
+ _, err = os.Stat(ipFilePath1)
+ Expect(err).To(HaveOccurred())
+ _, err = os.Stat(ipFilePath2)
+ Expect(err).To(HaveOccurred())
+ })
+
+ It("doesn't error when passed an unknown ID on DEL", func() {
+ const ifname string = "eth0"
+ const nspath string = "/some/where"
+
+ tmpDir, err := ioutil.TempDir("", "host_local_artifacts")
+ Expect(err).NotTo(HaveOccurred())
+ defer os.RemoveAll(tmpDir)
+
+ conf := fmt.Sprintf(`{
+ "cniVersion": "0.3.0",
+ "name": "mynet",
+ "type": "ipvlan",
+ "master": "foo0",
+ "ipam": {
+ "type": "host-local",
+ "subnet": "10.1.2.0/24",
+ "dataDir": "%s"
+ }
+}`, tmpDir)
+
+ args := &skel.CmdArgs{
+ ContainerID: "dummy",
+ Netns: nspath,
+ IfName: ifname,
+ StdinData: []byte(conf),
+ }
+
+ // Release the IP
+ err = testutils.CmdDelWithResult(nspath, ifname, func() error {
+ return cmdDel(args)
+ })
+ Expect(err).NotTo(HaveOccurred())
+ })
+
+ It("allocates and releases an address with ADD/DEL and 0.1.0 config", func() {
+ const ifname string = "eth0"
+ const nspath string = "/some/where"
+
+ tmpDir, err := ioutil.TempDir("", "host_local_artifacts")
+ Expect(err).NotTo(HaveOccurred())
+ defer os.RemoveAll(tmpDir)
+
+ err = ioutil.WriteFile(filepath.Join(tmpDir, "resolv.conf"), []byte("nameserver 192.0.2.3"), 0644)
+ Expect(err).NotTo(HaveOccurred())
+
+ conf := fmt.Sprintf(`{
+ "cniVersion": "0.1.0",
+ "name": "mynet",
+ "type": "ipvlan",
+ "master": "foo0",
+ "ipam": {
+ "type": "host-local",
+ "subnet": "10.1.2.0/24",
+ "dataDir": "%s",
+ "resolvConf": "%s/resolv.conf"
+ }
+}`, tmpDir, tmpDir)
+
+ args := &skel.CmdArgs{
+ ContainerID: "dummy",
+ Netns: nspath,
+ IfName: ifname,
+ StdinData: []byte(conf),
+ }
+
+ // Allocate the IP
+ r, raw, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
+ return cmdAdd(args)
+ })
+ Expect(err).NotTo(HaveOccurred())
+ Expect(strings.Index(string(raw), "\"ip4\":")).Should(BeNumerically(">", 0))
+
+ result, err := types020.GetResult(r)
+ Expect(err).NotTo(HaveOccurred())
+
+ expectedAddress, err := types.ParseCIDR("10.1.2.2/24")
+ Expect(err).NotTo(HaveOccurred())
+ expectedAddress.IP = expectedAddress.IP.To16()
+ Expect(result.IP4.IP).To(Equal(*expectedAddress))
+ Expect(result.IP4.Gateway).To(Equal(net.ParseIP("10.1.2.1")))
+
+ ipFilePath := filepath.Join(tmpDir, "mynet", "10.1.2.2")
+ contents, err := ioutil.ReadFile(ipFilePath)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(string(contents)).To(Equal("dummy"))
+
+ lastFilePath := filepath.Join(tmpDir, "mynet", "last_reserved_ip.CgECAQ==")
+ contents, err = ioutil.ReadFile(lastFilePath)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(string(contents)).To(Equal("10.1.2.2"))
+
+ Expect(result.DNS).To(Equal(types.DNS{Nameservers: []string{"192.0.2.3"}}))
+
+ // Release the IP
+ err = testutils.CmdDelWithResult(nspath, ifname, func() error {
+ return cmdDel(args)
+ })
+ Expect(err).NotTo(HaveOccurred())
+
+ _, err = os.Stat(ipFilePath)
+ Expect(err).To(HaveOccurred())
+ })
+
+ It("ignores whitespace in disk files", func() {
+ const ifname string = "eth0"
+ const nspath string = "/some/where"
+
+ tmpDir, err := ioutil.TempDir("", "host_local_artifacts")
+ Expect(err).NotTo(HaveOccurred())
+ defer os.RemoveAll(tmpDir)
+
+ conf := fmt.Sprintf(`{
+ "cniVersion": "0.3.1",
+ "name": "mynet",
+ "type": "ipvlan",
+ "master": "foo0",
+ "ipam": {
+ "type": "host-local",
+ "subnet": "10.1.2.0/24",
+ "dataDir": "%s"
+ }
+}`, tmpDir)
+
+ args := &skel.CmdArgs{
+ ContainerID: " dummy\n ",
+ Netns: nspath,
+ IfName: ifname,
+ StdinData: []byte(conf),
+ }
+
+ // Allocate the IP
+ r, _, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
+ return cmdAdd(args)
+ })
+ Expect(err).NotTo(HaveOccurred())
+
+ result, err := current.GetResult(r)
+ Expect(err).NotTo(HaveOccurred())
+
+ ipFilePath := filepath.Join(tmpDir, "mynet", result.IPs[0].Address.IP.String())
+ contents, err := ioutil.ReadFile(ipFilePath)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(string(contents)).To(Equal("dummy"))
+
+ // Release the IP
+ err = testutils.CmdDelWithResult(nspath, ifname, func() error {
+ return cmdDel(args)
+ })
+ Expect(err).NotTo(HaveOccurred())
+
+ _, err = os.Stat(ipFilePath)
+ Expect(err).To(HaveOccurred())
+ })
+
+ It("does not output an error message upon initial subnet creation", func() {
+ const ifname string = "eth0"
+ const nspath string = "/some/where"
+
+ tmpDir, err := ioutil.TempDir("", "host_local_artifacts")
+ Expect(err).NotTo(HaveOccurred())
+ defer os.RemoveAll(tmpDir)
+
+ conf := fmt.Sprintf(`{
+ "cniVersion": "0.2.0",
+ "name": "mynet",
+ "type": "ipvlan",
+ "master": "foo0",
+ "ipam": {
+ "type": "host-local",
+ "subnet": "10.1.2.0/24",
+ "dataDir": "%s"
+ }
+}`, tmpDir)
+
+ args := &skel.CmdArgs{
+ ContainerID: "testing",
+ Netns: nspath,
+ IfName: ifname,
+ StdinData: []byte(conf),
+ }
+
+ // Allocate the IP
+ _, out, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
+ return cmdAdd(args)
+ })
+ Expect(err).NotTo(HaveOccurred())
+ Expect(strings.Index(string(out), "Error retriving last reserved ip")).To(Equal(-1))
+ })
+
+ It("allocates a custom IP when requested by config args", func() {
+ const ifname string = "eth0"
+ const nspath string = "/some/where"
+
+ tmpDir, err := ioutil.TempDir("", "host_local_artifacts")
+ Expect(err).NotTo(HaveOccurred())
+ defer os.RemoveAll(tmpDir)
+
+ conf := fmt.Sprintf(`{
+ "cniVersion": "0.3.1",
+ "name": "mynet",
+ "type": "ipvlan",
+ "master": "foo0",
+ "ipam": {
+ "type": "host-local",
+ "dataDir": "%s",
+ "ranges": [
+ { "subnet": "10.1.2.0/24" }
+ ]
+ },
+ "args": {
+ "cni": {
+ "ips": ["10.1.2.88"]
+ }
+ }
+}`, tmpDir)
+
+ args := &skel.CmdArgs{
+ ContainerID: "dummy",
+ Netns: nspath,
+ IfName: ifname,
+ StdinData: []byte(conf),
+ }
+
+ // Allocate the IP
+ r, _, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
+ return cmdAdd(args)
+ })
+ Expect(err).NotTo(HaveOccurred())
+ result, err := current.GetResult(r)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(result.IPs).To(HaveLen(1))
+ Expect(result.IPs[0].Address.IP).To(Equal(net.ParseIP("10.1.2.88")))
+ })
+
+ It("allocates custom IPs from multiple ranges", func() {
+ const ifname string = "eth0"
+ const nspath string = "/some/where"
+
+ tmpDir, err := ioutil.TempDir("", "host_local_artifacts")
+ Expect(err).NotTo(HaveOccurred())
+ defer os.RemoveAll(tmpDir)
+
+ err = ioutil.WriteFile(filepath.Join(tmpDir, "resolv.conf"), []byte("nameserver 192.0.2.3"), 0644)
+ Expect(err).NotTo(HaveOccurred())
+
+ conf := fmt.Sprintf(`{
+ "cniVersion": "0.3.1",
+ "name": "mynet",
+ "type": "ipvlan",
+ "master": "foo0",
+ "ipam": {
+ "type": "host-local",
+ "dataDir": "%s",
+ "ranges": [
+ { "subnet": "10.1.2.0/24" },
+ { "subnet": "10.1.3.0/24" }
+ ]
+ },
+ "args": {
+ "cni": {
+ "ips": ["10.1.2.88", "10.1.3.77"]
+ }
+ }
+}`, tmpDir)
+
+ args := &skel.CmdArgs{
+ ContainerID: "dummy",
+ Netns: nspath,
+ IfName: ifname,
+ StdinData: []byte(conf),
+ }
+
+ // Allocate the IP
+ r, _, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
+ return cmdAdd(args)
+ })
+ Expect(err).NotTo(HaveOccurred())
+ result, err := current.GetResult(r)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(result.IPs).To(HaveLen(2))
+ Expect(result.IPs[0].Address.IP).To(Equal(net.ParseIP("10.1.2.88")))
+ Expect(result.IPs[1].Address.IP).To(Equal(net.ParseIP("10.1.3.77")))
+ })
+
+ It("allocates custom IPs from multiple protocols", func() {
+ const ifname string = "eth0"
+ const nspath string = "/some/where"
+
+ tmpDir, err := ioutil.TempDir("", "host_local_artifacts")
+ Expect(err).NotTo(HaveOccurred())
+ defer os.RemoveAll(tmpDir)
+
+ err = ioutil.WriteFile(filepath.Join(tmpDir, "resolv.conf"), []byte("nameserver 192.0.2.3"), 0644)
+ Expect(err).NotTo(HaveOccurred())
+
+ conf := fmt.Sprintf(`{
+ "cniVersion": "0.3.1",
+ "name": "mynet",
+ "type": "ipvlan",
+ "master": "foo0",
+ "ipam": {
+ "type": "host-local",
+ "dataDir": "%s",
+ "ranges": [
+ { "subnet": "10.1.2.0/24" },
+ { "subnet": "2001:db8:1::/24" }
+ ]
+ },
+ "args": {
+ "cni": {
+ "ips": ["10.1.2.88", "2001:db8:1::999"]
+ }
+ }
+}`, tmpDir)
+
+ args := &skel.CmdArgs{
+ ContainerID: "dummy",
+ Netns: nspath,
+ IfName: ifname,
+ StdinData: []byte(conf),
+ }
+
+ // Allocate the IP
+ r, _, err := testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
+ return cmdAdd(args)
+ })
+ Expect(err).NotTo(HaveOccurred())
+ result, err := current.GetResult(r)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(result.IPs).To(HaveLen(2))
+ Expect(result.IPs[0].Address.IP).To(Equal(net.ParseIP("10.1.2.88")))
+ Expect(result.IPs[1].Address.IP).To(Equal(net.ParseIP("2001:db8:1::999")))
+ })
+
+ It("fails if a requested custom IP is not used", func() {
+ const ifname string = "eth0"
+ const nspath string = "/some/where"
+
+ tmpDir, err := ioutil.TempDir("", "host_local_artifacts")
+ Expect(err).NotTo(HaveOccurred())
+ defer os.RemoveAll(tmpDir)
+
+ conf := fmt.Sprintf(`{
+ "cniVersion": "0.3.1",
+ "name": "mynet",
+ "type": "ipvlan",
+ "master": "foo0",
+ "ipam": {
+ "type": "host-local",
+ "dataDir": "%s",
+ "ranges": [
+ { "subnet": "10.1.2.0/24" },
+ { "subnet": "10.1.3.0/24" }
+ ]
+ },
+ "args": {
+ "cni": {
+ "ips": ["10.1.2.88", "10.1.2.77"]
+ }
+ }
+}`, tmpDir)
+
+ args := &skel.CmdArgs{
+ ContainerID: "dummy",
+ Netns: nspath,
+ IfName: ifname,
+ StdinData: []byte(conf),
+ }
+
+ // Allocate the IP
+ _, _, err = testutils.CmdAddWithResult(nspath, ifname, []byte(conf), func() error {
+ return cmdAdd(args)
+ })
+ Expect(err).To(HaveOccurred())
+ // Need to match prefix, because ordering is not guaranteed
+ Expect(err.Error()).To(HavePrefix("failed to allocate all requested IPs: 10.1.2."))
+ })
+})
+
+func mustCIDR(s string) net.IPNet {
+ ip, n, err := net.ParseCIDR(s)
+ n.IP = ip
+ if err != nil {
+ Fail(err.Error())
+ }
+
+ return *n
+}
--- /dev/null
+# bridge plugin
+
+## Overview
+
+With bridge plugin, all containers (on the same host) are plugged into a bridge (virtual switch) that resides in the host network namespace.
+The containers receive one end of the veth pair with the other end connected to the bridge.
+An IP address is only assigned to one end of the veth pair -- one residing in the container.
+The bridge itself can also be assigned an IP address, turning it into a gateway for the containers.
+Alternatively, the bridge can function purely in L2 mode and would need to be bridged to the host network interface (if other than container-to-container communication on the same host is desired).
+
+The network configuration specifies the name of the bridge to be used.
+If the bridge is missing, the plugin will create one on first use and, if gateway mode is used, assign it an IP that was returned by IPAM plugin via the gateway field.
+
+## Example configuration
+```
+{
+ "name": "mynet",
+ "type": "bridge",
+ "bridge": "mynet0",
+ "isDefaultGateway": true,
+ "forceAddress": false,
+ "ipMasq": true,
+ "hairpinMode": true,
+ "ipam": {
+ "type": "host-local",
+ "subnet": "10.10.0.0/16"
+ }
+}
+```
+
+## Network configuration reference
+
+* `name` (string, required): the name of the network.
+* `type` (string, required): "bridge".
+* `bridge` (string, optional): name of the bridge to use/create. Defaults to "cni0".
+* `isGateway` (boolean, optional): assign an IP address to the bridge. Defaults to false.
+* `isDefaultGateway` (boolean, optional): Sets isGateway to true and makes the assigned IP the default route. Defaults to false.
+* `forceAddress` (boolean, optional): Indicates if a new IP address should be set if the previous value has been changed. Defaults to false.
+* `ipMasq` (boolean, optional): set up IP Masquerade on the host for traffic originating from this network and destined outside of it. Defaults to false.
+* `mtu` (integer, optional): explicitly set MTU to the specified value. Defaults to the value chosen by the kernel.
+* `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.
+* `promiscMode` (boolean, optional): set promiscuous mode on the bridge. Defaults to false.
--- /dev/null
+// Copyright 2014 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"
+ "errors"
+ "fmt"
+ "net"
+ "runtime"
+ "syscall"
+
+ "io/ioutil"
+ "log"
+ "os"
+ "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/ip"
+ "github.com/containernetworking/plugins/pkg/ipam"
+ "github.com/containernetworking/plugins/pkg/ns"
+ "github.com/containernetworking/plugins/pkg/utils"
+ "github.com/vishvananda/netlink"
+)
+
+const defaultBrName = "cni0"
+
+type IPAMArgs struct {
+ IPs []net.IP `json:"ips"`
+}
+
+/*
+ * Add structs needed to parse mesos labels
+ */
+type Mesos struct {
+ NetworkInfo NetworkInfo `json:"network_info"`
+}
+
+type NetworkInfo struct {
+ Name string `json:"name"`
+ Labels struct {
+ Labels []struct {
+ Key string `json:"key"`
+ Value string `json:"value"`
+ } `json:"labels,omitempty"`
+ } `json:"labels,omitempty"`
+}
+
+type NetConf struct {
+ types.NetConf
+ BrName string `json:"bridge"`
+ IsGW bool `json:"isGateway"`
+ IsDefaultGW bool `json:"isDefaultGateway"`
+ ForceAddress bool `json:"forceAddress"`
+ IPMasq bool `json:"ipMasq"`
+ MTU int `json:"mtu"`
+ HairpinMode bool `json:"hairpinMode"`
+ PromiscMode bool `json:"promiscMode"`
+ Args *struct {
+ A *IPAMArgs `json:"cni"`
+ Mesos Mesos `json:"org.apache.mesos,omitempty"`
+ } `json:"args"`
+}
+
+type gwInfo struct {
+ gws []net.IPNet
+ family int
+ defaultRouteFound bool
+}
+
+func init() {
+ // this ensures that main runs only on main thread (thread group leader).
+ // since namespace ops (unshare, setns) are done for a single thread, we
+ // must ensure that the goroutine does not jump from OS thread to thread
+ runtime.LockOSThread()
+}
+
+func loadNetConf(bytes []byte) (*NetConf, string, error) {
+ n := &NetConf{
+ BrName: defaultBrName,
+ }
+ if err := json.Unmarshal(bytes, n); err != nil {
+ return nil, "", fmt.Errorf("failed to load netconf: %v", err)
+ }
+
+ return n, n.CNIVersion, nil
+}
+
+// calcGateways processes the results from the IPAM plugin and does the
+// following for each IP family:
+// - Calculates and compiles a list of gateway addresses
+// - Adds a default route if needed
+func calcGateways(result *current.Result, n *NetConf) (*gwInfo, *gwInfo, error) {
+
+ gwsV4 := &gwInfo{}
+ gwsV6 := &gwInfo{}
+
+ for _, ipc := range result.IPs {
+
+ // Determine if this config is IPv4 or IPv6
+ var gws *gwInfo
+ defaultNet := &net.IPNet{}
+ switch {
+ case ipc.Address.IP.To4() != nil:
+ gws = gwsV4
+ gws.family = netlink.FAMILY_V4
+ defaultNet.IP = net.IPv4zero
+ case len(ipc.Address.IP) == net.IPv6len:
+ gws = gwsV6
+ gws.family = netlink.FAMILY_V6
+ defaultNet.IP = net.IPv6zero
+ default:
+ return nil, nil, fmt.Errorf("Unknown IP object: %v", ipc)
+ }
+ defaultNet.Mask = net.IPMask(defaultNet.IP)
+
+ // All IPs currently refer to the container interface
+ ipc.Interface = current.Int(2)
+
+ // If not provided, calculate the gateway address corresponding
+ // to the selected IP address
+ if ipc.Gateway == nil && n.IsGW {
+ ipc.Gateway = calcGatewayIP(&ipc.Address)
+ }
+
+ // Add a default route for this family using the current
+ // gateway address if necessary.
+ if n.IsDefaultGW && !gws.defaultRouteFound {
+ for _, route := range result.Routes {
+ if route.GW != nil && defaultNet.String() == route.Dst.String() {
+ gws.defaultRouteFound = true
+ break
+ }
+ }
+ if !gws.defaultRouteFound {
+ result.Routes = append(
+ result.Routes,
+ &types.Route{Dst: *defaultNet, GW: ipc.Gateway},
+ )
+ gws.defaultRouteFound = true
+ }
+ }
+
+ // Append this gateway address to the list of gateways
+ if n.IsGW {
+ gw := net.IPNet{
+ IP: ipc.Gateway,
+ Mask: ipc.Address.Mask,
+ }
+ gws.gws = append(gws.gws, gw)
+ }
+ }
+ return gwsV4, gwsV6, nil
+}
+
+func getBridgeAddr(br *netlink.Bridge, family int) 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)
+ }
+
+ println("Called getBridgeAddr ")
+
+ for j, addr := range addrs {
+ //fmt.Println("bridge addr %d %v\n", j, addr)
+ log.Println("j", j, "bridge addr", addr)
+
+ log.Println("just addr", addr.IP.String())
+ log.Println("just label", addr.Label)
+ }
+
+ println("Return from getBridgeAddr ")
+
+ return nil
+}
+
+func ensureBridgeAddr(br *netlink.Bridge, 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)
+ }
+
+ ipnStr := ipn.String()
+ for _, a := range addrs {
+
+ // string comp is actually easiest for doing IPNet comps
+ if a.IPNet.String() == ipnStr {
+ return nil
+ }
+
+ // Multiple IPv6 addresses are allowed on the bridge if the
+ // corresponding subnets do not overlap. For IPv4 or for
+ // overlapping IPv6 subnets, reconfigure the IP address if
+ // 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 {
+ return err
+ }
+ } else {
+ return fmt.Errorf("%q already has an IP address different from %v", br.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 nil
+}
+
+func deleteBridgeAddr(br *netlink.Bridge, 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 nil
+}
+
+func bridgeByName(name string) (*netlink.Bridge, error) {
+ l, err := netlink.LinkByName(name)
+ if err != nil {
+ return nil, fmt.Errorf("could not lookup %q: %v", name, err)
+ }
+ br, ok := l.(*netlink.Bridge)
+ if !ok {
+ return nil, fmt.Errorf("%q already exists but is not a bridge", name)
+ }
+ return br, nil
+}
+
+func ensureBridge(brName string, mtu int, promiscMode bool) (*netlink.Bridge, error) {
+ br := &netlink.Bridge{
+ LinkAttrs: netlink.LinkAttrs{
+ Name: brName,
+ MTU: mtu,
+ // Let kernel use default txqueuelen; leaving it unset
+ // means 0, and a zero-length TX queue messes up FIFO
+ // traffic shapers which use TX queue length as the
+ // default packet limit
+ TxQLen: -1,
+ },
+ }
+
+ err := netlink.LinkAdd(br)
+ if err != nil && err != syscall.EEXIST {
+ return nil, fmt.Errorf("could not add %q: %v", brName, err)
+ }
+
+ if promiscMode {
+ if err := netlink.SetPromiscOn(br); err != nil {
+ return nil, fmt.Errorf("could not set promiscuous mode on %q: %v", brName, err)
+ }
+ }
+
+ // Re-fetch link to read all attributes and if it already existed,
+ // ensure it's really a bridge with similar configuration
+ br, err = bridgeByName(brName)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := netlink.LinkSetUp(br); err != nil {
+ return nil, err
+ }
+
+ return br, nil
+}
+
+func setupVeth(netns ns.NetNS, br *netlink.Bridge, ifName string, mtu int, hairpinMode bool) (*current.Interface, *current.Interface, error) {
+ contIface := ¤t.Interface{}
+ hostIface := ¤t.Interface{}
+
+ err := netns.Do(func(hostNS ns.NetNS) error {
+ // create the veth pair in the container and move host end into host netns
+ hostVeth, containerVeth, err := ip.SetupVeth(ifName, mtu, hostNS)
+ if err != nil {
+ return err
+ }
+ contIface.Name = containerVeth.Name
+ contIface.Mac = containerVeth.HardwareAddr.String()
+ contIface.Sandbox = netns.Path()
+ hostIface.Name = hostVeth.Name
+ return nil
+ })
+ if err != nil {
+ return nil, nil, err
+ }
+
+ // need to lookup hostVeth again as its index has changed during ns move
+ hostVeth, err := netlink.LinkByName(hostIface.Name)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to lookup %q: %v", hostIface.Name, err)
+ }
+ hostIface.Mac = hostVeth.Attrs().HardwareAddr.String()
+
+ // connect host veth end to the bridge
+ if err := netlink.LinkSetMaster(hostVeth, br); err != nil {
+ return nil, nil, fmt.Errorf("failed to connect %q to bridge %v: %v", hostVeth.Attrs().Name, br.Attrs().Name, err)
+ }
+
+ // set hairpin mode
+ if err = netlink.LinkSetHairpin(hostVeth, hairpinMode); err != nil {
+ return nil, nil, fmt.Errorf("failed to setup hairpin mode for %v: %v", hostVeth.Attrs().Name, err)
+ }
+
+ return hostIface, contIface, nil
+}
+
+func calcGatewayIP(ipn *net.IPNet) net.IP {
+ nid := ipn.IP.Mask(ipn.Mask)
+ return ip.NextIP(nid)
+}
+
+func setupBridge(n *NetConf) (*netlink.Bridge, *current.Interface, error) {
+ // create bridge if necessary
+ br, err := ensureBridge(n.BrName, n.MTU, n.PromiscMode)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to create bridge %q: %v", n.BrName, err)
+ }
+
+ return br, ¤t.Interface{
+ Name: br.Attrs().Name,
+ Mac: br.Attrs().HardwareAddr.String(),
+ }, nil
+}
+
+// disableIPV6DAD disables IPv6 Duplicate Address Detection (DAD)
+// for an interface.
+func disableIPV6DAD(ifName string) error {
+ f := fmt.Sprintf("/proc/sys/net/ipv6/conf/%s/accept_dad", ifName)
+ return ioutil.WriteFile(f, []byte("0"), 0644)
+}
+
+func enableIPForward(family int) error {
+ if family == netlink.FAMILY_V4 {
+ return ip.EnableIP4Forward()
+ }
+ return ip.EnableIP6Forward()
+}
+
+func cmdAdd(args *skel.CmdArgs) error {
+ n, cniVersion, err := loadNetConf(args.StdinData)
+ if err != nil {
+ return err
+ }
+
+ if n.IsDefaultGW {
+ n.IsGW = true
+ }
+
+ if n.HairpinMode && n.PromiscMode {
+ return fmt.Errorf("cannot set hairpin mode and promiscous mode at the same time.")
+ }
+
+ br, brInterface, err := setupBridge(n)
+ 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()
+
+ hostInterface, containerInterface, err := setupVeth(netns, br, args.IfName, n.MTU, n.HairpinMode)
+ if err != nil {
+ return err
+ }
+
+ // run the IPAM plugin and get back the config to apply
+ r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData)
+ if err != nil {
+ return err
+ }
+
+ // Convert whatever the IPAM result was into the current Result type
+ result, err := current.NewResultFromResult(r)
+ if err != nil {
+ return err
+ }
+
+ println("add --- n.Args.A.IPArgs value is", n.Args)
+ println("add --- n.Args.A.IPArgs value is", n.Args.A)
+
+ if len(result.IPs) == 0 {
+ return errors.New("IPAM plugin returned missing IP config")
+ }
+
+ result.Interfaces = []*current.Interface{brInterface, hostInterface, containerInterface}
+
+ // Gather gateway information for each IP family
+ gwsV4, gwsV6, err := calcGateways(result, n)
+ if err != nil {
+ return err
+ }
+
+ // Configure the container hardware address and IP address(es)
+ if err := netns.Do(func(_ ns.NetNS) error {
+ // Disable IPv6 DAD just in case hairpin mode is enabled on the
+ // bridge. Hairpin mode causes echos of neighbor solicitation
+ // packets, which causes DAD failures.
+ // TODO: (short term) Disable DAD conditional on actual hairpin mode
+ // TODO: (long term) Use enhanced DAD when that becomes available in kernels.
+ if err := disableIPV6DAD(args.IfName); err != nil {
+ return err
+ }
+
+ if err := ipam.ConfigureIface(args.IfName, result); err != nil {
+ return err
+ }
+
+ if result.IPs[0].Address.IP.To4() != nil {
+ if err := ip.SetHWAddrByIP(args.IfName, result.IPs[0].Address.IP, nil /* TODO IPv6 */); err != nil {
+ return err
+ }
+ }
+
+ // Refetch the veth since its MAC address may changed
+ link, err := netlink.LinkByName(args.IfName)
+ if err != nil {
+ return fmt.Errorf("could not lookup %q: %v", args.IfName, err)
+ }
+ containerInterface.Mac = link.Attrs().HardwareAddr.String()
+
+ return nil
+ }); err != nil {
+ return err
+ }
+
+ if n.IsGW {
+ var firstV4Addr net.IP
+ // 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
+ }
+
+ err = ensureBridgeAddr(br, gws.family, &gw, n.ForceAddress)
+ if err != nil {
+ return fmt.Errorf("failed to set bridge addr: %v", err)
+ }
+ }
+
+ if gws.gws != nil {
+ if err = enableIPForward(gws.family); err != nil {
+ return fmt.Errorf("failed to enable forwarding: %v", err)
+ }
+ }
+ }
+
+ if firstV4Addr != nil {
+ if err := ip.SetHWAddrByIP(n.BrName, firstV4Addr, nil /* TODO IPv6 */); err != nil {
+ return err
+ }
+ }
+ }
+
+ if n.IPMasq {
+ chain := utils.FormatChainName(n.Name, args.ContainerID)
+ comment := utils.FormatComment(n.Name, args.ContainerID)
+ for _, ipc := range result.IPs {
+ if err = ip.SetupIPMasq(ip.Network(&ipc.Address), chain, comment); err != nil {
+ return err
+ }
+ }
+ }
+
+ // Refetch the bridge since its MAC address may change when the first
+ // veth is added or after its IP address is set
+ br, err = bridgeByName(n.BrName)
+ if err != nil {
+ return err
+ }
+ brInterface.Mac = br.Attrs().HardwareAddr.String()
+
+ result.DNS = n.DNS
+
+ /*
+ * Example of getting an environment variable supplied to the plugin
+ */
+ mccval := os.Getenv("MCCVAL")
+ println("bridge -- mccval is: ", mccval)
+
+ cni_args := os.Getenv("CNI_ARGS")
+ println("bridge -- CNI_ARGS is: ", cni_args)
+
+ /*
+ * Get values for supplied labels
+ */
+ labels := map[string]string{}
+
+ if n.Args != nil {
+ for k, label := range n.Args.Mesos.NetworkInfo.Labels.Labels {
+ labels[label.Key] = label.Value
+ println("bridge -- Map k (for)", k)
+ println("bridge -- Map k (for)", k, label.Key, label.Value)
+ }
+
+ println("CNI Args NetworkInfo: Net Name: ", n.Args.Mesos.NetworkInfo.Name)
+ }
+
+ for key, value := range labels {
+ println("Key:", key, "Value:", value)
+ }
+
+ derivedIP := result.IPs[0].Address.IP.To4()
+ println("derived IP is (string):", derivedIP.String())
+
+ err = getBridgeAddr(br, netlink.FAMILY_V4)
+ if err != nil {
+ println("failed to get bridge addr: ", err)
+ }
+
+ staticIP, s_found := labels["StaticIP"]
+ if s_found {
+ println("StaticIP is: ", staticIP)
+ log.Println("StaticIP is: ", staticIP)
+ } else {
+ println("StaticIP label NOT set")
+ println("Set StaticIP to derivedIP")
+ staticIP = derivedIP.String()
+ s_found = true
+ }
+
+/*
+ if net.ParseIP(staticIP) == derivedIP {
+ println(" derived ip(string) == parsed StaticIP ", derivedIP.String(), staticIP)
+ } else {
+ println(" derived ip(string) NOT = parsed StaticIP ", derivedIP.String(), staticIP)
+ }
+*/
+ if derivedIP.String() == staticIP {
+ println("derived ip == StaticIP ", derivedIP.String(), staticIP)
+ } else {
+ println("derived ip NOT = StaticIP ", derivedIP.String(), staticIP)
+ }
+
+ viaIP, v_found := labels["viaIP"]
+ if v_found {
+ println("viaIP is: ", viaIP)
+ log.Println("viaIP is: ", viaIP)
+ } else {
+ println("viaIP label NOT set")
+ }
+
+ Uplink, u_found := labels["Uplink"]
+ if u_found {
+ println("Uplink is: ", Uplink)
+ log.Println("Uplink is: ", Uplink)
+ } else {
+ println("Uplink label NOT set")
+ }
+
+ bull, found := labels["bull"]
+ if !found {
+ println("Hard to believe, but bull not found")
+ } else {
+ println("Found: ", bull)
+ }
+
+ /*
+ * If all 3 lables were not supplied, don't advertise anything
+ */
+ if s_found && v_found && u_found {
+ log.Println("AdvRoute: Uplink - BGP Advertises /32", staticIP, Uplink)
+
+ /*
+ * If supplied link can't be found, don't do anything
+ */
+ link, err := netlink.LinkByName(string(Uplink))
+ if err != nil {
+ log.Println("Can't obtain link index for: ", Uplink)
+ } else {
+ dst := &net.IPNet{
+ IP: net.ParseIP(staticIP),
+ Mask: net.CIDRMask(32, 32),
+ }
+
+ route := netlink.Route{
+ Dst: dst,
+ LinkIndex: link.Attrs().Index,
+ Gw: net.ParseIP(viaIP),
+ }
+
+ if err := netlink.RouteAdd(&route); err != nil {
+ fmt.Fprintln(os.Stderr, "There was an error adding netlink route: ", err)
+ if (err == syscall.EAGAIN) {
+ log.Println("ERRNO: eagain")
+ } else if (err == syscall.EEXIST) {
+ log.Println("ERRNO: route already exists")
+ } else {
+ log.Println("ERRNO: value is: ", (int(err.(syscall.Errno))))
+ }
+ }
+ }
+ }
+
+ return types.PrintResult(result, cniVersion)
+}
+
+func cmdDel(args *skel.CmdArgs) error {
+ n, _, err := loadNetConf(args.StdinData)
+ if err != nil {
+ return err
+ }
+
+ if err := ipam.ExecDel(n.IPAM.Type, args.StdinData); err != nil {
+ return err
+ }
+
+ if args.Netns == "" {
+ return nil
+ }
+
+ br, berr := bridgeByName(n.BrName)
+
+ err = getBridgeAddr(br, netlink.FAMILY_V4)
+ if berr != nil {
+ println("failed to get bridge addr: ", berr)
+ }
+
+ println("delete --- n.Args.A.IPArgs value is", n.Args)
+ println("delete --- n.Args.A.IPArgs value is", n.Args.A)
+ //println("delete --- n.Args.A.IPs[0] value is", n.Args.A.IPs[0])
+
+ // There is a netns so try to clean up. Delete can be called multiple times
+ // so don't return an error if the device is already removed.
+ // If the device isn't there then don't try to clean up IP masq either.
+ var ipn *net.IPNet
+ err = ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error {
+ var err error
+ ipn, err = ip.DelLinkByNameAddr(args.IfName, netlink.FAMILY_ALL)
+ if err != nil && err == ip.ErrLinkNotFound {
+ return nil
+ }
+ return err
+ })
+
+ if err != nil {
+ return err
+ }
+
+ if ipn != nil && n.IPMasq {
+ chain := utils.FormatChainName(n.Name, args.ContainerID)
+ comment := utils.FormatComment(n.Name, args.ContainerID)
+ err = ip.TeardownIPMasq(ipn, chain, comment)
+ }
+
+ /*
+ * Example of getting an environment variable supplied to the plugin
+ */
+ mccval := os.Getenv("MCCVAL")
+ println("bridge -- mccval is: ", mccval)
+
+ cni_args := os.Getenv("CNI_ARGS")
+ println("bridge -- CNI_ARGS was: ", cni_args)
+ cniIP := strings.Split((cni_args), "=")
+ println("bridge -- CNI_ARGS is: ", cniIP[1])
+
+ /*
+ * Get values for supplied labels
+ */
+ labels := map[string]string{}
+
+ if n.Args != nil {
+ for k, label := range n.Args.Mesos.NetworkInfo.Labels.Labels {
+ labels[label.Key] = label.Value
+ println("bridge -- Map k (for)", k)
+ println("bridge -- Map k (for)", k, label.Key, label.Value)
+ }
+
+ println("CNI Args NetworkInfo: Net Name: ", n.Args.Mesos.NetworkInfo.Name)
+ }
+
+ for key, value := range labels {
+ println("Key:", key, "Value:", value)
+ }
+
+ staticIP, s_found := labels["StaticIP"]
+ if s_found {
+ println("StaticIP is: ", staticIP)
+ log.Println("StaticIP is: ", staticIP)
+ } else {
+ println("StaticIP label NOT set")
+ println("Set StaticIP to derivedIP")
+ //staticIP = derivedIP.String()
+ staticIP = cniIP[1]
+ s_found = true
+ }
+
+ viaIP, v_found := labels["viaIP"]
+ if v_found {
+ println("viaIP is: ", viaIP)
+ log.Println("viaIP is: ", viaIP)
+ } else {
+ println("viaIP label NOT set")
+ }
+
+ Uplink, u_found := labels["Uplink"]
+ if u_found {
+ println("Uplink is: ", Uplink)
+ log.Println("Uplink is: ", Uplink)
+ } else {
+ println("Uplink label NOT set")
+ }
+
+ bull, found := labels["bull"]
+ if !found {
+ println("Hard to believe, but bull not found")
+ } else {
+ println("Found: ", bull)
+ }
+
+ /*
+ * If all 3 lables were not supplied, don't advertise anything
+ */
+ if s_found && v_found && u_found {
+ log.Println("AdvRoute: Uplink - BGP Advertises /32", staticIP, Uplink)
+
+ /*
+ * If supplied link can't be found, don't do anything
+ */
+ link, err := netlink.LinkByName(string(Uplink))
+ if err != nil {
+ log.Println("Can't obtain link index for: ", Uplink)
+ } else {
+ dst := &net.IPNet{
+ IP: net.ParseIP(staticIP),
+ Mask: net.CIDRMask(32, 32),
+ }
+
+ route := netlink.Route{
+ Dst: dst,
+ LinkIndex: link.Attrs().Index,
+ Gw: net.ParseIP(viaIP),
+ }
+
+ if err := netlink.RouteDel(&route); err != nil {
+ fmt.Fprintln(os.Stderr, "There was an error adding netlink route: ", err)
+ if (err == syscall.EAGAIN) {
+ log.Println("ERRNO: eagain")
+ } else if (err == syscall.EEXIST) {
+ log.Println("ERRNO: route already exists")
+ } else {
+ log.Println("ERRNO: value is: ", (int(err.(syscall.Errno))))
+ }
+ }
+ }
+ }
+ return err
+}
+
+func main() {
+ skel.PluginMain(cmdAdd, cmdDel, version.All)
+}
--- /dev/null
+// Copyright 2014 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"
+ "errors"
+ "fmt"
+ "net"
+ "runtime"
+ "syscall"
+
+ "io/ioutil"
+
+ "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/vishvananda/netlink"
+)
+
+const defaultBrName = "cni0"
+
+type NetConf struct {
+ types.NetConf
+ BrName string `json:"bridge"`
+ IsGW bool `json:"isGateway"`
+ IsDefaultGW bool `json:"isDefaultGateway"`
+ ForceAddress bool `json:"forceAddress"`
+ IPMasq bool `json:"ipMasq"`
+ MTU int `json:"mtu"`
+ HairpinMode bool `json:"hairpinMode"`
+ PromiscMode bool `json:"promiscMode"`
+ Args *struct {
+ A *IPAMArgs `json:"cni"`
+ Mesos Mesos `json:"org.apache.mesos,omitempty"`
+ } `json:"args"`
+}
+
+type gwInfo struct {
+ gws []net.IPNet
+ family int
+ defaultRouteFound bool
+}
+
+func init() {
+ // this ensures that main runs only on main thread (thread group leader).
+ // since namespace ops (unshare, setns) are done for a single thread, we
+ // must ensure that the goroutine does not jump from OS thread to thread
+ runtime.LockOSThread()
+}
+
+func loadNetConf(bytes []byte) (*NetConf, string, error) {
+ n := &NetConf{
+ BrName: defaultBrName,
+ }
+ if err := json.Unmarshal(bytes, n); err != nil {
+ return nil, "", fmt.Errorf("failed to load netconf: %v", err)
+ }
+ return n, n.CNIVersion, nil
+}
+
+// calcGateways processes the results from the IPAM plugin and does the
+// following for each IP family:
+// - Calculates and compiles a list of gateway addresses
+// - Adds a default route if needed
+func calcGateways(result *current.Result, n *NetConf) (*gwInfo, *gwInfo, error) {
+
+ gwsV4 := &gwInfo{}
+ gwsV6 := &gwInfo{}
+
+ for _, ipc := range result.IPs {
+
+ // Determine if this config is IPv4 or IPv6
+ var gws *gwInfo
+ defaultNet := &net.IPNet{}
+ switch {
+ case ipc.Address.IP.To4() != nil:
+ gws = gwsV4
+ gws.family = netlink.FAMILY_V4
+ defaultNet.IP = net.IPv4zero
+ case len(ipc.Address.IP) == net.IPv6len:
+ gws = gwsV6
+ gws.family = netlink.FAMILY_V6
+ defaultNet.IP = net.IPv6zero
+ default:
+ return nil, nil, fmt.Errorf("Unknown IP object: %v", ipc)
+ }
+ defaultNet.Mask = net.IPMask(defaultNet.IP)
+
+ // All IPs currently refer to the container interface
+ ipc.Interface = current.Int(2)
+
+ // If not provided, calculate the gateway address corresponding
+ // to the selected IP address
+ if ipc.Gateway == nil && n.IsGW {
+ ipc.Gateway = calcGatewayIP(&ipc.Address)
+ }
+
+ // Add a default route for this family using the current
+ // gateway address if necessary.
+ if n.IsDefaultGW && !gws.defaultRouteFound {
+ for _, route := range result.Routes {
+ if route.GW != nil && defaultNet.String() == route.Dst.String() {
+ gws.defaultRouteFound = true
+ break
+ }
+ }
+ if !gws.defaultRouteFound {
+ result.Routes = append(
+ result.Routes,
+ &types.Route{Dst: *defaultNet, GW: ipc.Gateway},
+ )
+ gws.defaultRouteFound = true
+ }
+ }
+
+ // Append this gateway address to the list of gateways
+ if n.IsGW {
+ gw := net.IPNet{
+ IP: ipc.Gateway,
+ Mask: ipc.Address.Mask,
+ }
+ gws.gws = append(gws.gws, gw)
+ }
+ }
+ return gwsV4, gwsV6, nil
+}
+
+func ensureBridgeAddr(br *netlink.Bridge, 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)
+ }
+
+ ipnStr := ipn.String()
+ for _, a := range addrs {
+
+ // string comp is actually easiest for doing IPNet comps
+ if a.IPNet.String() == ipnStr {
+ return nil
+ }
+
+ // Multiple IPv6 addresses are allowed on the bridge if the
+ // corresponding subnets do not overlap. For IPv4 or for
+ // overlapping IPv6 subnets, reconfigure the IP address if
+ // 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 {
+ return err
+ }
+ } else {
+ return fmt.Errorf("%q already has an IP address different from %v", br.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 nil
+}
+
+func deleteBridgeAddr(br *netlink.Bridge, 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 nil
+}
+
+func bridgeByName(name string) (*netlink.Bridge, error) {
+ l, err := netlink.LinkByName(name)
+ if err != nil {
+ return nil, fmt.Errorf("could not lookup %q: %v", name, err)
+ }
+ br, ok := l.(*netlink.Bridge)
+ if !ok {
+ return nil, fmt.Errorf("%q already exists but is not a bridge", name)
+ }
+ return br, nil
+}
+
+func ensureBridge(brName string, mtu int, promiscMode bool) (*netlink.Bridge, error) {
+ br := &netlink.Bridge{
+ LinkAttrs: netlink.LinkAttrs{
+ Name: brName,
+ MTU: mtu,
+ // Let kernel use default txqueuelen; leaving it unset
+ // means 0, and a zero-length TX queue messes up FIFO
+ // traffic shapers which use TX queue length as the
+ // default packet limit
+ TxQLen: -1,
+ },
+ }
+
+ err := netlink.LinkAdd(br)
+ if err != nil && err != syscall.EEXIST {
+ return nil, fmt.Errorf("could not add %q: %v", brName, err)
+ }
+
+ if promiscMode {
+ if err := netlink.SetPromiscOn(br); err != nil {
+ return nil, fmt.Errorf("could not set promiscuous mode on %q: %v", brName, err)
+ }
+ }
+
+ // Re-fetch link to read all attributes and if it already existed,
+ // ensure it's really a bridge with similar configuration
+ br, err = bridgeByName(brName)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := netlink.LinkSetUp(br); err != nil {
+ return nil, err
+ }
+
+ return br, nil
+}
+
+func setupVeth(netns ns.NetNS, br *netlink.Bridge, ifName string, mtu int, hairpinMode bool) (*current.Interface, *current.Interface, error) {
+ contIface := ¤t.Interface{}
+ hostIface := ¤t.Interface{}
+
+ err := netns.Do(func(hostNS ns.NetNS) error {
+ // create the veth pair in the container and move host end into host netns
+ hostVeth, containerVeth, err := ip.SetupVeth(ifName, mtu, hostNS)
+ if err != nil {
+ return err
+ }
+ contIface.Name = containerVeth.Name
+ contIface.Mac = containerVeth.HardwareAddr.String()
+ contIface.Sandbox = netns.Path()
+ hostIface.Name = hostVeth.Name
+ return nil
+ })
+ if err != nil {
+ return nil, nil, err
+ }
+
+ // need to lookup hostVeth again as its index has changed during ns move
+ hostVeth, err := netlink.LinkByName(hostIface.Name)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to lookup %q: %v", hostIface.Name, err)
+ }
+ hostIface.Mac = hostVeth.Attrs().HardwareAddr.String()
+
+ // connect host veth end to the bridge
+ if err := netlink.LinkSetMaster(hostVeth, br); err != nil {
+ return nil, nil, fmt.Errorf("failed to connect %q to bridge %v: %v", hostVeth.Attrs().Name, br.Attrs().Name, err)
+ }
+
+ // set hairpin mode
+ if err = netlink.LinkSetHairpin(hostVeth, hairpinMode); err != nil {
+ return nil, nil, fmt.Errorf("failed to setup hairpin mode for %v: %v", hostVeth.Attrs().Name, err)
+ }
+
+ return hostIface, contIface, nil
+}
+
+func calcGatewayIP(ipn *net.IPNet) net.IP {
+ nid := ipn.IP.Mask(ipn.Mask)
+ return ip.NextIP(nid)
+}
+
+func setupBridge(n *NetConf) (*netlink.Bridge, *current.Interface, error) {
+ // create bridge if necessary
+ br, err := ensureBridge(n.BrName, n.MTU, n.PromiscMode)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to create bridge %q: %v", n.BrName, err)
+ }
+
+ return br, ¤t.Interface{
+ Name: br.Attrs().Name,
+ Mac: br.Attrs().HardwareAddr.String(),
+ }, nil
+}
+
+// disableIPV6DAD disables IPv6 Duplicate Address Detection (DAD)
+// for an interface.
+func disableIPV6DAD(ifName string) error {
+ f := fmt.Sprintf("/proc/sys/net/ipv6/conf/%s/accept_dad", ifName)
+ return ioutil.WriteFile(f, []byte("0"), 0644)
+}
+
+func enableIPForward(family int) error {
+ if family == netlink.FAMILY_V4 {
+ return ip.EnableIP4Forward()
+ }
+ return ip.EnableIP6Forward()
+}
+
+func cmdAdd(args *skel.CmdArgs) error {
+ n, cniVersion, err := loadNetConf(args.StdinData)
+ if err != nil {
+ return err
+ }
+
+ if n.IsDefaultGW {
+ n.IsGW = true
+ }
+
+ if n.HairpinMode && n.PromiscMode {
+ return fmt.Errorf("cannot set hairpin mode and promiscous mode at the same time.")
+ }
+
+ br, brInterface, err := setupBridge(n)
+ 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()
+
+ hostInterface, containerInterface, err := setupVeth(netns, br, args.IfName, n.MTU, n.HairpinMode)
+ if err != nil {
+ return err
+ }
+
+ // run the IPAM plugin and get back the config to apply
+ r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData)
+ if err != nil {
+ return err
+ }
+
+ // Convert whatever the IPAM result was into the current Result type
+ result, err := current.NewResultFromResult(r)
+ if err != nil {
+ return err
+ }
+
+ if len(result.IPs) == 0 {
+ return errors.New("IPAM plugin returned missing IP config")
+ }
+
+ result.Interfaces = []*current.Interface{brInterface, hostInterface, containerInterface}
+
+ // Gather gateway information for each IP family
+ gwsV4, gwsV6, err := calcGateways(result, n)
+ if err != nil {
+ return err
+ }
+
+ // Configure the container hardware address and IP address(es)
+ if err := netns.Do(func(_ ns.NetNS) error {
+ // Disable IPv6 DAD just in case hairpin mode is enabled on the
+ // bridge. Hairpin mode causes echos of neighbor solicitation
+ // packets, which causes DAD failures.
+ // TODO: (short term) Disable DAD conditional on actual hairpin mode
+ // TODO: (long term) Use enhanced DAD when that becomes available in kernels.
+ if err := disableIPV6DAD(args.IfName); err != nil {
+ return err
+ }
+
+ if err := ipam.ConfigureIface(args.IfName, result); err != nil {
+ return err
+ }
+
+ if result.IPs[0].Address.IP.To4() != nil {
+ if err := ip.SetHWAddrByIP(args.IfName, result.IPs[0].Address.IP, nil /* TODO IPv6 */); err != nil {
+ return err
+ }
+ }
+
+ // Refetch the veth since its MAC address may changed
+ link, err := netlink.LinkByName(args.IfName)
+ if err != nil {
+ return fmt.Errorf("could not lookup %q: %v", args.IfName, err)
+ }
+ containerInterface.Mac = link.Attrs().HardwareAddr.String()
+
+ return nil
+ }); err != nil {
+ return err
+ }
+
+ if n.IsGW {
+ var firstV4Addr net.IP
+ // 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
+ }
+
+ err = ensureBridgeAddr(br, gws.family, &gw, n.ForceAddress)
+ if err != nil {
+ return fmt.Errorf("failed to set bridge addr: %v", err)
+ }
+ }
+
+ if gws.gws != nil {
+ if err = enableIPForward(gws.family); err != nil {
+ return fmt.Errorf("failed to enable forwarding: %v", err)
+ }
+ }
+ }
+
+ if firstV4Addr != nil {
+ if err := ip.SetHWAddrByIP(n.BrName, firstV4Addr, nil /* TODO IPv6 */); err != nil {
+ return err
+ }
+ }
+ }
+
+ if n.IPMasq {
+ chain := utils.FormatChainName(n.Name, args.ContainerID)
+ comment := utils.FormatComment(n.Name, args.ContainerID)
+ for _, ipc := range result.IPs {
+ if err = ip.SetupIPMasq(ip.Network(&ipc.Address), chain, comment); err != nil {
+ return err
+ }
+ }
+ }
+
+ // Refetch the bridge since its MAC address may change when the first
+ // veth is added or after its IP address is set
+ br, err = bridgeByName(n.BrName)
+ if err != nil {
+ return err
+ }
+ brInterface.Mac = br.Attrs().HardwareAddr.String()
+
+ result.DNS = n.DNS
+
+ return types.PrintResult(result, cniVersion)
+}
+
+func cmdDel(args *skel.CmdArgs) error {
+ n, _, err := loadNetConf(args.StdinData)
+ if err != nil {
+ return err
+ }
+
+ if err := ipam.ExecDel(n.IPAM.Type, args.StdinData); err != nil {
+ return err
+ }
+
+ if args.Netns == "" {
+ return nil
+ }
+
+ // There is a netns so try to clean up. Delete can be called multiple times
+ // so don't return an error if the device is already removed.
+ // If the device isn't there then don't try to clean up IP masq either.
+ var ipn *net.IPNet
+ err = ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error {
+ var err error
+ ipn, err = ip.DelLinkByNameAddr(args.IfName, netlink.FAMILY_ALL)
+ if err != nil && err == ip.ErrLinkNotFound {
+ return nil
+ }
+ return err
+ })
+
+ if err != nil {
+ return err
+ }
+
+ if ipn != nil && n.IPMasq {
+ chain := utils.FormatChainName(n.Name, args.ContainerID)
+ comment := utils.FormatComment(n.Name, args.ContainerID)
+ err = ip.TeardownIPMasq(ipn, chain, comment)
+ }
+
+ return err
+}
+
+func main() {
+ skel.PluginMain(cmdAdd, cmdDel, version.All)
+}
--- /dev/null
+// 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 main
+
+import (
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+
+ "testing"
+)
+
+func TestBridge(t *testing.T) {
+ RegisterFailHandler(Fail)
+ RunSpecs(t, "bridge Suite")
+}
--- /dev/null
+// Copyright 2015 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"
+ "net"
+ "strings"
+
+ "github.com/containernetworking/cni/pkg/skel"
+ "github.com/containernetworking/cni/pkg/types"
+ "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/containernetworking/plugins/pkg/utils/hwaddr"
+
+ "github.com/vishvananda/netlink"
+
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+)
+
+const (
+ BRNAME = "bridge0"
+ IFNAME = "eth0"
+)
+
+// testCase defines the CNI network configuration and the expected
+// bridge addresses for a test case.
+type testCase struct {
+ cniVersion string // CNI Version
+ subnet string // Single subnet config: Subnet CIDR
+ gateway string // Single subnet config: Gateway
+ ranges []rangeInfo // Ranges list (multiple subnets config)
+ isGW bool
+ expGWCIDRs []string // Expected gateway addresses in CIDR form
+}
+
+// Range definition for each entry in the ranges list
+type rangeInfo struct {
+ subnet string
+ gateway string
+}
+
+// netConf() creates a NetConf structure for a test case.
+func (tc testCase) netConf() *NetConf {
+ return &NetConf{
+ NetConf: types.NetConf{
+ CNIVersion: tc.cniVersion,
+ Name: "testConfig",
+ Type: "bridge",
+ },
+ BrName: BRNAME,
+ IsGW: tc.isGW,
+ IPMasq: false,
+ MTU: 5000,
+ }
+}
+
+// Snippets for generating a JSON network configuration string.
+const (
+ netConfStr = `
+ "cniVersion": "%s",
+ "name": "testConfig",
+ "type": "bridge",
+ "bridge": "%s",
+ "isDefaultGateway": true,
+ "ipMasq": false`
+
+ ipamStartStr = `,
+ "ipam": {
+ "type": "host-local"`
+
+ // Single subnet configuration (legacy)
+ subnetConfStr = `,
+ "subnet": "%s"`
+ gatewayConfStr = `,
+ "gateway": "%s"`
+
+ // Ranges (multiple subnets) configuration
+ rangesStartStr = `,
+ "ranges": [`
+ rangeSubnetConfStr = `
+ {
+ "subnet": "%s"
+ }`
+ rangeSubnetGWConfStr = `
+ {
+ "subnet": "%s",
+ "gateway": "%s"
+ }`
+ rangesEndStr = `
+ ]`
+
+ ipamEndStr = `
+ }`
+)
+
+// netConfJSON() generates a JSON network configuration string
+// for a test case.
+func (tc testCase) netConfJSON() string {
+ conf := fmt.Sprintf(netConfStr, tc.cniVersion, BRNAME)
+ if tc.subnet != "" || tc.ranges != nil {
+ conf += ipamStartStr
+ if tc.subnet != "" {
+ conf += tc.subnetConfig()
+ }
+ if tc.ranges != nil {
+ conf += tc.rangesConfig()
+ }
+ conf += ipamEndStr
+ }
+ return "{" + conf + "\n}"
+}
+
+func (tc testCase) subnetConfig() string {
+ conf := fmt.Sprintf(subnetConfStr, tc.subnet)
+ if tc.gateway != "" {
+ conf += fmt.Sprintf(gatewayConfStr, tc.gateway)
+ }
+ return conf
+}
+
+func (tc testCase) rangesConfig() string {
+ conf := rangesStartStr
+ for i, tcRange := range tc.ranges {
+ if i > 0 {
+ conf += ","
+ }
+ if tcRange.gateway != "" {
+ conf += fmt.Sprintf(rangeSubnetGWConfStr, tcRange.subnet, tcRange.gateway)
+ } else {
+ conf += fmt.Sprintf(rangeSubnetConfStr, tcRange.subnet)
+ }
+ }
+ return conf + rangesEndStr
+}
+
+// createCmdArgs generates network configuration and creates command
+// arguments for a test case.
+func (tc testCase) createCmdArgs(targetNS ns.NetNS) *skel.CmdArgs {
+ conf := tc.netConfJSON()
+ return &skel.CmdArgs{
+ ContainerID: "dummy",
+ Netns: targetNS.Path(),
+ IfName: IFNAME,
+ StdinData: []byte(conf),
+ }
+}
+
+// expectedCIDRs determines the IPv4 and IPv6 CIDRs in which the resulting
+// addresses are expected to be contained.
+func (tc testCase) expectedCIDRs() ([]*net.IPNet, []*net.IPNet) {
+ var cidrsV4, cidrsV6 []*net.IPNet
+ appendSubnet := func(subnet string) {
+ ip, cidr, err := net.ParseCIDR(subnet)
+ Expect(err).NotTo(HaveOccurred())
+ if ipVersion(ip) == "4" {
+ cidrsV4 = append(cidrsV4, cidr)
+ } else {
+ cidrsV6 = append(cidrsV6, cidr)
+ }
+ }
+ if tc.subnet != "" {
+ appendSubnet(tc.subnet)
+ }
+ for _, r := range tc.ranges {
+ appendSubnet(r.subnet)
+ }
+ return cidrsV4, cidrsV6
+}
+
+// delBridgeAddrs() deletes addresses from the bridge
+func delBridgeAddrs(testNS ns.NetNS) {
+ err := testNS.Do(func(ns.NetNS) error {
+ defer GinkgoRecover()
+
+ br, err := netlink.LinkByName(BRNAME)
+ Expect(err).NotTo(HaveOccurred())
+ 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 ipVersion(ip net.IP) string {
+ if ip.To4() != nil {
+ return "4"
+ }
+ return "6"
+}
+
+type cmdAddDelTester interface {
+ setNS(testNS ns.NetNS, targetNS ns.NetNS)
+ cmdAddTest(tc testCase)
+ cmdDelTest(tc testCase)
+}
+
+func testerByVersion(version string) cmdAddDelTester {
+ switch {
+ case strings.HasPrefix(version, "0.3."):
+ return &testerV03x{}
+ default:
+ return &testerV01xOr02x{}
+ }
+}
+
+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) {
+ // Generate network config and command arguments
+ tester.args = tc.createCmdArgs(tester.targetNS)
+
+ // Execute cmdADD on the plugin
+ var result *current.Result
+ err := tester.testNS.Do(func(ns.NetNS) error {
+ defer GinkgoRecover()
+
+ r, raw, err := testutils.CmdAddWithResult(tester.targetNS.Path(), IFNAME, tester.args.StdinData, 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())
+
+ Expect(len(result.Interfaces)).To(Equal(3))
+ Expect(result.Interfaces[0].Name).To(Equal(BRNAME))
+ Expect(result.Interfaces[2].Name).To(Equal(IFNAME))
+
+ // 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))
+
+ // Ensure bridge has expected gateway address(es)
+ addrs, err := netlink.AddrList(link, 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())
+ if ip.To4() != nil {
+ hwAddr := fmt.Sprintf("%s", link.Attrs().HardwareAddr)
+ Expect(hwAddr).To(HavePrefix(hwaddr.PrivateMACPrefixString))
+ }
+
+ 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())
+ 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
+ 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()
+ if expCIDRsV4 != nil {
+ hwAddr := fmt.Sprintf("%s", link.Attrs().HardwareAddr)
+ Expect(hwAddr).To(HavePrefix(hwaddr.PrivateMACPrefixString))
+ }
+ 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(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())
+}
+
+func (tester *testerV03x) cmdDelTest(tc testCase) {
+ err := tester.testNS.Do(func(ns.NetNS) error {
+ defer GinkgoRecover()
+
+ err := testutils.CmdDelWithResult(tester.targetNS.Path(), IFNAME, 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
+ })
+}
+
+type testerV01xOr02x struct {
+ testNS ns.NetNS
+ targetNS ns.NetNS
+ args *skel.CmdArgs
+ vethName string
+}
+
+func (tester *testerV01xOr02x) setNS(testNS ns.NetNS, targetNS ns.NetNS) {
+ tester.testNS = testNS
+ tester.targetNS = targetNS
+}
+
+func (tester *testerV01xOr02x) cmdAddTest(tc testCase) {
+ // Generate network config and calculate gateway addresses
+ tester.args = tc.createCmdArgs(tester.targetNS)
+
+ // Execute cmdADD on the plugin
+ var result *types020.Result
+ err := tester.testNS.Do(func(ns.NetNS) error {
+ defer GinkgoRecover()
+
+ r, raw, err := testutils.CmdAddWithResult(tester.targetNS.Path(), IFNAME, tester.args.StdinData, func() error {
+ return cmdAdd(tester.args)
+ })
+ Expect(err).NotTo(HaveOccurred())
+ Expect(strings.Index(string(raw), "\"ip\":")).Should(BeNumerically(">", 0))
+
+ // We expect a version 0.1.0 result
+ result, err = types020.GetResult(r)
+ Expect(err).NotTo(HaveOccurred())
+
+ // Make sure bridge link exists
+ link, err := netlink.LinkByName(BRNAME)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(link.Attrs().Name).To(Equal(BRNAME))
+ Expect(link).To(BeAssignableToTypeOf(&netlink.Bridge{}))
+
+ // Ensure bridge has expected gateway address(es)
+ addrs, err := netlink.AddrList(link, 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())
+ if ip.To4() != nil {
+ hwAddr := fmt.Sprintf("%s", link.Attrs().HardwareAddr)
+ Expect(hwAddr).To(HavePrefix(hwaddr.PrivateMACPrefixString))
+ }
+
+ 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; can't
+ // check the for the specific link since version 0.1.0
+ // doesn't report interfaces
+ links, err := netlink.LinkList()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(len(links)).To(Equal(3)) // Bridge, veth, and loopback
+ 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()
+ if expCIDRsV4 != nil {
+ hwAddr := fmt.Sprintf("%s", link.Attrs().HardwareAddr)
+ Expect(hwAddr).To(HavePrefix(hwaddr.PrivateMACPrefixString))
+ }
+ 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(err).NotTo(HaveOccurred())
+ Expect(len(addrs)).To(Equal(len(expCIDRsV6) + 1)) // Link local address is automatic
+
+ // Ensure the default route
+ routes, err := netlink.RouteList(link, 0)
+ Expect(err).NotTo(HaveOccurred())
+
+ var defaultRouteFound bool
+ for _, cidr := range tc.expGWCIDRs {
+ gwIP, _, err := net.ParseCIDR(cidr)
+ Expect(err).NotTo(HaveOccurred())
+ for _, route := range routes {
+ defaultRouteFound = (route.Dst == nil && route.Src == nil && route.Gw.Equal(gwIP))
+ if defaultRouteFound {
+ break
+ }
+ }
+ Expect(defaultRouteFound).To(Equal(true))
+ }
+
+ return nil
+ })
+ Expect(err).NotTo(HaveOccurred())
+}
+
+func (tester *testerV01xOr02x) cmdDelTest(tc testCase) {
+ err := tester.testNS.Do(func(ns.NetNS) error {
+ defer GinkgoRecover()
+
+ err := testutils.CmdDelWithResult(tester.targetNS.Path(), IFNAME, func() error {
+ return cmdDel(tester.args)
+ })
+ Expect(err).NotTo(HaveOccurred())
+ return nil
+ })
+ Expect(err).NotTo(HaveOccurred())
+
+ // Make sure the container veth has been deleted; cannot check
+ // host veth as version 0.1.0 can't report its name
+ err = tester.testNS.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())
+}
+
+func cmdAddDelTest(testNS ns.NetNS, tc testCase) {
+ // Get a Add/Del tester based on test case version
+ tester := testerByVersion(tc.cniVersion)
+
+ targetNS, err := ns.NewNS()
+ Expect(err).NotTo(HaveOccurred())
+ defer targetNS.Close()
+ tester.setNS(testNS, targetNS)
+
+ // Test IP allocation
+ tester.cmdAddTest(tc)
+
+ // Test IP Release
+ tester.cmdDelTest(tc)
+
+ // Clean up bridge addresses for next test case
+ delBridgeAddrs(testNS)
+}
+
+var _ = Describe("bridge Operations", func() {
+ var originalNS ns.NetNS
+
+ BeforeEach(func() {
+ // Create a new NetNS so we don't modify the host
+ var err error
+ originalNS, err = ns.NewNS()
+ Expect(err).NotTo(HaveOccurred())
+ })
+
+ AfterEach(func() {
+ Expect(originalNS.Close()).To(Succeed())
+ })
+
+ It("creates a bridge", func() {
+ conf := testCase{cniVersion: "0.3.1"}.netConf()
+ err := originalNS.Do(func(ns.NetNS) error {
+ defer GinkgoRecover()
+
+ bridge, _, err := setupBridge(conf)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(bridge.Attrs().Name).To(Equal(BRNAME))
+
+ // Double check that the link was added
+ link, err := netlink.LinkByName(BRNAME)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(link.Attrs().Name).To(Equal(BRNAME))
+ Expect(link.Attrs().Promisc).To(Equal(0))
+ return nil
+ })
+ Expect(err).NotTo(HaveOccurred())
+ })
+
+ It("handles an existing bridge", func() {
+ err := originalNS.Do(func(ns.NetNS) error {
+ defer GinkgoRecover()
+
+ err := netlink.LinkAdd(&netlink.Bridge{
+ LinkAttrs: netlink.LinkAttrs{
+ Name: BRNAME,
+ },
+ })
+ Expect(err).NotTo(HaveOccurred())
+ link, err := netlink.LinkByName(BRNAME)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(link.Attrs().Name).To(Equal(BRNAME))
+ ifindex := link.Attrs().Index
+
+ tc := testCase{cniVersion: "0.3.1", isGW: false}
+ conf := tc.netConf()
+
+ bridge, _, err := setupBridge(conf)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(bridge.Attrs().Name).To(Equal(BRNAME))
+ Expect(bridge.Attrs().Index).To(Equal(ifindex))
+
+ // Double check that the link has the same ifindex
+ link, err = netlink.LinkByName(BRNAME)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(link.Attrs().Name).To(Equal(BRNAME))
+ Expect(link.Attrs().Index).To(Equal(ifindex))
+ return nil
+ })
+ Expect(err).NotTo(HaveOccurred())
+ })
+
+ It("configures and deconfigures a bridge and veth with default route 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"},
+ },
+ {
+ // IPv6 only
+ 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",
+ },
+ },
+ {
+ // 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",
+ },
+ },
+ }
+ for _, tc := range testCases {
+ tc.cniVersion = "0.3.0"
+ cmdAddDelTest(originalNS, tc)
+ }
+ })
+
+ It("configures and deconfigures a bridge and veth with default route with ADD/DEL for 0.3.1 config", func() {
+ testCases := []testCase{
+ {
+ // IPv4 only
+ subnet: "10.1.2.0/24",
+ expGWCIDRs: []string{"10.1.2.1/24"},
+ },
+ {
+ // IPv6 only
+ 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.3.1"
+ cmdAddDelTest(originalNS, tc)
+ }
+ })
+
+ It("deconfigures an unconfigured bridge with DEL", func() {
+ tc := testCase{
+ cniVersion: "0.3.0",
+ subnet: "10.1.2.0/24",
+ expGWCIDRs: []string{"10.1.2.1/24"},
+ }
+
+ tester := testerV03x{}
+ targetNS, err := ns.NewNS()
+ Expect(err).NotTo(HaveOccurred())
+ defer targetNS.Close()
+ tester.setNS(originalNS, targetNS)
+ tester.args = tc.createCmdArgs(targetNS)
+
+ // Execute cmdDEL on the plugin, expect no errors
+ tester.cmdDelTest(tc)
+ })
+
+ It("configures and deconfigures a bridge and veth with default route with ADD/DEL for 0.1.0 config", func() {
+ testCases := []testCase{
+ {
+ // IPv4 only
+ subnet: "10.1.2.0/24",
+ expGWCIDRs: []string{"10.1.2.1/24"},
+ },
+ {
+ // IPv6 only
+ 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.1.0"
+ cmdAddDelTest(originalNS, tc)
+ }
+ })
+
+ It("ensure bridge address", func() {
+ conf := testCase{cniVersion: "0.3.1", isGW: true}.netConf()
+
+ testCases := []struct {
+ gwCIDRFirst string
+ gwCIDRSecond string
+ }{
+ {
+ // IPv4
+ gwCIDRFirst: "10.0.0.1/8",
+ gwCIDRSecond: "10.1.2.3/16",
+ },
+ {
+ // IPv6, overlapping subnets
+ gwCIDRFirst: "2001:db8:1::1/48",
+ gwCIDRSecond: "2001:db8:1:2::1/64",
+ },
+ {
+ // IPv6, non-overlapping subnets
+ gwCIDRFirst: "2001:db8:1:2::1/64",
+ gwCIDRSecond: "fd00:1234::1/64",
+ },
+ }
+ for _, tc := range testCases {
+
+ gwIP, gwSubnet, err := net.ParseCIDR(tc.gwCIDRFirst)
+ Expect(err).NotTo(HaveOccurred())
+ gwnFirst := net.IPNet{IP: gwIP, Mask: gwSubnet.Mask}
+ gwIP, gwSubnet, err = net.ParseCIDR(tc.gwCIDRSecond)
+ Expect(err).NotTo(HaveOccurred())
+ gwnSecond := net.IPNet{IP: gwIP, Mask: gwSubnet.Mask}
+
+ var family, expNumAddrs int
+ switch {
+ case gwIP.To4() != nil:
+ family = netlink.FAMILY_V4
+ expNumAddrs = 1
+ default:
+ family = netlink.FAMILY_V6
+ // Expect configured gw address plus link local
+ expNumAddrs = 2
+ }
+
+ subnetsOverlap := gwnFirst.Contains(gwnSecond.IP) ||
+ gwnSecond.Contains(gwnFirst.IP)
+
+ err = originalNS.Do(func(ns.NetNS) error {
+ defer GinkgoRecover()
+
+ // Create the bridge
+ bridge, _, err := setupBridge(conf)
+ Expect(err).NotTo(HaveOccurred())
+
+ // Function to check IP address(es) on bridge
+ checkBridgeIPs := func(cidr0, cidr1 string) {
+ addrs, err := netlink.AddrList(bridge, family)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(len(addrs)).To(Equal(expNumAddrs))
+ addr := addrs[0].IPNet.String()
+ Expect(addr).To(Equal(cidr0))
+ if cidr1 != "" {
+ addr = addrs[1].IPNet.String()
+ Expect(addr).To(Equal(cidr1))
+ }
+ }
+
+ // Check if ForceAddress has default value
+ Expect(conf.ForceAddress).To(Equal(false))
+
+ // Set first address on bridge
+ err = ensureBridgeAddr(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)
+ if family == netlink.FAMILY_V4 || subnetsOverlap {
+ // IPv4 or overlapping IPv6 subnets:
+ // Expect an error, and address should remain the same
+ Expect(err).To(HaveOccurred())
+ checkBridgeIPs(tc.gwCIDRFirst, "")
+ } else {
+ // Non-overlapping IPv6 subnets:
+ // There should be 2 addresses (in addition to link local)
+ Expect(err).NotTo(HaveOccurred())
+ expNumAddrs++
+ checkBridgeIPs(tc.gwCIDRSecond, tc.gwCIDRFirst)
+ }
+
+ // Set the second address on the bridge
+ // with ForceAddress set to true.
+ err = ensureBridgeAddr(bridge, family, &gwnSecond, true)
+ Expect(err).NotTo(HaveOccurred())
+ if family == netlink.FAMILY_V4 || subnetsOverlap {
+ // IPv4 or overlapping IPv6 subnets:
+ // IP address should be reconfigured.
+ checkBridgeIPs(tc.gwCIDRSecond, "")
+ } else {
+ // Non-overlapping IPv6 subnets:
+ // There should be 2 addresses (in addition to link local)
+ checkBridgeIPs(tc.gwCIDRSecond, tc.gwCIDRFirst)
+ }
+
+ return nil
+ })
+ Expect(err).NotTo(HaveOccurred())
+
+ // Clean up bridge addresses for next test case
+ delBridgeAddrs(originalNS)
+ }
+ })
+ It("ensure promiscuous mode on bridge", func() {
+ const IFNAME = "bridge0"
+ const EXPECTED_IP = "10.0.0.0/8"
+ const CHANGED_EXPECTED_IP = "10.1.2.3/16"
+
+ conf := &NetConf{
+ NetConf: types.NetConf{
+ CNIVersion: "0.3.1",
+ Name: "testConfig",
+ Type: "bridge",
+ },
+ BrName: IFNAME,
+ IsGW: true,
+ IPMasq: false,
+ HairpinMode: false,
+ PromiscMode: true,
+ MTU: 5000,
+ }
+
+ err := originalNS.Do(func(ns.NetNS) error {
+ defer GinkgoRecover()
+
+ _, _, err := setupBridge(conf)
+ Expect(err).NotTo(HaveOccurred())
+ // Check if ForceAddress has default value
+ Expect(conf.ForceAddress).To(Equal(false))
+
+ //Check if promiscuous mode is set correctly
+ link, err := netlink.LinkByName("bridge0")
+ Expect(err).NotTo(HaveOccurred())
+
+ Expect(link.Attrs().Promisc).To(Equal(1))
+
+ return nil
+ })
+ Expect(err).NotTo(HaveOccurred())
+ })
+})