Skip to content

Commit

Permalink
use existing host-device CNI plugin logic
Browse files Browse the repository at this point in the history
The existing host-device CNI plugin has a more complete implementation
that already handles edge cases like interface name collision and
handles the operations errors gracefully.

Signed-off-by: Antonio Ojea <[email protected]>
  • Loading branch information
aojea committed May 14, 2024
1 parent 174dff6 commit 20f7b42
Showing 1 changed file with 195 additions and 82 deletions.
277 changes: 195 additions & 82 deletions plugins/network-device-injector/network-device-injector.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,143 +44,256 @@ var (
verbose bool
)

// an annotated netdevice
// https://man7.org/linux/man-pages/man7/netdevice.7.html
type netdevice struct {
Name string `json:"name"` // name in the runtime namespace
NewName string `json:"new_name"` // name inside the pod namespace
Address string `json:"address"`
Prefix int `json:"prefix"`
MTU int `json:"mtu"`
}
// Based on existing host-device CNI plugin
// https://github.com/containernetworking/plugins/blob/main/plugins/main/host-device/host-device.go

func (n *netdevice) inject(nsPath string) error {
// Lock the OS Thread so we don't accidentally switch namespaces
runtime.LockOSThread()
defer runtime.UnlockOSThread()
// setTempName sets a temporary name for netdevice to avoid collisions with interfaces names.
func setTempName(dev netlink.Link) (netlink.Link, error) {
tempName := fmt.Sprintf("%s%d", "temp_", dev.Attrs().Index)

// Save the current network namespace
currentNs, err := ns.GetCurrentNS()
if err != nil {
return err
// rename to tempName
if err := netlink.LinkSetName(dev, tempName); err != nil {
return nil, fmt.Errorf("failed to rename device %q to %q: %v", dev.Attrs().Name, tempName, err)
}
defer currentNs.Close()

containerNs, err := ns.GetNS(nsPath)
// Get updated Link obj
tempDev, err := netlink.LinkByName(tempName)
if err != nil {
return err
return nil, fmt.Errorf("failed to find %q after rename to %q: %v", dev.Attrs().Name, tempName, err)
}
defer containerNs.Close()

link, err := netlink.LinkByName(n.Name)
return tempDev, nil
}

func moveLinkIn(hostDev netlink.Link, containerNs ns.NetNS, ifName string) (netlink.Link, error) {
origLinkFlags := hostDev.Attrs().Flags
hostDevName := hostDev.Attrs().Name
defaultNs, err := ns.GetCurrentNS()
if err != nil {
return err
return nil, fmt.Errorf("failed to get host namespace: %v", err)
}

// Devices can be renamed only when down
err = netlink.LinkSetDown(link)
if err != nil {
return err
if err = netlink.LinkSetDown(hostDev); err != nil {
return nil, fmt.Errorf("failed to set %q down: %v", hostDev.Attrs().Name, err)
}
// Save host device name into the container device's alias property
err = netlink.LinkSetAlias(link, link.Attrs().Name)

// restore original link state in case of error
defer func() {
if err != nil {
if origLinkFlags&net.FlagUp == net.FlagUp && hostDev != nil {
_ = netlink.LinkSetUp(hostDev)
}
}
}()

hostDev, err = setTempName(hostDev)
if err != nil {
return fmt.Errorf("fail to set alias for iface %s: %w", n.Name, err)
return nil, fmt.Errorf("failed to rename device %q to temporary name: %v", hostDevName, err)
}
err = netlink.LinkSetNsFd(link, int(containerNs.Fd()))
if err != nil {
return fmt.Errorf("fail to move link for iface %s to ns %d : %v", n.Name, int(containerNs.Fd()), err)

// restore original netdev name in case of error
defer func() {
if err != nil && hostDev != nil {
_ = netlink.LinkSetName(hostDev, hostDevName)
}
}()

if err = netlink.LinkSetNsFd(hostDev, int(containerNs.Fd())); err != nil {
return nil, fmt.Errorf("failed to move %q to container ns: %v", hostDev.Attrs().Name, err)
}

// This is now inside the container namespace
err = containerNs.Do(func(_ ns.NetNS) error {
link, err = netlink.LinkByName(n.Name)
var contDev netlink.Link
tempDevName := hostDev.Attrs().Name
if err = containerNs.Do(func(_ ns.NetNS) error {
var err error
contDev, err = netlink.LinkByName(tempDevName)
if err != nil {
return err
return fmt.Errorf("failed to find %q: %v", tempDevName, err)
}

err = netlink.LinkSetName(link, n.NewName)
if err != nil {
return err
// move netdev back to host namespace in case of error
defer func() {
if err != nil {
_ = netlink.LinkSetNsFd(contDev, int(defaultNs.Fd()))
// we need to get updated link object as link was moved back to host namepsace
_ = defaultNs.Do(func(_ ns.NetNS) error {
hostDev, _ = netlink.LinkByName(tempDevName)
return nil
})
}
}()

// Save host device name into the container device's alias property
if err = netlink.LinkSetAlias(contDev, hostDevName); err != nil {
return fmt.Errorf("failed to set alias to %q: %v", tempDevName, err)
}
// Rename container device to respect args.IfName
if err = netlink.LinkSetName(contDev, ifName); err != nil {
return fmt.Errorf("failed to rename device %q to %q: %v", tempDevName, ifName, err)
}

// restore tempDevName in case of error
defer func() {
if err != nil {
_ = netlink.LinkSetName(contDev, tempDevName)
}
}()

// Bring container device up
if err = netlink.LinkSetUp(link); err != nil {
return err
if err = netlink.LinkSetUp(contDev); err != nil {
return fmt.Errorf("failed to set %q up: %v", ifName, err)
}

if n.MTU > 0 {
err = netlink.LinkSetMTU(link, n.MTU)
// bring device down in case of error
defer func() {
if err != nil {
return err
_ = netlink.LinkSetDown(contDev)
}
}
}()

if n.Address == "" {
return nil
// Retrieve link again to get up-to-date name and attributes
contDev, err = netlink.LinkByName(ifName)
if err != nil {
return fmt.Errorf("failed to find %q: %v", ifName, err)
}
return nil
}); err != nil {
return nil, err
}

nlAddr, err := netlink.ParseAddr(fmt.Sprintf("%s/%d", n.Address, n.Prefix))
return contDev, nil
}

func moveLinkOut(containerNs ns.NetNS, ifName string) error {
defaultNs, err := ns.GetCurrentNS()
if err != nil {
return err
}
defer defaultNs.Close()

var tempName string
var origDev netlink.Link
err = containerNs.Do(func(_ ns.NetNS) error {
dev, err := netlink.LinkByName(ifName)
if err != nil {
return err
return fmt.Errorf("failed to find %q: %v", ifName, err)
}
origDev = dev

err = netlink.AddrAdd(link, nlAddr)
// Devices can be renamed only when down
if err = netlink.LinkSetDown(dev); err != nil {
return fmt.Errorf("failed to set %q down: %v", ifName, err)
}

defer func() {
// If moving the device to the host namespace fails, set its name back to ifName so that this
// function can be retried. Also bring the device back up, unless it was already down before.
if err != nil {
_ = netlink.LinkSetName(dev, ifName)
if dev.Attrs().Flags&net.FlagUp == net.FlagUp {
_ = netlink.LinkSetUp(dev)
}
}
}()

newLink, err := setTempName(dev)
if err != nil {
return err
return fmt.Errorf("failed to rename device %q to temporary name: %v", ifName, err)
}
dev = newLink
tempName = dev.Attrs().Name

if err = netlink.LinkSetNsFd(dev, int(defaultNs.Fd())); err != nil {
return fmt.Errorf("failed to move %q to host netns: %v", tempName, err)
}
return nil
})
return err

if err != nil {
return err
}

// Rename the device to its original name from the host namespace
tempDev, err := netlink.LinkByName(tempName)
if err != nil {
return fmt.Errorf("failed to find %q in host namespace: %v", tempName, err)
}

if err = netlink.LinkSetName(tempDev, tempDev.Attrs().Alias); err != nil {
// move device back to container ns so it may be retired
defer func() {
_ = netlink.LinkSetNsFd(tempDev, int(containerNs.Fd()))
_ = containerNs.Do(func(_ ns.NetNS) error {
lnk, err := netlink.LinkByName(tempName)
if err != nil {
return err
}
_ = netlink.LinkSetName(lnk, ifName)
if origDev.Attrs().Flags&net.FlagUp == net.FlagUp {
_ = netlink.LinkSetUp(lnk)
}
return nil
})
}()
return fmt.Errorf("failed to restore %q to original name %q: %v", tempName, tempDev.Attrs().Alias, err)
}

return nil
}

// remove the network device from the Pod namespace and recover its name
// Leaves the interface in down state to avoid issues with the root network.
func (n *netdevice) release(nsPath string) error {
// an annotated netdevice
// https://man7.org/linux/man-pages/man7/netdevice.7.html
type netdevice struct {
Name string `json:"name"` // name in the runtime namespace
NewName string `json:"new_name"` // name inside the pod namespace
Address string `json:"address"`
Prefix int `json:"prefix"`
MTU int `json:"mtu"`
}

func (n *netdevice) inject(nsPath string) error {
// Lock the OS Thread so we don't accidentally switch namespaces
runtime.LockOSThread()
defer runtime.UnlockOSThread()

// Save the current network namespace
currentNs, err := ns.GetCurrentNS()
containerNs, err := ns.GetNS(nsPath)
if err != nil {
return err
}
defer currentNs.Close()
defer containerNs.Close()

containerNs, err := ns.GetNS(nsPath)
hostDev, err := netlink.LinkByName(n.Name)
if err != nil {
return err
}
defer containerNs.Close()

// This is now inside the container namespace
err = containerNs.Do(func(_ ns.NetNS) error {
link, err := netlink.LinkByName(n.NewName)
if err != nil {
return err
}
_, err = moveLinkIn(hostDev, containerNs, n.NewName)
if err != nil {
return fmt.Errorf("failed to move link %v", err)
}
return nil
}

// Devices can be renamed only when down
err = netlink.LinkSetDown(link)
if err != nil {
return err
}
// remove the network device from the Pod namespace and recover its name
// Leaves the interface in down state to avoid issues with the root network.
func (n *netdevice) release(nsPath string) error {
// Lock the OS Thread so we don't accidentally switch namespaces
runtime.LockOSThread()
defer runtime.UnlockOSThread()

err = netlink.LinkSetName(link, n.Name)
if err != nil {
return err
}
containerNs, err := ns.GetNS(nsPath)
if err != nil {
return err
}
defer containerNs.Close()

err = netlink.LinkSetNsFd(link, int(currentNs.Fd()))
if err != nil {
return fmt.Errorf("fail to move link for iface %s to ns %d : %v", n.Name, int(currentNs.Fd()), err)
}
err = moveLinkOut(containerNs, n.NewName)
if err != nil {
return err
}

return nil
})
return err
return nil
}

// our injector plugin
Expand Down

0 comments on commit 20f7b42

Please sign in to comment.