Skip to content

Commit

Permalink
spec/plugins: pass interface names and MACs back to runtime
Browse files Browse the repository at this point in the history
Allows the runtime to leave interface naming decisions up to the
plugin and for the plugin to pass interface details back to the
runtime which can be useful for further runtime operations.
  • Loading branch information
dcbw committed Mar 30, 2016
1 parent b4f221d commit 960feef
Show file tree
Hide file tree
Showing 10 changed files with 207 additions and 81 deletions.
18 changes: 16 additions & 2 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ The operations that the CNI plugin needs to support are:
- **Extra arguments**. This allows granular configuration of CNI plugins on a per-container basis.
- **Name of the interface inside the container**. This is the name that should be assigned to the interface created inside the container (network namespace); consequently it must comply with the standard Linux restrictions on interface names.
- Result:
- **Container interface details**. Depending on the plugin, this can include the container interface name and/or the host interface name, and the hardware addresses of both interfaces.
- **IPs assigned to the interface**. This is either an IPv4 address, an IPv6 address, or both.
- **DNS information**. Dictionary that includes DNS information for nameservers, domain, search domains and options.

Expand All @@ -67,7 +68,7 @@ It will then look for this executable in a list of predefined directories. Once
- `CNI_COMMAND`: indicates the desired operation; either `ADD` or `DEL`
- `CNI_CONTAINERID`: Container ID
- `CNI_NETNS`: Path to network namespace file
- `CNI_IFNAME`: Interface name to set up
- `CNI_IFNAME`: Interface name to set up; plugin must honor this interface name or return an error
- `CNI_ARGS`: Extra arguments passed in by the user at invocation time. Alphanumeric key-value pairs separated by semicolons; for example, "FOO=BAR;ABC=123"
- `CNI_PATH`: Colon-separated list of paths to search for CNI plugin executables

Expand All @@ -76,11 +77,17 @@ Network configuration in JSON format is streamed through stdin.

### Result

