diff --git a/pkg/ip/ip_suite_test.go b/pkg/ip/ip_suite_test.go new file mode 100644 index 00000000..3fdd57e4 --- /dev/null +++ b/pkg/ip/ip_suite_test.go @@ -0,0 +1,27 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ip_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestIp(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Ip Suite") +} diff --git a/pkg/ip/link.go b/pkg/ip/link.go index 1b785672..c9662d98 100644 --- a/pkg/ip/link.go +++ b/pkg/ip/link.go @@ -21,6 +21,7 @@ import ( "os" "github.com/containernetworking/cni/pkg/ns" + "github.com/containernetworking/cni/pkg/utils/hwaddr" "github.com/vishvananda/netlink" ) @@ -151,3 +152,30 @@ func DelLinkByNameAddr(ifName string, family int) (*net.IPNet, error) { return addrs[0].IPNet, nil } + +func SetHWAddrByIP(ifName string, ip4 net.IP, ip6 net.IP) error { + iface, err := netlink.LinkByName(ifName) + if err != nil { + return fmt.Errorf("failed to lookup %q: %v", ifName, err) + } + + switch { + case ip4 == nil && ip6 == nil: + return fmt.Errorf("neither ip4 or ip6 specified") + + case ip4 != nil: + { + hwAddr, err := hwaddr.GenerateHardwareAddr4(ip4, hwaddr.PrivateMACPrefix) + if err != nil { + return fmt.Errorf("failed to generate hardware addr: %v", err) + } + if err = netlink.LinkSetHardwareAddr(iface, hwAddr); err != nil { + return fmt.Errorf("failed to add hardware addr to %q: %v", ifName, err) + } + } + case ip6 != nil: + // TODO: IPv6 + } + + return nil +} diff --git a/pkg/ip/link_test.go b/pkg/ip/link_test.go new file mode 100644 index 00000000..5c139d98 --- /dev/null +++ b/pkg/ip/link_test.go @@ -0,0 +1,179 @@ +// Copyright 2016 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ip_test + +import ( + "fmt" + "net" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/containernetworking/cni/pkg/ip" + "github.com/containernetworking/cni/pkg/ns" + + "github.com/vishvananda/netlink" + "github.com/vishvananda/netlink/nl" +) + +func getHwAddr(linkname string) string { + veth, err := netlink.LinkByName(linkname) + Expect(err).NotTo(HaveOccurred()) + return fmt.Sprintf("%s", veth.Attrs().HardwareAddr) +} + +var _ = Describe("Link", func() { + const ( + ifaceFormatString string = "i%d" + mtu int = 1400 + ip4onehwaddr = "0a:58:01:01:01:01" + ) + var ( + hostNetNS ns.NetNS + containerNetNS ns.NetNS + ifaceCounter int = 0 + hostVethName string + containerVethName string + + ip4one = net.ParseIP("1.1.1.1") + ip4two = net.ParseIP("1.1.1.2") + ) + + BeforeEach(func() { + var err error + + hostNetNS, err = ns.NewNS() + Expect(err).NotTo(HaveOccurred()) + + containerNetNS, err = ns.NewNS() + Expect(err).NotTo(HaveOccurred()) + + _ = containerNetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + hostVeth, containerVeth, err := ip.SetupVeth(fmt.Sprintf(ifaceFormatString, ifaceCounter), mtu, hostNetNS) + if err != nil { + return err + } + Expect(err).NotTo(HaveOccurred()) + + hostVethName = hostVeth.Attrs().Name + containerVethName = containerVeth.Attrs().Name + + return nil + }) + }) + + AfterEach(func() { + Expect(containerNetNS.Close()).To(Succeed()) + Expect(hostNetNS.Close()).To(Succeed()) + ifaceCounter++ + }) + + It("SetupVeth must put the veth endpoints into the separate namespaces", func() { + _ = containerNetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + _, err := netlink.LinkByName(containerVethName) + Expect(err).NotTo(HaveOccurred()) + + return nil + }) + + _ = hostNetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + _, err := netlink.LinkByName(hostVethName) + Expect(err).NotTo(HaveOccurred()) + + return nil + }) + }) + + It("DelLinkByName must delete the veth endpoints", func() { + _ = containerNetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + // this will delete the host endpoint too + err := ip.DelLinkByName(containerVethName) + Expect(err).NotTo(HaveOccurred()) + + _, err = netlink.LinkByName(containerVethName) + Expect(err).To(HaveOccurred()) + + return nil + }) + + _ = hostNetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + _, err := netlink.LinkByName(hostVethName) + Expect(err).To(HaveOccurred()) + + return nil + }) + }) + + It("DelLinkByNameAddr must throw an error for configured interfaces", func() { + _ = containerNetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + // this will delete the host endpoint too + addr, err := ip.DelLinkByNameAddr(containerVethName, nl.FAMILY_V4) + Expect(err).To(HaveOccurred()) + + var ipNetNil *net.IPNet + Expect(addr).To(Equal(ipNetNil)) + return nil + }) + }) + + It("SetHWAddrByIP must change the interface hwaddr and be predictable", func() { + + _ = containerNetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + var err error + hwaddrBefore := getHwAddr(containerVethName) + + err = ip.SetHWAddrByIP(containerVethName, ip4one, nil) + Expect(err).NotTo(HaveOccurred()) + hwaddrAfter1 := getHwAddr(containerVethName) + + Expect(hwaddrBefore).NotTo(Equal(hwaddrAfter1)) + Expect(hwaddrAfter1).To(Equal(ip4onehwaddr)) + + return nil + }) + }) + + It("SetHWAddrByIP must be injective", func() { + + _ = containerNetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + err := ip.SetHWAddrByIP(containerVethName, ip4one, nil) + Expect(err).NotTo(HaveOccurred()) + hwaddrAfter1 := getHwAddr(containerVethName) + + err = ip.SetHWAddrByIP(containerVethName, ip4two, nil) + Expect(err).NotTo(HaveOccurred()) + hwaddrAfter2 := getHwAddr(containerVethName) + + Expect(hwaddrAfter1).NotTo(Equal(hwaddrAfter2)) + return nil + }) + }) +}) diff --git a/pkg/ipam/ipam_suite_test.go b/pkg/ipam/ipam_suite_test.go new file mode 100644 index 00000000..e80c8675 --- /dev/null +++ b/pkg/ipam/ipam_suite_test.go @@ -0,0 +1,27 @@ +// 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 ipam_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestIpam(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Ipam Suite") +} diff --git a/pkg/utils/hwaddr/hwaddr.go b/pkg/utils/hwaddr/hwaddr.go new file mode 100644 index 00000000..aaf3b8a0 --- /dev/null +++ b/pkg/utils/hwaddr/hwaddr.go @@ -0,0 +1,63 @@ +// 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 hwaddr + +import ( + "fmt" + "net" +) + +const ( + ipRelevantByteLen = 4 + PrivateMACPrefixString = "0a:58" +) + +var ( + // private mac prefix safe to use + PrivateMACPrefix = []byte{0x0a, 0x58} +) + +type SupportIp4OnlyErr struct{ msg string } + +func (e SupportIp4OnlyErr) Error() string { return e.msg } + +type MacParseErr struct{ msg string } + +func (e MacParseErr) Error() string { return e.msg } + +type InvalidPrefixLengthErr struct{ msg string } + +func (e InvalidPrefixLengthErr) Error() string { return e.msg } + +// GenerateHardwareAddr4 generates 48 bit virtual mac addresses based on the IP4 input. +func GenerateHardwareAddr4(ip net.IP, prefix []byte) (net.HardwareAddr, error) { + switch { + + case ip.To4() == nil: + return nil, SupportIp4OnlyErr{msg: "GenerateHardwareAddr4 only supports valid IPv4 address as input"} + + case len(prefix) != len(PrivateMACPrefix): + return nil, InvalidPrefixLengthErr{msg: fmt.Sprintf( + "Prefix has length %d instead of %d", len(prefix), len(PrivateMACPrefix)), + } + } + + ipByteLen := len(ip) + return (net.HardwareAddr)( + append( + prefix, + ip[ipByteLen-ipRelevantByteLen:ipByteLen]...), + ), nil +} diff --git a/pkg/utils/hwaddr/hwaddr_suite_test.go b/pkg/utils/hwaddr/hwaddr_suite_test.go new file mode 100644 index 00000000..e3bbfe97 --- /dev/null +++ b/pkg/utils/hwaddr/hwaddr_suite_test.go @@ -0,0 +1,27 @@ +// 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 hwaddr_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestHwaddr(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Hwaddr Suite") +} diff --git a/pkg/utils/hwaddr/hwaddr_test.go b/pkg/utils/hwaddr/hwaddr_test.go new file mode 100644 index 00000000..b77ccd89 --- /dev/null +++ b/pkg/utils/hwaddr/hwaddr_test.go @@ -0,0 +1,74 @@ +// 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 hwaddr_test + +import ( + "net" + + "github.com/containernetworking/cni/pkg/utils/hwaddr" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Hwaddr", func() { + Context("Generate Hardware Address", func() { + It("generate hardware address based on ipv4 address", func() { + testCases := []struct { + ip net.IP + expectedMAC net.HardwareAddr + }{ + { + ip: net.ParseIP("10.0.0.2"), + expectedMAC: (net.HardwareAddr)(append(hwaddr.PrivateMACPrefix, 0x0a, 0x00, 0x00, 0x02)), + }, + { + ip: net.ParseIP("10.250.0.244"), + expectedMAC: (net.HardwareAddr)(append(hwaddr.PrivateMACPrefix, 0x0a, 0xfa, 0x00, 0xf4)), + }, + { + ip: net.ParseIP("172.17.0.2"), + expectedMAC: (net.HardwareAddr)(append(hwaddr.PrivateMACPrefix, 0xac, 0x11, 0x00, 0x02)), + }, + { + ip: net.IPv4(byte(172), byte(17), byte(0), byte(2)), + expectedMAC: (net.HardwareAddr)(append(hwaddr.PrivateMACPrefix, 0xac, 0x11, 0x00, 0x02)), + }, + } + + for _, tc := range testCases { + mac, err := hwaddr.GenerateHardwareAddr4(tc.ip, hwaddr.PrivateMACPrefix) + Expect(err).NotTo(HaveOccurred()) + Expect(mac).To(Equal(tc.expectedMAC)) + } + }) + + It("return error if input is not ipv4 address", func() { + testCases := []net.IP{ + net.ParseIP(""), + net.ParseIP("2001:db8:0:1:1:1:1:1"), + } + for _, tc := range testCases { + _, err := hwaddr.GenerateHardwareAddr4(tc, hwaddr.PrivateMACPrefix) + Expect(err).To(BeAssignableToTypeOf(hwaddr.SupportIp4OnlyErr{})) + } + }) + + It("return error if prefix is invalid", func() { + _, err := hwaddr.GenerateHardwareAddr4(net.ParseIP("10.0.0.2"), []byte{0x58}) + Expect(err).To(BeAssignableToTypeOf(hwaddr.InvalidPrefixLengthErr{})) + }) + }) +}) diff --git a/plugins/main/bridge/bridge.go b/plugins/main/bridge/bridge.go index d4fc89c3..5f9659d7 100644 --- a/plugins/main/bridge/bridge.go +++ b/plugins/main/bridge/bridge.go @@ -247,7 +247,15 @@ func cmdAdd(args *skel.CmdArgs) error { // TODO: IPV6 } - return ipam.ConfigureIface(args.IfName, result) + if err := ipam.ConfigureIface(args.IfName, result); err != nil { + return err + } + + if err := ip.SetHWAddrByIP(args.IfName, result.IP4.IP.IP, nil /* TODO IPv6 */); err != nil { + return err + } + + return nil }); err != nil { return err } @@ -262,6 +270,10 @@ func cmdAdd(args *skel.CmdArgs) error { return err } + if err := ip.SetHWAddrByIP(n.BrName, gwn.IP, nil /* TODO IPv6 */); err != nil { + return err + } + if err := ip.EnableIP4Forward(); err != nil { return fmt.Errorf("failed to enable forwarding: %v", err) } diff --git a/plugins/main/bridge/bridge_test.go b/plugins/main/bridge/bridge_test.go index 14fef56b..7c0e8132 100644 --- a/plugins/main/bridge/bridge_test.go +++ b/plugins/main/bridge/bridge_test.go @@ -24,6 +24,8 @@ import ( "github.com/containernetworking/cni/pkg/testutils" "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/utils/hwaddr" + "github.com/vishvananda/netlink" . "github.com/onsi/ginkgo" @@ -179,9 +181,19 @@ var _ = Describe("bridge Operations", func() { Expect(err).NotTo(HaveOccurred()) Expect(len(links)).To(Equal(3)) // Bridge, veth, and loopback for _, l := range links { - if l.Attrs().Name != BRNAME && l.Attrs().Name != "lo" { - _, isVeth := l.(*netlink.Veth) - Expect(isVeth).To(Equal(true)) + switch { + case l.Attrs().Name == BRNAME: + { + _, isBridge := l.(*netlink.Bridge) + Expect(isBridge).To(Equal(true)) + hwAddr := fmt.Sprintf("%s", l.Attrs().HardwareAddr) + Expect(hwAddr).To(HavePrefix(hwaddr.PrivateMACPrefixString)) + } + case l.Attrs().Name != BRNAME && l.Attrs().Name != "lo": + { + _, isVeth := l.(*netlink.Veth) + Expect(isVeth).To(Equal(true)) + } } } Expect(err).NotTo(HaveOccurred()) @@ -197,6 +209,9 @@ var _ = Describe("bridge Operations", func() { Expect(err).NotTo(HaveOccurred()) Expect(link.Attrs().Name).To(Equal(IFNAME)) + hwAddr := fmt.Sprintf("%s", link.Attrs().HardwareAddr) + Expect(hwAddr).To(HavePrefix(hwaddr.PrivateMACPrefixString)) + // Ensure the default route routes, err := netlink.RouteList(link, 0) Expect(err).NotTo(HaveOccurred()) diff --git a/plugins/main/macvlan/macvlan.go b/plugins/main/macvlan/macvlan.go index 7739d7b8..63c9f7f0 100644 --- a/plugins/main/macvlan/macvlan.go +++ b/plugins/main/macvlan/macvlan.go @@ -149,6 +149,10 @@ func cmdAdd(args *skel.CmdArgs) error { } err = netns.Do(func(_ ns.NetNS) error { + if err := ip.SetHWAddrByIP(args.IfName, result.IP4.IP.IP, nil /* TODO IPv6 */); err != nil { + return err + } + return ipam.ConfigureIface(args.IfName, result) }) if err != nil { diff --git a/plugins/main/macvlan/macvlan_test.go b/plugins/main/macvlan/macvlan_test.go index 90cac2c1..a0a14863 100644 --- a/plugins/main/macvlan/macvlan_test.go +++ b/plugins/main/macvlan/macvlan_test.go @@ -21,6 +21,7 @@ import ( "github.com/containernetworking/cni/pkg/skel" "github.com/containernetworking/cni/pkg/testutils" "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/utils/hwaddr" "github.com/vishvananda/netlink" @@ -139,6 +140,10 @@ var _ = Describe("macvlan Operations", func() { link, err := netlink.LinkByName(IFNAME) Expect(err).NotTo(HaveOccurred()) Expect(link.Attrs().Name).To(Equal(IFNAME)) + + hwAddr := fmt.Sprintf("%s", link.Attrs().HardwareAddr) + Expect(hwAddr).To(HavePrefix(hwaddr.PrivateMACPrefixString)) + return nil }) Expect(err).NotTo(HaveOccurred()) diff --git a/plugins/main/ptp/ptp.go b/plugins/main/ptp/ptp.go index aa695e39..014b472c 100644 --- a/plugins/main/ptp/ptp.go +++ b/plugins/main/ptp/ptp.go @@ -64,6 +64,15 @@ func setupContainerVeth(netns, ifName string, mtu int, pr *types.Result) (string return err } + hostNS.Do(func(_ ns.NetNS) error { + hostVethName = hostVeth.Attrs().Name + if err := ip.SetHWAddrByIP(hostVethName, pr.IP4.IP.IP, nil /* TODO IPv6 */); err != nil { + return fmt.Errorf("failed to set hardware addr by IP: %v", err) + } + + return nil + }) + if err = ipam.ConfigureIface(ifName, pr); err != nil { return err } @@ -73,6 +82,10 @@ func setupContainerVeth(netns, ifName string, mtu int, pr *types.Result) (string return fmt.Errorf("failed to look up %q: %v", ifName, err) } + if err := ip.SetHWAddrByIP(contVeth.Attrs().Name, pr.IP4.IP.IP, nil /* TODO IPv6 */); err != nil { + return fmt.Errorf("failed to set hardware addr by IP: %v", err) + } + // Delete the route that was automatically added route := netlink.Route{ LinkIndex: contVeth.Attrs().Index, @@ -113,8 +126,6 @@ func setupContainerVeth(netns, ifName string, mtu int, pr *types.Result) (string } } - hostVethName = hostVeth.Attrs().Name - return nil }) return hostVethName, err diff --git a/test b/test index 91220800..2f1a4b21 100755 --- a/test +++ b/test @@ -11,8 +11,8 @@ set -e source ./build -TESTABLE="libcni plugins/ipam/dhcp plugins/ipam/host-local plugins/main/loopback pkg/invoke pkg/ns pkg/skel pkg/types pkg/utils plugins/main/ipvlan plugins/main/macvlan plugins/main/bridge plugins/main/ptp plugins/test/noop" -FORMATTABLE="$TESTABLE libcni pkg/ip pkg/ipam pkg/testutils plugins/meta/flannel plugins/meta/tuning" +TESTABLE="libcni plugins/ipam/dhcp plugins/ipam/host-local plugins/main/loopback pkg/invoke pkg/ns pkg/skel pkg/types pkg/utils plugins/main/ipvlan plugins/main/macvlan plugins/main/bridge plugins/main/ptp plugins/test/noop pkg/utils/hwaddr pkg/ip" +FORMATTABLE="$TESTABLE pkg/testutils plugins/meta/flannel plugins/meta/tuning" # user has not provided PKG override if [ -z "$PKG" ]; then