[lxc-devel] [lxd/master] add IPVLAN support

s3rj1k on Github lxc-bot at linuxcontainers.org
Wed Oct 17 10:42:29 UTC 2018


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 403 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20181017/27598bf6/attachment.bin>
-------------- next part --------------
From 75e501bb1f0836412f909c379c5f8e853acf1617 Mon Sep 17 00:00:00 2001
From: s3rj1k <evasive.gyron at gmail.com>
Date: Wed, 17 Oct 2018 12:22:56 +0300
Subject: [PATCH 1/7] refactor networkValidAddress functions, for input
 validation purposes

---
 lxd/networks_config.go |  6 ++---
 lxd/networks_utils.go  | 53 +++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 55 insertions(+), 4 deletions(-)

diff --git a/lxd/networks_config.go b/lxd/networks_config.go
index bf0f0c5019..e2a31e4e2e 100644
--- a/lxd/networks_config.go
+++ b/lxd/networks_config.go
@@ -47,10 +47,10 @@ var networkConfigKeys = map[string]func(value string) error{
 	"tunnel.TARGET.protocol": func(value string) error {
 		return shared.IsOneOf(value, []string{"gre", "vxlan"})
 	},
-	"tunnel.TARGET.local":     networkValidAddressV4,
-	"tunnel.TARGET.remote":    networkValidAddressV4,
+	"tunnel.TARGET.local":     networkValidAddress,
+	"tunnel.TARGET.remote":    networkValidAddress,
 	"tunnel.TARGET.port":      networkValidPort,
-	"tunnel.TARGET.group":     networkValidAddressV4,
+	"tunnel.TARGET.group":     networkValidAddress,
 	"tunnel.TARGET.id":        shared.IsInt64,
 	"tunnel.TARGET.interface": networkValidName,
 	"tunnel.TARGET.ttl":       shared.IsUint8,
diff --git a/lxd/networks_utils.go b/lxd/networks_utils.go
index a0802b7d86..d3e8fbff4b 100644
--- a/lxd/networks_utils.go
+++ b/lxd/networks_utils.go
@@ -552,19 +552,70 @@ func networkValidAddressCIDRV4(value string) error {
 	return nil
 }
 
-func networkValidAddressV4(value string) error {
+func networkValidAddress(value string) error {
 	if value == "" {
 		return nil
 	}
 
 	ip := net.ParseIP(value)
 	if ip == nil {
+		return fmt.Errorf("Not an IP address: %s", value)
+	}
+
+	return nil
+}
+
+// validate list of v4 IPs (space separated)
+func networkValidAddressV4List(value string) error {
+	for _, v := range strings.Fields(value) {
+		err := networkValidAddressV4(v)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// validate list of v6 IPs (space separated)
+func networkValidAddressV6List(value string) error {
+	for _, v := range strings.Fields(value) {
+		err := networkValidAddressV6(v)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func networkValidAddressV4(value string) error {
+	if value == "" {
+		return nil
+	}
+
+	ip := net.ParseIP(value)
+	if ip.To4() == nil {
 		return fmt.Errorf("Not an IPv4 address: %s", value)
 	}
 
 	return nil
 }
 
+func networkValidAddressV6(value string) error {
+	if value == "" {
+		return nil
+	}
+
+	ip := net.ParseIP(value)
+	if ip == nil {
+		return fmt.Errorf("Not an IPv6 address: %s", value)
+	}
+	if ip.To4() != nil {
+		return fmt.Errorf("Not an IPv6 address: %s", value)
+	}
+
+	return nil
+}
+
 func networkValidNetworkV4(value string) error {
 	if value == "" {
 		return nil

From 8a3b6ba5796066afca06e3f73a19f93b8b2015c6 Mon Sep 17 00:00:00 2001
From: s3rj1k <evasive.gyron at gmail.com>
Date: Wed, 17 Oct 2018 12:40:11 +0300
Subject: [PATCH 2/7] add IPVLAN support

---
 lxd/container.go      |  30 ++++++-
 lxd/container_lxc.go  | 200 ++++++++++++++++++++++++++++++++++++++++--
 lxd/networks_utils.go | 121 ++++++++++++++++++++++++-
 3 files changed, 341 insertions(+), 10 deletions(-)

diff --git a/lxd/container.go b/lxd/container.go
index ba8ac1edbc..12d6479ef9 100644
--- a/lxd/container.go
+++ b/lxd/container.go
@@ -356,13 +356,39 @@ func containerValidDevices(db *db.Cluster, devices types.Devices, profile bool,
 				return fmt.Errorf("Missing nic type")
 			}
 
-			if !shared.StringInSlice(m["nictype"], []string{"bridged", "macvlan", "p2p", "physical", "sriov"}) {
+			if !shared.StringInSlice(m["nictype"], []string{"bridged", "macvlan", "ipvlan", "p2p", "physical", "sriov"}) {
 				return fmt.Errorf("Bad nic type: %s", m["nictype"])
 			}
 
-			if shared.StringInSlice(m["nictype"], []string{"bridged", "macvlan", "physical", "sriov"}) && m["parent"] == "" {
+			if shared.StringInSlice(m["nictype"], []string{"bridged", "macvlan", "ipvlan", "physical", "sriov"}) && m["parent"] == "" {
 				return fmt.Errorf("Missing parent for %s type nic", m["nictype"])
 			}
+
+			if m["nictype"] == "ipvlan" {
+
+				var isV4, isV6 bool
+				var val string
+
+				if val, isV4 = m["ipv4.address"]; isV4 {
+					err := networkValidAddressV4List(val)
+					if err != nil {
+						return err
+					}
+				}
+
+				if val, isV6 = m["ipv6.address"]; isV6 {
+					err := networkValidAddressV6List(val)
+					if err != nil {
+						return err
+					}
+				}
+
+				if !isV4 && !isV6 {
+					return fmt.Errorf("Missing IPv4 or IPv6 address for %s nic", m["nictype"])
+				}
+
+			}
+
 		} else if m["type"] == "infiniband" {
 			if m["nictype"] == "" {
 				return fmt.Errorf("Missing nic type")
diff --git a/lxd/container_lxc.go b/lxd/container_lxc.go
index 8b6c9d4fa6..1492be040a 100644
--- a/lxd/container_lxc.go
+++ b/lxd/container_lxc.go
@@ -1567,6 +1567,13 @@ func (c *containerLXC) initLXC(config bool) error {
 				if err != nil {
 					return err
 				}
+			} else if m["nictype"] == "ipvlan" {
+				// go-lxc.v2 lacks support for IPVLAN, so we will create only loopback interface
+				// and at later stage we will move newly created IPVLAN interface from default ns to container ns
+				err = lxcSetConfigItem(cc, fmt.Sprintf("%s.%d.type", networkKeyPrefix, networkidx), "empty")
+				if err != nil {
+					return err
+				}
 			}
 
 			err = lxcSetConfigItem(cc, fmt.Sprintf("%s.%d.flags", networkKeyPrefix, networkidx), "up")
@@ -2319,6 +2326,66 @@ func (c *containerLXC) startCommon() (string, error) {
 				}
 			}
 
+			// Creating loopback device inside container namespace for IPVLAN network
+			if m["type"] == "nic" && m["nictype"] == "ipvlan" {
+				// go-lxc.v2 lacks support for IPVLAN, so we will create only loopback interface
+				// and at later stage we will move newly created IPVLAN interface from default ns to container ns
+				err = lxcSetConfigItem(c.c, fmt.Sprintf("%s.%d.type", networkKeyPrefix, networkidx), "empty")
+				if err != nil {
+					return "", err
+				}
+
+				if !shared.PathExists(fmt.Sprintf("/sys/class/net/%s", m["parent"])) {
+					return "", fmt.Errorf("Parent network device %s does not exist", m["parent"])
+				}
+
+				// enable IPv4 forwarding, sysctl -w net.ipv4.ip_forward=1
+				err = networkSysctl("ipv4/ip_forward", "1")
+				if err != nil {
+					return "", err
+				}
+				// enable IPv4 Proxy ARP, sysctl -w net.ipv4.conf.DEV.proxy_arp=1
+				err = networkSysctl(fmt.Sprintf("ipv4/conf/%s/proxy_arp", m["parent"]), "1")
+				if err != nil {
+					return "", err
+				}
+				// disable IPv4 RP Filter, sysctl -w net.ipv4.conf.DEV.rp_filter=0
+				err = networkSysctl(fmt.Sprintf("ipv4/conf/%s/rp_filter", m["parent"]), "0")
+				if err != nil {
+					return "", err
+				}
+
+				// enable IPv6, sysctl -w net.ipv6.conf.DEV.disable_ipv6=0
+				err = networkSysctl(fmt.Sprintf("ipv6/conf/%s/disable_ipv6", m["parent"]), "0")
+				if err == nil {
+					// enable IPv6 forwarding, sysctl -w net.ipv6.conf.all.forwarding=1
+					err = networkSysctl("ipv6/conf/all/forwarding", "1")
+					if err != nil {
+						return "", err
+					}
+					// enable IPv6 Proxy NDP, sysctl -w net.ipv6.conf.DEV.proxy_ndp=1
+					err = networkSysctl(fmt.Sprintf("ipv6/conf/%s/proxy_ndp", m["parent"]), "1")
+					if err != nil {
+						return "", err
+					}
+					// disable IPv6 autoconfiguration, sysctl -w net.ipv6.conf.DEV.autoconf=0
+					err = networkSysctl(fmt.Sprintf("ipv6/conf/%s/autoconf", m["parent"]), "0")
+					if err != nil {
+						return "", err
+					}
+					// disable IPv6 router advertisement, sysctl -w net.ipv6.conf.DEV.accept_ra=0
+					err = networkSysctl(fmt.Sprintf("ipv6/conf/%s/accept_ra", m["parent"]), "0")
+					if err != nil {
+						return "", err
+					}
+					// disable IPv6 router advertisement default route, sysctl -w net.ipv6.conf.DEV.accept_ra_defrtr=0
+					err = networkSysctl(fmt.Sprintf("ipv6/conf/%s/accept_ra_defrtr", m["parent"]), "0")
+					if err != nil {
+						return "", err
+					}
+				}
+			}
+
 			if m["nictype"] == "bridged" && shared.IsTrue(m["security.mac_filtering"]) {
 				// Read device name from config
 				vethName := ""
@@ -2587,6 +2654,37 @@ func (c *containerLXC) Start(stateful bool) error {
 		return err
 	}
 
+	// Inject IPVLAN interface inside container namespace
+	if c.IsRunning() {
+		for name, m := range c.ExpandedDevices() {
+			if m["type"] == "nic" && m["nictype"] == "ipvlan" {
+				// Creating temporary name for IPVLAN interface.
+				ifaceName, err := networkGenerateInterfaceName(c.Name(), name)
+				if err != nil {
+					return err
+				}
+
+				// Creating IPVLAN interface inside default namespace
+				_, err = shared.RunCommand("ip", "link", "add", ifaceName, "link", m["parent"], "type", "ipvlan", "mode", "l3s")
+				if err != nil {
+					return fmt.Errorf("Failed to create new IPVLAN interface: %s", err)
+				}
+
+				// Moving IPVLAN interface to container namespace
+				_, err = shared.RunCommand("ip", "link", "set", ifaceName, "netns", strconv.Itoa(c.InitPID()), "name", name, "up")
+				if err != nil {
+					return fmt.Errorf("Failed to assign IPVLAN interface to container: %s", err)
+				}
+
+				// Configuring IP addresses
+				err = c.addIPVLANConfiguration()
+				if err != nil {
+					return err
+				}
+			}
+		}
+	}
+
 	logger.Info("Started container", ctxMap)
 	eventSendLifecycle(c.project, "container-started",
 		fmt.Sprintf("/1.0/containers/%s", c.name), nil)
@@ -2913,6 +3011,12 @@ func (c *containerLXC) OnStop(target string) error {
 		// Wait for other post-stop actions to be done
 		c.IsRunning()
 
+		// remove IPVLAN configuration, if exists
+		err = c.removeIPVLANConfiguration()
+		if err != nil {
+			logger.Error("Failed to unconfigure IPVLAN interface", log.Ctx{"container": c.Name(), "err": err})
+		}
+
 		// Unload the apparmor profile
 		err = AADestroy(c)
 		if err != nil {
@@ -7321,7 +7425,7 @@ func (c *containerLXC) restartProxyDevices() error {
 func (c *containerLXC) createNetworkDevice(name string, m types.Device) (string, error) {
 	var dev, n1 string
 
-	if shared.StringInSlice(m["nictype"], []string{"bridged", "p2p", "macvlan"}) {
+	if shared.StringInSlice(m["nictype"], []string{"bridged", "p2p", "macvlan", "ipvlan"}) {
 		// Host Virtual NIC name
 		if m["host_name"] != "" {
 			n1 = m["host_name"]
@@ -7362,8 +7466,8 @@ func (c *containerLXC) createNetworkDevice(name string, m types.Device) (string,
 		dev = n2
 	}
 
-	// Handle physical and macvlan
-	if shared.StringInSlice(m["nictype"], []string{"macvlan", "physical"}) {
+	// Handle physical, macvlan, ipvlan
+	if shared.StringInSlice(m["nictype"], []string{"macvlan", "physical", "ipvlan"}) {
 		// Deal with VLAN
 		device := m["parent"]
 		if m["vlan"] != "" {
@@ -7393,10 +7497,20 @@ func (c *containerLXC) createNetworkDevice(name string, m types.Device) (string,
 
 			dev = n1
 		}
+
+		// Handle ipvlan
+		if m["nictype"] == "ipvlan" {
+			_, err := shared.RunCommand("ip", "link", "add", "dev", n1, "link", device, "type", "ipvlan", "mode", "l3s")
+			if err != nil {
+				return "", fmt.Errorf("Failed to create the new ipvlan interface: %s", err)
+			}
+
+			dev = n1
+		}
 	}
 
-	// Set the MAC address
-	if m["hwaddr"] != "" {
+	// Set the MAC address, skip for IPVLAN type
+	if m["hwaddr"] != "" && m["nictype"] != "ipvlan" {
 		_, err := shared.RunCommand("ip", "link", "set", "dev", dev, "address", m["hwaddr"])
 		if err != nil {
 			deviceRemoveInterface(dev)
@@ -7769,6 +7883,58 @@ func (c *containerLXC) removeNetworkFilter(hwaddr string, bridge string) error {
 	return nil
 }
 
+func (c *containerLXC) removeIPVLANConfiguration() error {
+	for _, m := range c.ExpandedDevices() {
+		// not checking here for nictype == ipvlan,
+		// for network device update to work
+		if m["type"] == "nic" {
+			if val, ok := m["ipv4.address"]; ok {
+				for _, addr := range strings.Fields(val) {
+					err := networkUnConfigureIPVlanAddress(addr, m["parent"])
+					if err != nil {
+						return err
+					}
+				}
+			}
+			if val, ok := m["ipv6.address"]; ok {
+				for _, addr := range strings.Fields(val) {
+					err := networkUnConfigureIPVlanAddress(addr, m["parent"])
+					if err != nil {
+						return err
+					}
+				}
+			}
+		}
+	}
+
+	return nil
+}
+
+func (c *containerLXC) addIPVLANConfiguration() error {
+	for _, m := range c.ExpandedDevices() {
+		if m["type"] == "nic" && m["nictype"] == "ipvlan" {
+			if val, ok := m["ipv4.address"]; ok {
+				for _, addr := range strings.Fields(val) {
+					err := networkConfigureIPVlanAddress(addr, m["parent"])
+					if err != nil {
+						return err
+					}
+				}
+			}
+			if val, ok := m["ipv6.address"]; ok {
+				for _, addr := range strings.Fields(val) {
+					err := networkConfigureIPVlanAddress(addr, m["parent"])
+					if err != nil {
+						return err
+					}
+				}
+			}
+		}
+	}
+
+	return nil
+}
+
 func (c *containerLXC) removeNetworkFilters() error {
 	for k, m := range c.expandedDevices {
 		if m["type"] != "nic" || m["nictype"] != "bridged" {
@@ -7829,6 +7995,12 @@ func (c *containerLXC) insertNetworkDevice(name string, m types.Device) (types.D
 		return nil, fmt.Errorf("Failed to attach interface: %s: %s", devName, err)
 	}
 
+	// Configuring IP addresses for IPVLAN, if exists
+	err = c.addIPVLANConfiguration()
+	if err != nil {
+		return nil, err
+	}
+
 	return m, nil
 }
 
@@ -7862,12 +8034,26 @@ func (c *containerLXC) removeNetworkDevice(name string, m types.Device) error {
 	}
 	defer lxc.Release(cc)
 
-	// Remove the interface from the container
-	err = cc.DetachInterfaceRename(m["name"], hostName)
+	// Check if interface exists inside container namespace
+	ifaces, err := cc.Interfaces()
 	if err != nil {
 		return fmt.Errorf("Failed to detach interface: %s: %s", m["name"], err)
 	}
 
+	// remove IPVLAN configuration, if exists
+	err = c.removeIPVLANConfiguration()
+	if err != nil {
+		return err
+	}
+
+	// Remove the interface from the container if it exists
+	if shared.StringInSlice(m["name"], ifaces) {
+		err = cc.DetachInterfaceRename(m["name"], hostName)
+		if err != nil {
+			return fmt.Errorf("Failed to detach interface: %s: %s", m["name"], err)
+		}
+	}
+
 	// If a veth, destroy it
 	if m["nictype"] != "physical" && m["nictype"] != "sriov" {
 		deviceRemoveInterface(hostName)
diff --git a/lxd/networks_utils.go b/lxd/networks_utils.go
index d3e8fbff4b..7febd1d0cf 100644
--- a/lxd/networks_utils.go
+++ b/lxd/networks_utils.go
@@ -6,6 +6,7 @@ import (
 	"encoding/binary"
 	"encoding/hex"
 	"fmt"
+	"hash/fnv"
 	"io/ioutil"
 	"math"
 	"math/big"
@@ -111,7 +112,7 @@ func networkIsInUse(c container, name string) bool {
 			continue
 		}
 
-		if !shared.StringInSlice(d["nictype"], []string{"bridged", "macvlan", "physical", "sriov"}) {
+		if !shared.StringInSlice(d["nictype"], []string{"bridged", "macvlan", "ipvlan", "physical", "sriov"}) {
 			continue
 		}
 
@@ -1183,3 +1184,121 @@ func networkGetState(netIf net.Interface) api.NetworkState {
 	network.Counters = shared.NetworkGetCounters(netIf.Name)
 	return network
 }
+
+func networkConfigureIPVlanAddress(address, parentDevName string) error {
+
+	if address == "" {
+		return fmt.Errorf("Network address can not be empty for parent nic: %s", parentDevName)
+	}
+
+	// handle IPv4
+	if err := networkValidAddressV4(address); err == nil {
+		// add Local Route
+		_, err := shared.RunCommand("ip", "-4", "route", "add", "local", fmt.Sprintf("%s/32", address), "dev", parentDevName)
+		if err != nil {
+			switch {
+			case strings.Contains(err.Error(), "File exists"):
+			default:
+				return err
+			}
+		}
+		// add Proxy ARP record
+		_, err = shared.RunCommand("ip", "-4", "neigh", "add", "proxy", address, "dev", parentDevName)
+		if err != nil {
+			return err
+		}
+	}
+
+	// handle IPv6
+	if err := networkValidAddressV6(address); err == nil {
+		// add Local Route
+		_, err := shared.RunCommand("ip", "-6", "route", "add", "local", fmt.Sprintf("%s/128", address), "dev", parentDevName)
+		if err != nil {
+			switch {
+			case strings.Contains(err.Error(), "File exists"):
+			default:
+				return err
+			}
+		}
+		// add Proxy NDP record
+		_, err = shared.RunCommand("ip", "-6", "neigh", "add", "proxy", address, "dev", parentDevName)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func networkUnConfigureIPVlanAddress(address, parentDevName string) error {
+
+	if address == "" {
+		return fmt.Errorf("Network address can not be empty for parent nic: %s", parentDevName)
+	}
+
+	// handle IPv4
+	if err := networkValidAddressV4(address); err == nil {
+		// delete Local Route
+		_, err := shared.RunCommand("ip", "-4", "route", "del", "local", fmt.Sprintf("%s/32", address), "dev", parentDevName)
+		if err != nil {
+			switch {
+			case strings.Contains(err.Error(), "No such process"):
+			case strings.Contains(err.Error(), "No such file or directory"):
+			default:
+				return err
+			}
+		}
+		// delete Proxy ARP record
+		_, err = shared.RunCommand("ip", "-4", "neigh", "del", "proxy", address, "dev", parentDevName)
+		if err != nil {
+			switch {
+			case strings.Contains(err.Error(), "No such process"):
+			case strings.Contains(err.Error(), "No such file or directory"):
+			default:
+				return err
+			}
+		}
+	}
+
+	// handle IPv6
+	if err := networkValidAddressV6(address); err == nil {
+		// delete Local Route
+		_, err := shared.RunCommand("ip", "-6", "route", "del", "local", fmt.Sprintf("%s/128", address), "dev", parentDevName)
+		if err != nil {
+			switch {
+			case strings.Contains(err.Error(), "No such process"):
+			case strings.Contains(err.Error(), "No such file or directory"):
+			default:
+				return err
+			}
+		}
+		// delete Proxy NDP record
+		_, err = shared.RunCommand("ip", "-6", "neigh", "del", "proxy", address, "dev", parentDevName)
+		if err != nil {
+			switch {
+			case strings.Contains(err.Error(), "No such process"):
+			case strings.Contains(err.Error(), "No such file or directory"):
+			default:
+				return err
+			}
+		}
+	}
+
+	return nil
+}
+
+func networkGenerateInterfaceName(preffix, suffix string) (string, error) {
+	// Interface name is based upon hash (32-bit FNV-1a) of supplied preffix and suffix.
+	// Function can be used to make interface creation thread-safe and predictable.
+	// Interface name length is limited by kernel, so we use 32-bit sized hash.
+	h := fnv.New32a()
+	h.Write([]byte(fmt.Sprintf("%s-%s", preffix, suffix)))
+	name := fmt.Sprintf("eth%010d", h.Sum32())
+
+	err := networkValidName(name)
+	if err != nil {
+		return "", err
+	}
+
+	return name, nil
+}

From ebeeb923ddc547f06ae20ddfaf6c7eefd7e70dc8 Mon Sep 17 00:00:00 2001
From: s3rj1k <evasive.gyron at gmail.com>
Date: Wed, 17 Oct 2018 12:42:01 +0300
Subject: [PATCH 3/7] use case instead of if inside devlxdConfig

---
 lxd/devlxd.go | 10 ++++++++--
 1 file changed, 8 insertions(+), 2 deletions(-)

diff --git a/lxd/devlxd.go b/lxd/devlxd.go
index eb7735b215..06f9346003 100644
--- a/lxd/devlxd.go
+++ b/lxd/devlxd.go
@@ -59,8 +59,11 @@ type devLxdHandler struct {
 var devlxdConfigGet = devLxdHandler{"/1.0/config", func(d *Daemon, c container, w http.ResponseWriter, r *http.Request) *devLxdResponse {
 	filtered := []string{}
 	for k := range c.ExpandedConfig() {
-		if strings.HasPrefix(k, "user.") {
+		switch {
+		case strings.HasPrefix(k, "user."):
 			filtered = append(filtered, fmt.Sprintf("/1.0/config/%s", k))
+		default:
+			continue
 		}
 	}
 	return okResponse(filtered, "json")
@@ -68,7 +71,10 @@ var devlxdConfigGet = devLxdHandler{"/1.0/config", func(d *Daemon, c container,
 
 var devlxdConfigKeyGet = devLxdHandler{"/1.0/config/{key}", func(d *Daemon, c container, w http.ResponseWriter, r *http.Request) *devLxdResponse {
 	key := mux.Vars(r)["key"]
-	if !strings.HasPrefix(key, "user.") {
+
+	switch {
+	case strings.HasPrefix(key, "user."):
+	default:
 		return &devLxdResponse{"not authorized", http.StatusForbidden, "raw"}
 	}
 

From f47c859d8915c9f259acc1ab4c488007054bd1f7 Mon Sep 17 00:00:00 2001
From: s3rj1k <evasive.gyron at gmail.com>
Date: Wed, 17 Oct 2018 12:43:39 +0300
Subject: [PATCH 4/7] expose IPVLAN device inside devLXD

---
 lxd/devlxd.go | 46 ++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 46 insertions(+)

diff --git a/lxd/devlxd.go b/lxd/devlxd.go
index 06f9346003..064562f38e 100644
--- a/lxd/devlxd.go
+++ b/lxd/devlxd.go
@@ -86,6 +86,50 @@ var devlxdConfigKeyGet = devLxdHandler{"/1.0/config/{key}", func(d *Daemon, c co
 	return okResponse(value, "raw")
 }}
 
+var devlxdDevicesGet = devLxdHandler{"/1.0/devices", func(d *Daemon, c container, w http.ResponseWriter, r *http.Request) *devLxdResponse {
+	filtered := []string{}
+	for name, m := range c.ExpandedDevices() {
+		switch {
+		case m["type"] == "nic" && m["nictype"] == "ipvlan":
+			filtered = append(filtered, fmt.Sprintf("/1.0/device/%s", name))
+		default:
+			continue
+		}
+	}
+	return okResponse(filtered, "json")
+}}
+
+var devlxdDeviceOptionsGet = devLxdHandler{"/1.0/device/{key}", func(d *Daemon, c container, w http.ResponseWriter, r *http.Request) *devLxdResponse {
+	key := mux.Vars(r)["key"]
+
+	m, ok := c.ExpandedDevices()[key]
+	if !ok {
+		return &devLxdResponse{"not found", http.StatusNotFound, "raw"}
+	}
+
+	switch {
+	case m["type"] == "nic" && m["nictype"] == "ipvlan":
+		filtered := make(map[string]string, len(m))
+		for k, v := range m {
+			switch k {
+			case "type":
+				filtered[k] = v
+			case "ipv4.address":
+				filtered[k] = v
+			case "ipv6.address":
+				filtered[k] = v
+			case "nictype":
+				filtered[k] = v
+			default:
+				continue
+			}
+		}
+		return okResponse(filtered, "json")
+	default:
+		return &devLxdResponse{"not authorized", http.StatusForbidden, "raw"}
+	}
+}}
+
 var devlxdImageExport = devLxdHandler{"/1.0/images/{fingerprint}/export", func(d *Daemon, c container, w http.ResponseWriter, r *http.Request) *devLxdResponse {
 	if !shared.IsTrue(c.ExpandedConfig()["security.devlxd.images"]) {
 		return &devLxdResponse{"not authorized", http.StatusForbidden, "raw"}
@@ -214,6 +258,8 @@ var handlers = []devLxdHandler{
 	}},
 	devlxdConfigGet,
 	devlxdConfigKeyGet,
+	devlxdDevicesGet,
+	devlxdDeviceOptionsGet,
 	devlxdMetadataGet,
 	devlxdEventsGet,
 	devlxdImageExport,

From fca78fe7d454c6f3650b711c01370b9f018410e5 Mon Sep 17 00:00:00 2001
From: s3rj1k <evasive.gyron at gmail.com>
Date: Wed, 17 Oct 2018 12:58:32 +0300
Subject: [PATCH 5/7] add nameserver key configuration for nic (IPVLAN) device

---
 lxd/container.go      |  9 +++++++++
 lxd/devlxd.go         |  2 ++
 lxd/networks_utils.go | 11 +++++++++++
 3 files changed, 22 insertions(+)

diff --git a/lxd/container.go b/lxd/container.go
index 12d6479ef9..e631a7e5b6 100644
--- a/lxd/container.go
+++ b/lxd/container.go
@@ -135,6 +135,8 @@ func containerValidDeviceConfigKey(t, k string) bool {
 			return true
 		case "vlan":
 			return true
+		case "nameserver":
+			return true
 		case "ipv4.address":
 			return true
 		case "ipv6.address":
@@ -383,6 +385,13 @@ func containerValidDevices(db *db.Cluster, devices types.Devices, profile bool,
 					}
 				}
 
+				if val, ok = m["nameserver"]; ok {
+					err := networkValidAddressList(val)
+					if err != nil {
+						return err
+					}
+				}
+
 				if !isV4 && !isV6 {
 					return fmt.Errorf("Missing IPv4 or IPv6 address for %s nic", m["nictype"])
 				}
diff --git a/lxd/devlxd.go b/lxd/devlxd.go
index 064562f38e..d23bd25bed 100644
--- a/lxd/devlxd.go
+++ b/lxd/devlxd.go
@@ -118,6 +118,8 @@ var devlxdDeviceOptionsGet = devLxdHandler{"/1.0/device/{key}", func(d *Daemon,
 				filtered[k] = v
 			case "ipv6.address":
 				filtered[k] = v
+			case "nameserver":
+				filtered[k] = v
 			case "nictype":
 				filtered[k] = v
 			default:
diff --git a/lxd/networks_utils.go b/lxd/networks_utils.go
index 7febd1d0cf..ebc685d2fc 100644
--- a/lxd/networks_utils.go
+++ b/lxd/networks_utils.go
@@ -588,6 +588,17 @@ func networkValidAddressV6List(value string) error {
 	return nil
 }
 
+// validate list of IPs (space separated)
+func networkValidAddressList(value string) error {
+	for _, v := range strings.Fields(value) {
+		err := networkValidAddress(v)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
 func networkValidAddressV4(value string) error {
 	if value == "" {
 		return nil

From 7145b106b4d7b6939714158e61ce50c7465d4494 Mon Sep 17 00:00:00 2001
From: s3rj1k <evasive.gyron at gmail.com>
Date: Wed, 17 Oct 2018 13:09:43 +0300
Subject: [PATCH 6/7] fix nameserver key check

---
 lxd/container.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lxd/container.go b/lxd/container.go
index e631a7e5b6..3d4dd8ae9b 100644
--- a/lxd/container.go
+++ b/lxd/container.go
@@ -385,7 +385,7 @@ func containerValidDevices(db *db.Cluster, devices types.Devices, profile bool,
 					}
 				}
 
-				if val, ok = m["nameserver"]; ok {
+				if val, ok := m["nameserver"]; ok {
 					err := networkValidAddressList(val)
 					if err != nil {
 						return err

From e4db677765d114de95ee2573974eb1c827dab93f Mon Sep 17 00:00:00 2001
From: s3rj1k <evasive.gyron at gmail.com>
Date: Wed, 17 Oct 2018 13:39:53 +0300
Subject: [PATCH 7/7] update docs for network config, devlxd

---
 doc/containers.md | 51 +++++++++++++++++++++++++++++++----------------
 doc/dev-lxd.md    | 34 +++++++++++++++++++++++++++++++
 2 files changed, 68 insertions(+), 17 deletions(-)

diff --git a/doc/containers.md b/doc/containers.md
index 211c50febb..3c57a28610 100644
--- a/doc/containers.md
+++ b/doc/containers.md
@@ -225,28 +225,30 @@ LXD supports different kind of network devices:
  - `physical`: Straight physical device passthrough from the host. The targeted device will vanish from the host and appear in the container.
  - `bridged`: Uses an existing bridge on the host and creates a virtual device pair to connect the host bridge to the container.
  - `macvlan`: Sets up a new network device based on an existing one but using a different MAC address.
+ - `ipvlan`: Sets up a new network device based on an existing one using a same MAC address.
  - `p2p`: Creates a virtual device pair, putting one side in the container and leaving the other side on the host.
  - `sriov`: Passes a virtual function of an SR-IOV enabled physical network device into the container.
 
 Different network interface types have different additional properties, the current list is:
 
-Key                     | Type      | Default           | Required  | Used by                           | API extension                          | Description
-:--                     | :--       | :--               | :--       | :--                               | :--                                    | :--
-nictype                 | string    | -                 | yes       | all                               | -                                      | The device type, one of "bridged", "macvlan", "p2p", "physical", or "sriov"
-limits.ingress          | string    | -                 | no        | bridged, p2p                      | -                                      | I/O limit in bit/s for incoming traffic (supports kbit, Mbit, Gbit suffixes)
-limits.egress           | string    | -                 | no        | bridged, p2p                      | -                                      | I/O limit in bit/s for outgoing traffic (supports kbit, Mbit, Gbit suffixes)
-limits.max              | string    | -                 | no        | bridged, p2p                      | -                                      | Same as modifying both limits.ingress and limits.egress
-name                    | string    | kernel assigned   | no        | all                               | -                                      | The name of the interface inside the container
-host\_name              | string    | randomly assigned | no        | bridged, macvlan, p2p, sriov      | -                                      | The name of the interface inside the host
-hwaddr                  | string    | randomly assigned | no        | all                               | -                                      | The MAC address of the new interface
-mtu                     | integer   | parent MTU        | no        | all                               | -                                      | The MTU of the new interface
-parent                  | string    | -                 | yes       | bridged, macvlan, physical, sriov | -                                      | The name of the host device or bridge
-vlan                    | integer   | -                 | no        | macvlan, physical                 | network\_vlan, network\_vlan\_physical | The VLAN ID to attach to
-ipv4.address            | string    | -                 | no        | bridged                           | network                                | An IPv4 address to assign to the container through DHCP
-ipv6.address            | string    | -                 | no        | bridged                           | network                                | An IPv6 address to assign to the container through DHCP
-security.mac\_filtering | boolean   | false             | no        | bridged                           | network                                | Prevent the container from spoofing another's MAC address
-maas.subnet.ipv4        | string    | -                 | no        | bridged, macvlan, physical, sriov | maas\_network                          | MAAS IPv4 subnet to register the container in
-maas.subnet.ipv6        | string    | -                 | no        | bridged, macvlan, physical, sriov | maas\_network                          | MAAS IPv6 subnet to register the container in
+Key                     | Type      | Default           | Required  | Used by                                   | API extension                          | Description
+:--                     | :--       | :--               | :--       | :--                                       | :--                                    | :--
+nictype                 | string    | -                 | yes       | all                                       | -                                      | The device type, one of "bridged", "macvlan", "p2p", "physical", or "sriov"
+limits.ingress          | string    | -                 | no        | bridged, p2p                              | -                                      | I/O limit in bit/s for incoming traffic (supports kbit, Mbit, Gbit suffixes)
+limits.egress           | string    | -                 | no        | bridged, p2p                              | -                                      | I/O limit in bit/s for outgoing traffic (supports kbit, Mbit, Gbit suffixes)
+limits.max              | string    | -                 | no        | bridged, p2p                              | -                                      | Same as modifying both limits.ingress and limits.egress
+name                    | string    | kernel assigned   | no        | all                                       | -                                      | The name of the interface inside the container
+host\_name              | string    | randomly assigned | no        | bridged, macvlan, ipvlan, p2p, sriov      | -                                      | The name of the interface inside the host
+hwaddr                  | string    | randomly assigned | no        | all                                       | -                                      | The MAC address of the new interface
+mtu                     | integer   | parent MTU        | no        | all                                       | -                                      | The MTU of the new interface
+parent                  | string    | -                 | yes       | bridged, macvlan, ipvlan, physical, sriov | -                                      | The name of the host device or bridge
+vlan                    | integer   | -                 | no        | macvlan, physical                         | network\_vlan, network\_vlan\_physical | The VLAN ID to attach to
+ipv4.address            | string    | -                 | no*       | bridged, ipvlan                           | network                                | An IPv4 address to assign to the container through DHCP, for IPVLAN list of IPv4 for container (required at least one IPv4/IPv6)
+ipv6.address            | string    | -                 | no*       | bridged, ipvlan                           | network                                | An IPv6 address to assign to the container through DHCP, for IPVLAN list of IPv6 for container (required at least one IPv4/IPv6)
+nameserver              | string    | -                 | no        | ipvlan                                    | network                                | A list of Nameserver IPs that will be exposed to container exposed through devLXD
+security.mac\_filtering | boolean   | false             | no        | bridged                                   | network                                | Prevent the container from spoofing another's MAC address
+maas.subnet.ipv4        | string    | -                 | no        | bridged, macvlan, physical, sriov         | maas\_network                          | MAAS IPv4 subnet to register the container in
+maas.subnet.ipv6        | string    | -                 | no        | bridged, macvlan, physical, sriov         | maas\_network                          | MAAS IPv6 subnet to register the container in
 
 #### bridged or macvlan for connection to physical network
 The `bridged` and `macvlan` interface types can both be used to connect
@@ -265,6 +267,21 @@ your containers to talk to the host itself.
 In such case, a bridge is preferable. A bridge will also let you use mac
 filtering and I/O limits which cannot be applied to a macvlan device.
 
+#### IPVLAN
+The `ipvlan` interface type can both be used to connect
+to an existing physical network without L2 support.
+
+This interface type is similar to `macvlan` but instead it uses parent MAC.
+This effectively saves MAC address table size of network devices.
+Also `ipvlan` in L3s mode support netfilter, so you can use iptables
+for container from hardware node.
+
+The downside to this is that `ipvlan` configuration is more cumbersome,
+lack of L2 forces you to use Proxy ARP and Proxy NDP, this issue is solved
+by LXD itself.
+
+To expose IP configuration inside container LXD uses superpowers of DevLXD.
+
 #### SR-IOV
 The `sriov` interface type supports SR-IOV enabled network devices. These
 devices associate a set of virtual functions (VFs) with the single physical
diff --git a/doc/dev-lxd.md b/doc/dev-lxd.md
index 692f2513b4..7879007152 100644
--- a/doc/dev-lxd.md
+++ b/doc/dev-lxd.md
@@ -40,6 +40,8 @@ authentication support in the `/dev/lxd/sock` API.
    * /1.0
      * /1.0/config
        * /1.0/config/{key}
+     * /1.0/devices
+       * /1.0/device/{key}
      * /1.0/events
      * /1.0/images/{fingerprint}/export
      * /1.0/meta-data
@@ -98,6 +100,38 @@ Return value:
 
     blah
 
+#### `/1.0/devices`
+##### GET
+ * Description: List of devices keys
+ * Return: list of devices keys URL
+
+Currently only `ipvlan` device keys are accessible to the container.
+
+Return value:
+
+```json
+[
+    "/1.0/device/eth0"
+]
+```
+
+#### `/1.0/device/<KEY>`
+##### GET
+ * Description: Value of that key
+ * Return: JSON multi-parameter value
+
+Return value:
+
+```json
+{
+  "ipv4.address": "80.80.90.60",
+  "ipv6.address": "2a02:2200:1:2eee:80:80:90:60",
+  "nameserver": "8.8.8.8 1.1.1.1",
+  "nictype": "ipvlan",
+  "type": "nic"
+}
+```
+
 #### `/1.0/events`
 ##### GET
  * Description: websocket upgrade


More information about the lxc-devel mailing list