Success is indicated by a return code of zero and the following JSON printed to stdout in the case of the ADD command. This should be the same output as was returned by the IPAM plugin (see [IP Allocation](#ip-allocation) for details).
Success is indicated by a return code of zero and the following JSON printed to stdout in the case of the ADD command. The `ip4`, `ip6`, and `dns` items should be the same output as was returned by the IPAM plugin (see [IP Allocation](#ip-allocation) for details).

```
{
"cniVersion": "0.1.0",
"interface": {
"hostIfname": <name>, (optional)
"hostMac": <MAC address>, (optional)
"containerIfname": <name>, (optional)
"containerMac": <MAC address> (optional)
}
"ip4": {
"ip": <ipv4-and-subnet-in-CIDR>,
"gateway": <ipv4-of-the-gateway>, (optional)
Expand All @@ -105,6 +112,13 @@ Success is indicated by a return code of zero and the following JSON printed to
The result is returned in the same format as specified in the [configuration](#network-configuration).
The specification does not declare how this information must be processed by CNI consumers.
Examples include generating an `/etc/resolv.conf` file to be injected into the container filesystem or running a DNS forwarder on the host.
The `interface` block is optional for backwards compatibility but plugins are strongly recommended to return it.
If the plugin does not use L2 addressing and thus MAC addresses are not meaningful, then `hostMac` and `containerMac` can be omitted.
`containerIfname` must always be returned if the `interface` block is returned.
`hostIfname` should be returned if the container is connected with an interface that provides two endpoints, like the Linux veth type.
Other types with only one endpoint, like macvlan or ipvlan, do not require returning `hostIfname`.
If the `CNI_IFNAME` variable is passed to the plugin, it must honor that name or return an error if it cannot.
If the `CNI_IFNAME` variable is omitted, the plugin should return the `interface` block so that the caller knows what interface name was created by the plugin inside the container.

Errors are indicated by a non-zero return code and the following JSON being printed to stdout:
```
Expand Down
78 changes: 67 additions & 11 deletions pkg/ip/link.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"fmt"
"net"
"os"
"syscall"

. "github.com/appc/cni/pkg/ops"
"github.com/vishvananda/netlink"
Expand All @@ -40,9 +41,15 @@ func makeVethPair(name, peer string, mtu int) (netlink.Link, error) {
return veth, nil
}

func makeVeth(name string, mtu int) (peerName string, veth netlink.Link, err error) {
func makeVeth(mtu int) (peerName string, veth netlink.Link, err error) {
for i := 0; i < 10; i++ {
peerName, err = RandomVethName()
var name string

peerName, err = RandomIfaceName("veth")
if err != nil {
return
}
name, err = RandomIfaceName("veth")
if err != nil {
return
}
Expand All @@ -66,25 +73,74 @@ func makeVeth(name string, mtu int) (peerName string, veth netlink.Link, err err
return
}

// RandomVethName returns string "veth" with random prefix (hashed from entropy)
func RandomVethName() (string, error) {
entropy := make([]byte, 4)
const ENTROPY_LEN = 4

// RandomIfaceName returns string @prefix with random suffix (hashed from entropy)
func RandomIfaceName(prefix string) (string, error) {
if len(prefix)+ENTROPY_LEN > syscall.IFNAMSIZ {
return "", fmt.Errorf("prefix %s too long", len(prefix))
}
entropy := make([]byte, ENTROPY_LEN)
_, err := rand.Reader.Read(entropy)
if err != nil {
return "", fmt.Errorf("failed to generate random veth name: %v", err)
}

// NetworkManager (recent versions) will ignore veth devices that start with "veth"
return fmt.Sprintf("veth%x", entropy), nil
// NetworkManager (1.0+) will ignore externally created virtual interfaces
return fmt.Sprintf("%s%x", prefix, entropy), nil
}

func RenameLink(curName, newName, newPrefix string) (string, error) {
if len(newName) == 0 && len(newPrefix) == 0 {
return "", fmt.Errorf("one of new name or new name prefix required")
}

link, err := Net().LinkByName(curName)
if err != nil {
return "", err
}

if len(newName) > 0 {
if err := Net().LinkSetName(link, newName); err != nil {
return "", err
}
return newName, nil
}

for idx := 0; idx < 100; idx++ {
newName = fmt.Sprintf(newPrefix, idx)
err = Net().LinkSetName(link, newName)
if err == nil {
return newName, nil
} else if !os.IsExist(err) {
return "", err
}
}
return "", err
}

// SetupVeth sets up a virtual ethernet link.
// Should be in container netns, and will switch back to hostNS to set the host
// veth end up.
func SetupVeth(contVethName string, mtu int, hostNS *os.File) (hostVeth, contVeth netlink.Link, err error) {
var hostVethName string
hostVethName, contVeth, err = makeVeth(contVethName, mtu)
// veth end up. If @contVethName is given the container interface will have
// that name or an error will be returned. If instead @contVethPrefix is
// given the container interface name will be constructed from that prefix
// and will be a unique name in the namespace.
func SetupVeth(contVethName, contVethPrefix string, mtu int, hostNS *os.File) (hostVeth, contVeth netlink.Link, err error) {
var hostVethName, newName string
hostVethName, contVeth, err = makeVeth(mtu)
if err != nil {
return
}
newName, err = RenameLink(contVeth.Attrs().Name, contVethName, contVethPrefix)
if err != nil {
return
}
contVethName = newName

// Re-fetch container veth to get all properties/attributes
contVeth, err = Net().LinkByName(contVethName)
if err != nil {
err = fmt.Errorf("failed to lookup %q: %v", contVethName, err)
return
}

Expand Down
18 changes: 15 additions & 3 deletions pkg/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,10 @@ type NetConf struct {

// Result is what gets returned from the plugin (via stdout) to the caller
type Result struct {
IP4 *IPConfig `json:"ip4,omitempty"`
IP6 *IPConfig `json:"ip6,omitempty"`
DNS DNS `json:"dns,omitempty"`
Interface *InterfaceConfig `json:"interface,omitempty"`
IP4 *IPConfig `json:"ip4,omitempty"`
IP6 *IPConfig `json:"ip6,omitempty"`
DNS DNS `json:"dns,omitempty"`
}

func (r *Result) Print() error {
Expand All @@ -87,6 +88,9 @@ func (r *Result) String() string {
if r.IP6 != nil {
str += fmt.Sprintf("IP6:%+v, ", *r.IP6)
}
if r.Interface != nil {
str += fmt.Sprintf("Interface:%+v, ", *r.Interface)
}
return fmt.Sprintf("%sDNS:%+v", str, r.DNS)
}

Expand All @@ -105,6 +109,14 @@ type DNS struct {
Options []string `json:"options,omitempty"`
}

// InterfaceConfig contains values about the created interfaces
type InterfaceConfig struct {
HostIfname string `json:"hostIfname,omitempty"`
HostMac string `json:"hostMac,omitempty"`
ContainerIfname string `json:"containerIfname,omitempty"`
ContainerMac string `json:"containerMac,omitempty"`
}

type Route struct {
Dst net.IPNet
GW net.IP
Expand Down
25 changes: 15 additions & 10 deletions plugins/main/bridge/bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,35 +122,38 @@ func ensureBridge(brName string, mtu int) (*netlink.Bridge, error) {
return br, nil
}

func setupVeth(netns string, br *netlink.Bridge, ifName string, mtu int) error {
var hostVethName string
func setupVeth(netns string, br *netlink.Bridge, ifName string, mtu int) (*types.InterfaceConfig, error) {
ifaceConfig := &types.InterfaceConfig{}

err := Net().WithNetNSPath(netns, false, func(hostNS *os.File) error {
// create the veth pair in the container and move host end into host netns
hostVeth, _, err := ip.SetupVeth(ifName, mtu, hostNS)
hostVeth, containerVeth, err := ip.SetupVeth(ifName, "eth%d", mtu, hostNS)
if err != nil {
return err
}

hostVethName = hostVeth.Attrs().Name
ifaceConfig.ContainerIfname = containerVeth.Attrs().Name
ifaceConfig.ContainerMac = containerVeth.Attrs().HardwareAddr.String()
ifaceConfig.HostIfname = hostVeth.Attrs().Name
return nil
})
if err != nil {
return err
return nil, err
}

// need to lookup hostVeth again as its index has changed during ns move
hostVeth, err := Net().LinkByName(hostVethName)
hostVeth, err := Net().LinkByName(ifaceConfig.HostIfname)
if err != nil {
return fmt.Errorf("failed to lookup %q: %v", hostVethName, err)
return nil, fmt.Errorf("failed to lookup %q: %v", ifaceConfig.HostIfname, err)
}
ifaceConfig.HostMac = hostVeth.Attrs().HardwareAddr.String()

// connect host veth end to the bridge
if err = Net().LinkSetMaster(hostVeth, br); err != nil {
return fmt.Errorf("failed to connect %q to bridge %v: %v", hostVethName, br.Attrs().Name, err)
return nil, fmt.Errorf("failed to connect %q to bridge %v: %v", hostVeth.Attrs().Name, br.Attrs().Name, err)
}

return nil
return ifaceConfig, nil
}

func calcGatewayIP(ipn *net.IPNet) net.IP {
Expand Down Expand Up @@ -179,7 +182,8 @@ func cmdAdd(args *skel.CmdArgs) error {
return err
}

if err = setupVeth(args.Netns, br, args.IfName, n.MTU); err != nil {
ifaceConfig, err := setupVeth(args.Netns, br, args.IfName, n.MTU)
if err != nil {
return err
}

Expand Down Expand Up @@ -227,6 +231,7 @@ func cmdAdd(args *skel.CmdArgs) error {
}

result.DNS = n.DNS
result.Interface = ifaceConfig
return result.Print()
}

Expand Down
19 changes: 11 additions & 8 deletions plugins/main/bridge/bridge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ var _ = Describe("bridge Operations", func() {
result := types.Result{}
err = json.Unmarshal(out, &result)
Expect(err).NotTo(HaveOccurred())
Expect(result.Interface.ContainerIfname).To(Equal(IFNAME))

// Make sure bridge link exists
link, err := tops.LinkByName(BRNAME)
Expand All @@ -184,13 +185,11 @@ var _ = Describe("bridge Operations", func() {
links, err := tops.LinkList()
Expect(err).NotTo(HaveOccurred())
Expect(len(links)).To(Equal(2))
for _, l := range links {
if l.Attrs().Name != BRNAME {
veth, isVeth := l.(*netlink.Veth)
Expect(isVeth).To(Equal(true))
Expect(veth.PeerName).To(Equal(IFNAME))
}
}
link, err = tops.LinkByName(result.Interface.HostIfname)
Expect(err).NotTo(HaveOccurred())
veth, isVeth := link.(*netlink.Veth)
Expect(isVeth).To(Equal(true))
Expect(veth.PeerName).To(Equal(IFNAME))

// Find the veth peer in the container namespace
var innerErr error
Expand All @@ -207,13 +206,17 @@ var _ = Describe("bridge Operations", func() {
err = cmdDel(args)
Expect(err).NotTo(HaveOccurred())

// Make sure macvlan link has been deleted
// Make sure veth link has been deleted
err = tops.WithNetNS(targetNetNS, false, func(*os.File) error {
innerLink, innerErr = tops.LinkByName(IFNAME)
return nil
})
Expect(err).NotTo(HaveOccurred())
Expect(innerErr).To(HaveOccurred())
Expect(innerLink).To(BeNil())

link, err = tops.LinkByName(result.Interface.HostIfname)
Expect(err).To(HaveOccurred())
Expect(link).To(BeNil())
})
})
Loading

0 comments on commit 960feef

Please sign in to comment.