[lxc-devel] [lxd/master] IP Filtering with dynamic IPs

tomponline on Github lxc-bot at linuxcontainers.org
Fri Jun 28 12:47:03 UTC 2019


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 370 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20190628/94acb86e/attachment-0001.bin>
-------------- next part --------------
From 7f372ca362462410183643085f9f2d5a03d30535 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Thu, 27 Jun 2019 16:33:18 +0100
Subject: [PATCH 01/15] container/lxc: Ensure if bridge nic added/removed that
 dnsmasq config is refreshed

Fixes #5892

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/container_lxc.go | 13 ++++++++-----
 1 file changed, 8 insertions(+), 5 deletions(-)

diff --git a/lxd/container_lxc.go b/lxd/container_lxc.go
index 1471d401bd..5463cc2fae 100644
--- a/lxd/container_lxc.go
+++ b/lxd/container_lxc.go
@@ -5790,12 +5790,15 @@ func (c *containerLXC) Update(args db.ContainerArgs, userRequested bool) error {
 		}
 	}
 
-	// Update network leases
+	// Update network leases if a bridged device has changed.
 	needsUpdate := false
-	for _, m := range updateDevices {
-		if m["type"] == "nic" && m["nictype"] == "bridged" {
-			needsUpdate = true
-			break
+	deviceLists := []map[string]types.Device{removeDevices, addDevices, updateDevices}
+	for _, deviceList := range deviceLists {
+		for _, m := range deviceList {
+			if m["type"] == "nic" && m["nictype"] == "bridged" {
+				needsUpdate = true
+				break
+			}
 		}
 	}
 

From 97b658573e682c9ffb23bf846cc29a3a401b615e Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Fri, 28 Jun 2019 10:30:42 +0100
Subject: [PATCH 02/15] test: Adds test for checking dnsmasq host config is
 refreshed when nic added/removed

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 test/suites/container_devices_nic_bridged.sh | 29 ++++++++++++++++++++
 1 file changed, 29 insertions(+)

diff --git a/test/suites/container_devices_nic_bridged.sh b/test/suites/container_devices_nic_bridged.sh
index 925d3052fa..b9ccac54dc 100644
--- a/test/suites/container_devices_nic_bridged.sh
+++ b/test/suites/container_devices_nic_bridged.sh
@@ -288,7 +288,36 @@ test_container_devices_nic_bridged() {
     false
   fi
 
+  # Check dnsmasq host file is updated on new device.
+  lxc init testimage "${ctName}" -p "${ctName}"
+  lxc config device add "${ctName}" eth0 nic nictype=bridged parent="${brName}" name=eth0 ipv4.address=192.0.2.200 ipv6.address=2001:db8::200
+
+  ls -lR "${LXD_DIR}/networks/${brName}/dnsmasq.hosts/"
+
+  if ! grep "192.0.2.200" "${LXD_DIR}/networks/${brName}/dnsmasq.hosts/${ctName}" ; then
+    echo "dnsmasq host config not updated with IPv4 address"
+    false
+  fi
+
+  if ! grep "2001:db8::200" "${LXD_DIR}/networks/${brName}/dnsmasq.hosts/${ctName}" ; then
+    echo "dnsmasq host config not updated with IPv6 address"
+    false
+  fi
+
+  lxc config device remove "${ctName}" eth0
+
+  if grep "192.0.2.200" "${LXD_DIR}/networks/${brName}/dnsmasq.hosts/${ctName}" ; then
+    echo "dnsmasq host config still has old IPv4 address"
+    false
+  fi
+
+  if grep "2001:db8::200" "${LXD_DIR}/networks/${brName}/dnsmasq.hosts/${ctName}" ; then
+    echo "dnsmasq host config still has old IPv6 address"
+    false
+  fi
+
   # Cleanup.
+  lxc delete "${ctName}" -f
   lxc network delete "${brName}"
   lxc profile delete "${ctName}"
 }

From c485e9c07304b5461964d0848ffb42581851ee25 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Thu, 27 Jun 2019 16:32:22 +0100
Subject: [PATCH 03/15] container: Relaxes validation for
 security.ipv4_filtering and security.ipv6_filtering

These no longer need a static IP defined, opening the way for automatic IP allocation.

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/container.go | 8 --------
 1 file changed, 8 deletions(-)

diff --git a/lxd/container.go b/lxd/container.go
index 29e4d06b1d..bec08a195e 100644
--- a/lxd/container.go
+++ b/lxd/container.go
@@ -443,20 +443,12 @@ func containerValidDevices(cluster *db.Cluster, devices types.Devices, profile b
 				if m["nictype"] != "bridged" {
 					return fmt.Errorf("Bad nic type for security.ipv4_filtering: %s", m["nictype"])
 				}
-
-				if m["ipv4.address"] == "" {
-					return fmt.Errorf("ipv4.address required for security.ipv4_filtering")
-				}
 			}
 
 			if shared.IsTrue(m["security.ipv6_filtering"]) {
 				if m["nictype"] != "bridged" {
 					return fmt.Errorf("Bad nic type for security.ipv6_filtering: %s", m["nictype"])
 				}
-
-				if m["ipv6.address"] == "" {
-					return fmt.Errorf("ipv6.address required for security.ipv6_filtering")
-				}
 			}
 		} else if m["type"] == "infiniband" {
 			if m["nictype"] == "" {

From 0cc23fabe6eac3cbafeac6bb98d0ca84e0f14085 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Fri, 28 Jun 2019 11:44:42 +0100
Subject: [PATCH 04/15] shared/container: Adds volatile prefixes .ipv4_address
 and .ipv6_address

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 shared/container.go | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/shared/container.go b/shared/container.go
index d46ea943e1..dd13e2defa 100644
--- a/shared/container.go
+++ b/shared/container.go
@@ -351,6 +351,14 @@ func ConfigKeyChecker(key string) (func(value string) error, error) {
 		if strings.HasSuffix(key, ".spoofcheck") {
 			return IsAny, nil
 		}
+
+		if strings.HasSuffix(key, ".ipv4_address") {
+			return IsAny, nil
+		}
+
+		if strings.HasSuffix(key, ".ipv6_address") {
+			return IsAny, nil
+		}
 	}
 
 	if strings.HasPrefix(key, "environment.") {

From e3f2a68a90c6c613b423f899293f52356f723b18 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Thu, 27 Jun 2019 16:35:51 +0100
Subject: [PATCH 05/15] networks/utils: Adds networkDHCPAllocatedIPs and
 networkDHCPStaticContainerIPs functions

These functions trawl the static dnsmasq host config files and dynamic leases file for a list of all used IPs.

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/networks_utils.go | 147 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 147 insertions(+)

diff --git a/lxd/networks_utils.go b/lxd/networks_utils.go
index 3f84e55b23..619e1e97b4 100644
--- a/lxd/networks_utils.go
+++ b/lxd/networks_utils.go
@@ -879,6 +879,153 @@ func networkGetDnsmasqVersion() (*version.DottedVersion, error) {
 	return version.NewDottedVersion(lines[2])
 }
 
+// DHCPAllocation represents an IP allocation from dnsmasq.
+type DHCPAllocation struct {
+	IP     net.IP
+	Name   string
+	MAC    net.HardwareAddr
+	Static bool
+}
+
+// networkDHCPStaticContainerIPs retrieves the dnsmasq statically allocated IPs for a container.
+// Returns IPv4 and IPv6 DHCPAllocation structs respectively.
+func networkDHCPStaticContainerIPs(network string, containerName string) (DHCPAllocation, DHCPAllocation, error) {
+	var IPv4, IPv6 DHCPAllocation
+
+	file, err := os.Open(shared.VarPath("networks", network, "dnsmasq.hosts") + "/" + containerName)
+	if err != nil {
+		return IPv4, IPv6, err
+	}
+	defer file.Close()
+
+	scanner := bufio.NewScanner(file)
+	for scanner.Scan() {
+		fields := strings.SplitN(scanner.Text(), ",", -1)
+		for _, field := range fields {
+			// Check if field is IPv4 or IPv6 address.
+			if strings.Count(field, ".") == 3 {
+				IP := net.ParseIP(field)
+				if IP.To4() == nil {
+					return IPv4, IPv6, fmt.Errorf("Error parsing IP address: %v", field)
+				}
+				IPv4 = DHCPAllocation{Name: containerName, Static: true, IP: IP.To4()}
+
+			} else if strings.HasPrefix(field, "[") && strings.HasSuffix(field, "]") {
+				IP := net.ParseIP(field[1 : len(field)-1])
+				if IP == nil {
+					return IPv4, IPv6, fmt.Errorf("Error parsing IP address: %v", field)
+				}
+				IPv6 = DHCPAllocation{Name: containerName, Static: true, IP: IP}
+			}
+		}
+	}
+	if err := scanner.Err(); err != nil {
+		return IPv4, IPv6, err
+	}
+
+	return IPv4, IPv6, nil
+}
+
+// networkDHCPAllocatedIPs returns a map of IPs currently allocated (statically and dynamically)
+// in dnsmasq for a specific network. The returned map is keyed by a 16 byte array representing
+// the net.IP format. The value of each map item is a DHCPAllocation struct containing at least
+// whether the allocation was static or dynamic and optionally container name or MAC address.
+// MAC addresses are only included for dynamic IPv4 allocations (where name is not reliable).
+// Static allocations are not overridden by dynamic allocations, allowing for container name to be
+// included for static IPv6 allocations. IPv6 addresses that are dynamically assigned cannot be
+// reliably linked to containers using either name or MAC because dnsmasq does not record the MAC
+// address for these records, and the recorded host name can be set by the container if the dns.mode
+// for the network is set to "dynamic" and so cannot be trusted, so in this case we do not return
+// any identifying info.
+func networkDHCPAllocatedIPs(network string) (map[[4]byte]DHCPAllocation, map[[16]byte]DHCPAllocation, error) {
+	IPv4s := make(map[[4]byte]DHCPAllocation)
+	IPv6s := make(map[[16]byte]DHCPAllocation)
+
+	// First read all statically allocated IPs.
+	files, err := ioutil.ReadDir(shared.VarPath("networks", network, "dnsmasq.hosts"))
+	if err != nil {
+		return IPv4s, IPv6s, err
+	}
+
+	for _, entry := range files {
+		IPv4, IPv6, err := networkDHCPStaticContainerIPs(network, entry.Name())
+		if err != nil {
+			return IPv4s, IPv6s, err
+		}
+
+		if IPv4.IP != nil {
+			var IPKey [4]byte
+			copy(IPKey[:], IPv4.IP.To4())
+			IPv4s[IPKey] = IPv4
+		}
+
+		if IPv6.IP != nil {
+			var IPKey [16]byte
+			copy(IPKey[:], IPv6.IP.To16())
+			IPv6s[IPKey] = IPv6
+		}
+	}
+
+	// Next read all dynamic allocated IPs.
+	file, err := os.Open(shared.VarPath("networks", network, "dnsmasq.leases"))
+	if err != nil {
+		return IPv4s, IPv6s, err
+	}
+	defer file.Close()
+
+	scanner := bufio.NewScanner(file)
+	for scanner.Scan() {
+		fields := strings.Fields(scanner.Text())
+		if len(fields) == 5 {
+			IP := net.ParseIP(fields[2])
+			if IP == nil {
+				return IPv4s, IPv6s, fmt.Errorf("Error parsing IP address: %v", fields[2])
+			}
+
+			// Handle IPv6 addresses.
+			if IP.To4() == nil {
+				var IPKey [16]byte
+				copy(IPKey[:], IP.To16())
+
+				// Don't replace IPs from static config as more reliable.
+				if IPv6s[IPKey].Name != "" {
+					continue
+				}
+
+				IPv6s[IPKey] = DHCPAllocation{
+					Static: false,
+					IP:     IP.To16(),
+				}
+			} else {
+				// MAC only available in IPv4 leases.
+				MAC, err := net.ParseMAC(fields[1])
+				if err != nil {
+					return IPv4s, IPv6s, err
+				}
+
+				var IPKey [4]byte
+				copy(IPKey[:], IP.To4())
+
+				// Don't replace IPs from static config as more reliable.
+				if IPv4s[IPKey].Name != "" {
+					continue
+				}
+
+				IPv4s[IPKey] = DHCPAllocation{
+					MAC:    MAC,
+					Static: false,
+					IP:     IP.To4(),
+				}
+			}
+		}
+	}
+	if err := scanner.Err(); err != nil {
+		return IPv4s, IPv6s, err
+	}
+
+	return IPv4s, IPv6s, nil
+}
+
 func networkUpdateStatic(s *state.State, networkName string) error {
 	// We don't want to race with ourselves here
 	networkStaticLock.Lock()

From 962ed025b34dd937f6e11a105b2ba7671fe5bf07 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Thu, 27 Jun 2019 16:37:07 +0100
Subject: [PATCH 06/15] networks/utils: Adds networkDHCPFindFreeIPv4 and
 networkDHCPFindFreeIPv6 functions

These functions use the used IP lists from networkDHCPAllocatedIPs to figure out the next free IP.

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/networks_utils.go | 132 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 132 insertions(+)

diff --git a/lxd/networks_utils.go b/lxd/networks_utils.go
index 619e1e97b4..9461053840 100644
--- a/lxd/networks_utils.go
+++ b/lxd/networks_utils.go
@@ -22,6 +22,7 @@ import (
 
 	"github.com/google/gopacket"
 	"github.com/google/gopacket/layers"
+	"github.com/mdlayher/eui64"
 	"golang.org/x/net/context"
 	"golang.org/x/sys/unix"
 
@@ -1026,6 +1027,137 @@ func networkDHCPAllocatedIPs(network string) (map[[4]byte]DHCPAllocation, map[[1
 	return IPv4s, IPv6s, nil
 }
 
+// networkDHCPFindFreeIPv6 attempts to find a free IPv6 address for the device.
+// It first checks whether there is an existing allocation for the container. Due to the limitations
+// of dnsmasq lease file format, we can only search for previous static allocations.
+// If no previous allocation, then if SLAAC (stateless) mode is enabled on the network, or if
+// DHCPv6 stateful mode is enabled without custom ranges, then an EUI64 IP is generated from the
+// device's MAC address. Finally if stateful custom ranges are enabled, then a free IP is picked
+// from the ranges configured.
+func networkDHCPFindFreeIPv6(usedIPs map[[16]byte]DHCPAllocation, netConfig map[string]string, ctName string, deviceMAC string) (net.IP, error) {
+	// Lets see if there is already an allocation for container on this network.
+	for _, DHCP := range usedIPs {
+		if ctName == DHCP.Name {
+			return DHCP.IP, nil
+		}
+	}
+
+	// If no existing allocation found, try and find a free one in the subnet pool.
+	lxdIP, subnet, err := net.ParseCIDR(netConfig["ipv6.address"])
+	if err != nil {
+		return nil, err
+	}
+
+	// Try using an EUI64 IP when in either SLAAC or DHCPv6 stateful mode without custom ranges.
+	if !shared.IsTrue(netConfig["ipv6.dhcp.stateful"]) || netConfig["ipv6.dhcp.ranges"] == "" {
+		MAC, err := net.ParseMAC(deviceMAC)
+		if err != nil {
+			return nil, err
+		}
+
+		IP, err := eui64.ParseMAC(subnet.IP, MAC)
+		if err != nil {
+			return nil, err
+		}
+
+		// Check IP is not already allocated.
+		var IPKey [16]byte
+		copy(IPKey[:], IP.To16())
+		if _, inUse := usedIPs[IPKey]; !inUse {
+			return IP, nil
+		}
+	}
+
+	// If we get here, then someone already has our SLAAC IP, or we are using custom ranges.
+	inc := big.NewInt(1)
+	startBig := big.NewInt(0)
+	startBig.SetBytes(networkGetIP(subnet, 1))
+	endBig := big.NewInt(0)
+	endBig.SetBytes(networkGetIP(subnet, -1))
+
+	for {
+		if startBig.Cmp(endBig) >= 0 {
+			break
+		}
+
+		IP := net.IP(startBig.Bytes())
+
+		// Check IP generated is not LXD's IP.
+		if IP.Equal(lxdIP) {
+			startBig.Add(startBig, inc)
+			continue
+		}
+
+		// Check IP is not already allocated.
+		var IPKey [16]byte
+		copy(IPKey[:], IP.To16())
+		if _, inUse := usedIPs[IPKey]; inUse {
+			startBig.Add(startBig, inc)
+			continue
+		}
+
+		// Used by networkUpdateStatic temporarily to build new static allocations.
+		return IP, nil
+	}
+
+	return nil, fmt.Errorf("No available IP could not be found")
+}
+
+// networkDHCPFindFreeIPv4 attempts to find a free IPv4 address for the device.
+// It first checks whether there is an existing allocation for the container.
+// If no previous allocation, then a free IP is picked from the ranges configured.
+func networkDHCPFindFreeIPv4(usedIPs map[[4]byte]DHCPAllocation, netConfig map[string]string, ctName string, deviceMAC string) (net.IP, error) {
+	MAC, err := net.ParseMAC(deviceMAC)
+	if err != nil {
+		return nil, err
+	}
+
+	// Lets see if there is already an allocation for our device.
+	for _, DHCP := range usedIPs {
+		if ctName == DHCP.Name || bytes.Compare(MAC, DHCP.MAC) == 0 {
+			return DHCP.IP, nil
+		}
+	}
+
+	// If no existing allocation found, try and find a free one in the subnet pool.
+	lxdIP, subnet, err := net.ParseCIDR(netConfig["ipv4.address"])
+	if err != nil {
+		return nil, err
+	}
+
+	inc := big.NewInt(1)
+	startBig := big.NewInt(0)
+	startBig.SetBytes(networkGetIP(subnet, 1))
+	endBig := big.NewInt(0)
+	endBig.SetBytes(networkGetIP(subnet, -2))
+
+	for {
+		if startBig.Cmp(endBig) >= 0 {
+			break
+		}
+
+		IP := net.IP(startBig.Bytes())
+
+		// Check IP generated is not LXD's IP.
+		if IP.Equal(lxdIP) {
+			startBig.Add(startBig, inc)
+			continue
+		}
+
+		// Check IP is not already allocated.
+		var IPKey [4]byte
+		copy(IPKey[:], IP.To4())
+		if _, inUse := usedIPs[IPKey]; inUse {
+			startBig.Add(startBig, inc)
+			continue
+		}
+
+		return IP, nil
+	}
+
+	return nil, fmt.Errorf("No available IP could not be found")
+}
+
 func networkUpdateStatic(s *state.State, networkName string) error {
 	// We don't want to race with ourselves here
 	networkStaticLock.Lock()

From 2b8ba7332951c5aca2ad34a93687e3842c2a405e Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Thu, 27 Jun 2019 16:38:44 +0100
Subject: [PATCH 07/15] container/lxc: Adds networkDHCPAllocateIPsStatically
 function

This function decides whether static IPs need allocating and records the result.

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/container_lxc.go | 34 ++++++++++++++++++++++++++++++++++
 1 file changed, 34 insertions(+)

diff --git a/lxd/container_lxc.go b/lxd/container_lxc.go
index 5463cc2fae..8e2823fcef 100644
--- a/lxd/container_lxc.go
+++ b/lxd/container_lxc.go
@@ -8854,6 +8854,40 @@ func (c *containerLXC) matchEbtablesRule(activeRule []string, matchRule []string
 	return true
 }
 
+// networkDHCPAllocateIPsStatically allocates static IP addresses for a container device from the
+// DHCP pool of the parent network. Allocation of IPs is controlled by security.ipv4_filtering and
+// security.ipv6_filtering device keys.
+func (c *containerLXC) networkDHCPAllocateIPsStatically(deviceName string, m types.Device) error {
+	// Get a list of used IPs for all containers in device's network.
+	usedIPv4s, usedIPv6s, err := networkDHCPAllocatedIPs(m["parent"])
+	if err != nil {
+		return err
+	}
+
+	network, err := networkLoadByName(c.state, m["parent"])
+	if err != nil {
+		return nil
+	}
+
+	if shared.IsTrue(m["security.ipv6_filtering"]) && m["ipv6.address"] == "" && m["hwaddr"] != "" {
+		IP, err := networkDHCPFindFreeIPv6(usedIPv6s, network.config, c.Name(), m["hwaddr"])
+		if err != nil {
+			return errors.Wrap(err, fmt.Sprintf("DHCP IPv6 static allocation failed for %s device %s", c.Name(), deviceName))
+		}
+		m["ipv6.address"] = IP.String() // Used by networkUpdateStatic() to generate dnsmasq config.
+	}
+
+	if shared.IsTrue(m["security.ipv4_filtering"]) && m["ipv4.address"] == "" && m["hwaddr"] != "" {
+		IP, err := networkDHCPFindFreeIPv4(usedIPv4s, network.config, c.Name(), m["hwaddr"])
+		if err != nil {
+			return errors.Wrap(err, fmt.Sprintf("DHCP IPv4 static allocation failed for %s device %s", c.Name(), deviceName))
+		}
+		m["ipv4.address"] = IP.String() // Used by networkUpdateStatic() to generate dnsmasq config.
+	}
+
+	return nil
+}
+
 func (c *containerLXC) insertNetworkDevice(name string, m types.Device) (types.Device, error) {
 	// Load the go-lxc struct
 	err := c.initLXC(false)

From 39146a213d1d37377ee301427f12ff9a2e04eb46 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Thu, 27 Jun 2019 16:39:59 +0100
Subject: [PATCH 08/15] networks/utils: Adds c.networkDHCPAllocateIPsStatically
 to networkUpdateStatic

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/networks_utils.go | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/lxd/networks_utils.go b/lxd/networks_utils.go
index 9461053840..02f113904d 100644
--- a/lxd/networks_utils.go
+++ b/lxd/networks_utils.go
@@ -1203,6 +1203,13 @@ func networkUpdateStatic(s *state.State, networkName string) error {
 				entries[d["parent"]] = [][]string{}
 			}
 
+			if shared.IsTrue(d["security.ipv4_filtering"]) || shared.IsTrue(d["security.ipv6_filtering"]) {
+				err := c.(*containerLXC).networkDHCPAllocateIPsStatically(k, d)
+				if err != nil {
+					return err
+				}
+			}
+
 			entries[d["parent"]] = append(entries[d["parent"]], []string{d["hwaddr"], c.Project(), c.Name(), d["ipv4.address"], d["ipv6.address"]})
 		}
 	}

From 544b6badc0219ba42fe757766d7364d6be9fd9ba Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Fri, 28 Jun 2019 11:41:42 +0100
Subject: [PATCH 09/15] container/lxc: Consistent comment endings in
 setupHostVethDevice()

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/container_lxc.go | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/lxd/container_lxc.go b/lxd/container_lxc.go
index 8e2823fcef..a6ab5875f4 100644
--- a/lxd/container_lxc.go
+++ b/lxd/container_lxc.go
@@ -3752,12 +3752,12 @@ func (c *containerLXC) setupHostVethDevice(deviceName string, device types.Devic
 		return fmt.Errorf("Failed to find host side veth name for device \"%s\"", deviceName)
 	}
 
-	// Remove any old network filters
+	// Remove any old network filters.
 	if oldDevice["nictype"] == "bridged" && shared.IsTrue(oldDevice["security.mac_filtering"]) || shared.IsTrue(oldDevice["security.ipv4_filtering"]) || shared.IsTrue(oldDevice["security.ipv6_filtering"]) {
 		c.removeNetworkFilters(deviceName, oldDevice)
 	}
 
-	// Setup network filters
+	// Setup network filters.
 	if device["nictype"] == "bridged" && shared.IsTrue(device["security.mac_filtering"]) || shared.IsTrue(device["security.ipv4_filtering"]) || shared.IsTrue(device["security.ipv6_filtering"]) {
 		err := c.setNetworkFilters(deviceName, device)
 		if err != nil {
@@ -3765,7 +3765,7 @@ func (c *containerLXC) setupHostVethDevice(deviceName string, device types.Devic
 		}
 	}
 
-	// Refresh tc limits
+	// Refresh tc limits.
 	err := c.setNetworkLimits(device)
 	if err != nil {
 		return err
@@ -3776,7 +3776,7 @@ func (c *containerLXC) setupHostVethDevice(deviceName string, device types.Devic
 		c.removeNetworkRoutes(deviceName, oldDevice)
 	}
 
-	// Setup static routes to container
+	// Setup static routes to container.
 	err = c.setNetworkRoutes(device)
 	if err != nil {
 		return err

From 5eb7397518ae770eabb3dd1acb6f167cf83b7d4c Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Fri, 28 Jun 2019 11:42:45 +0100
Subject: [PATCH 10/15] container/lxc: Updates
 generateNetworkFilterEbtablesRules to accept IP info as arguments

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/container_lxc.go | 18 +++++++++---------
 1 file changed, 9 insertions(+), 9 deletions(-)

diff --git a/lxd/container_lxc.go b/lxd/container_lxc.go
index a6ab5875f4..14a4306336 100644
--- a/lxd/container_lxc.go
+++ b/lxd/container_lxc.go
@@ -8643,7 +8643,7 @@ func (c *containerLXC) getVolatileHwaddr(deviceName string) string {
 }
 
 // generateNetworkFilterEbtablesRules returns a customised set of ebtables filter rules based on the device.
-func (c *containerLXC) generateNetworkFilterEbtablesRules(m types.Device) [][]string {
+func (c *containerLXC) generateNetworkFilterEbtablesRules(m types.Device, IPv4 net.IP, IPv6 net.IP) [][]string {
 	// MAC source filtering rules. Blocks any packet coming from container with an incorrect Ethernet source MAC.
 	// This is required for IP filtering too.
 	rules := [][]string{
@@ -8651,30 +8651,30 @@ func (c *containerLXC) generateNetworkFilterEbtablesRules(m types.Device) [][]st
 		{"ebtables", "-t", "filter", "-A", "FORWARD", "-s", "!", m["hwaddr"], "-i", m["host_name"], "-j", "DROP"},
 	}
 
-	if shared.IsTrue(m["security.ipv4_filtering"]) && m["ipv4.address"] != "" {
+	if shared.IsTrue(m["security.ipv4_filtering"]) && IPv4 != nil {
 		rules = append(rules,
 			// Prevent ARP MAC spoofing (prevents the container poisoning the ARP cache of its neighbours with a MAC address that isn't its own).
 			[]string{"ebtables", "-t", "filter", "-A", "INPUT", "-p", "ARP", "-i", m["host_name"], "--arp-mac-src", "!", m["hwaddr"], "-j", "DROP"},
 			[]string{"ebtables", "-t", "filter", "-A", "FORWARD", "-p", "ARP", "-i", m["host_name"], "--arp-mac-src", "!", m["hwaddr"], "-j", "DROP"},
 			// Prevent ARP IP spoofing (prevents the container redirecting traffic for IPs that are not its own).
-			[]string{"ebtables", "-t", "filter", "-A", "INPUT", "-p", "ARP", "-i", m["host_name"], "--arp-ip-src", "!", m["ipv4.address"], "-j", "DROP"},
-			[]string{"ebtables", "-t", "filter", "-A", "FORWARD", "-p", "ARP", "-i", m["host_name"], "--arp-ip-src", "!", m["ipv4.address"], "-j", "DROP"},
+			[]string{"ebtables", "-t", "filter", "-A", "INPUT", "-p", "ARP", "-i", m["host_name"], "--arp-ip-src", "!", IPv4.String(), "-j", "DROP"},
+			[]string{"ebtables", "-t", "filter", "-A", "FORWARD", "-p", "ARP", "-i", m["host_name"], "--arp-ip-src", "!", IPv4.String(), "-j", "DROP"},
 			// Allow DHCPv4 to the host only. This must come before the IP source filtering rules below.
 			[]string{"ebtables", "-t", "filter", "-A", "INPUT", "-p", "IPv4", "-s", m["hwaddr"], "-i", m["host_name"], "--ip-src", "0.0.0.0", "--ip-dst", "255.255.255.255", "--ip-proto", "udp", "--ip-dport", "67", "-j", "ACCEPT"},
 			// IP source filtering rules. Blocks any packet coming from container with an incorrect IP source address.
-			[]string{"ebtables", "-t", "filter", "-A", "INPUT", "-p", "IPv4", "-i", m["host_name"], "--ip-src", "!", m["ipv4.address"], "-j", "DROP"},
-			[]string{"ebtables", "-t", "filter", "-A", "FORWARD", "-p", "IPv4", "-i", m["host_name"], "--ip-src", "!", m["ipv4.address"], "-j", "DROP"},
+			[]string{"ebtables", "-t", "filter", "-A", "INPUT", "-p", "IPv4", "-i", m["host_name"], "--ip-src", "!", IPv4.String(), "-j", "DROP"},
+			[]string{"ebtables", "-t", "filter", "-A", "FORWARD", "-p", "IPv4", "-i", m["host_name"], "--ip-src", "!", IPv4.String(), "-j", "DROP"},
 		)
 	}
 
-	if shared.IsTrue(m["security.ipv6_filtering"]) && m["ipv6.address"] != "" {
+	if shared.IsTrue(m["security.ipv6_filtering"]) && IPv6 != nil {
 		rules = append(rules,
 			// Allow DHCPv6 and Router Solicitation to the host only. This must come before the IP source filtering rules below.
 			[]string{"ebtables", "-t", "filter", "-A", "INPUT", "-p", "IPv6", "-s", m["hwaddr"], "-i", m["host_name"], "--ip6-src", "fe80::/ffc0::", "--ip6-dst", "ff02::1:2/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", "--ip6-proto", "udp", "--ip6-dport", "547", "-j", "ACCEPT"},
 			[]string{"ebtables", "-t", "filter", "-A", "INPUT", "-p", "IPv6", "-s", m["hwaddr"], "-i", m["host_name"], "--ip6-src", "fe80::/ffc0::", "--ip6-dst", "ff02::2/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", "--ip6-proto", "ipv6-icmp", "--ip6-icmp-type", "router-solicitation", "-j", "ACCEPT"},
 			// IP source filtering rules. Blocks any packet coming from container with an incorrect IP source address.
-			[]string{"ebtables", "-t", "filter", "-A", "INPUT", "-p", "IPv6", "-i", m["host_name"], "--ip6-src", "!", fmt.Sprintf("%s/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", m["ipv6.address"]), "-j", "DROP"},
-			[]string{"ebtables", "-t", "filter", "-A", "FORWARD", "-p", "IPv6", "-i", m["host_name"], "--ip6-src", "!", fmt.Sprintf("%s/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", m["ipv6.address"]), "-j", "DROP"},
+			[]string{"ebtables", "-t", "filter", "-A", "INPUT", "-p", "IPv6", "-i", m["host_name"], "--ip6-src", "!", fmt.Sprintf("%s/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", IPv6.String()), "-j", "DROP"},
+			[]string{"ebtables", "-t", "filter", "-A", "FORWARD", "-p", "IPv6", "-i", m["host_name"], "--ip6-src", "!", fmt.Sprintf("%s/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", IPv6.String()), "-j", "DROP"},
 		)
 	}
 

From 1f95eee35299e27d84c7bb2160b3f3243bdcd284 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Fri, 28 Jun 2019 11:43:15 +0100
Subject: [PATCH 11/15] container/lxc: Updates
 generateNetworkFilterIptablesRules to accept IP info as arguments

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/container_lxc.go | 12 +++---------
 1 file changed, 3 insertions(+), 9 deletions(-)

diff --git a/lxd/container_lxc.go b/lxd/container_lxc.go
index 14a4306336..21b4723233 100644
--- a/lxd/container_lxc.go
+++ b/lxd/container_lxc.go
@@ -8682,7 +8682,7 @@ func (c *containerLXC) generateNetworkFilterEbtablesRules(m types.Device, IPv4 n
 }
 
 // generateNetworkFilterIptablesRules returns a customised set of iptables filter rules based on the device.
-func (c *containerLXC) generateNetworkFilterIptablesRules(m types.Device) (rules [][]string, err error) {
+func (c *containerLXC) generateNetworkFilterIptablesRules(m types.Device, IPv6 net.IP) (rules [][]string, err error) {
 	mac, err := net.ParseMAC(m["hwaddr"])
 	if err != nil {
 		return
@@ -8698,14 +8698,8 @@ func (c *containerLXC) generateNetworkFilterIptablesRules(m types.Device) (rules
 	// not assigned to the container by sending a specially crafted gratuitous NDP packet with
 	// correct source address and MAC at the IP & ethernet layers, but a fraudulent IP or MAC
 	// inside the ICMPv6 NDP packet.
-	if shared.IsTrue(m["security.ipv6_filtering"]) && m["ipv6.address"] != "" {
-		ipv6 := net.ParseIP(m["ipv6.address"])
-		if ipv6 == nil {
-			err = fmt.Errorf("Invalid IPv6 address")
-			return
-		}
-
-		ipv6Hex := hex.EncodeToString(ipv6)
+	if shared.IsTrue(m["security.ipv6_filtering"]) && IPv6 != nil {
+		ipv6Hex := hex.EncodeToString(IPv6)
 
 		rules = append(rules,
 			// Prevent Neighbor Advertisement IP spoofing (prevents the container redirecting traffic for IPs that are not its own).

From adfc682c4be08a7a7b611fa93d2f7336b3bfbc56 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Fri, 28 Jun 2019 11:44:00 +0100
Subject: [PATCH 12/15] container/lxc: Updates setNetworkFilters to read IP
 info from dnsmasq host config

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/container_lxc.go | 20 ++++++++++++++++++--
 1 file changed, 18 insertions(+), 2 deletions(-)

diff --git a/lxd/container_lxc.go b/lxd/container_lxc.go
index 21b4723233..3641e97ef4 100644
--- a/lxd/container_lxc.go
+++ b/lxd/container_lxc.go
@@ -8737,6 +8737,22 @@ func (c *containerLXC) setNetworkFilters(deviceName string, m types.Device) (err
 		}
 	}
 
+	// Read static IP configured from dnsmasq host config.
+	IPv4, IPv6, err := networkDHCPStaticContainerIPs(m["parent"], c.Name())
+	if err != nil {
+		return err
+	}
+	volatile := map[string]string{
+		fmt.Sprintf("volatile.%s.ipv4_address", deviceName): IPv4.IP.String(),
+		fmt.Sprintf("volatile.%s.ipv6_address", deviceName): IPv6.IP.String(),
+	}
+
+	// Store the IPs used for filter for removal later.
+	err = c.VolatileSet(volatile)
+	if err != nil {
+		return err
+	}
+
 	// If anything goes wrong, clean up so we don't leave orphaned rules.
 	defer func() {
 		if err != nil {
@@ -8744,7 +8760,7 @@ func (c *containerLXC) setNetworkFilters(deviceName string, m types.Device) (err
 		}
 	}()
 
-	rules := c.generateNetworkFilterEbtablesRules(m)
+	rules := c.generateNetworkFilterEbtablesRules(m, IPv4.IP, IPv6.IP)
 	for _, rule := range rules {
 		_, err = shared.RunCommand(rule[0], rule[1:]...)
 		if err != nil {
@@ -8752,7 +8768,7 @@ func (c *containerLXC) setNetworkFilters(deviceName string, m types.Device) (err
 		}
 	}
 
-	rules, err = c.generateNetworkFilterIptablesRules(m)
+	rules, err = c.generateNetworkFilterIptablesRules(m, IPv6.IP)
 	if err != nil {
 		return err
 	}

From 3447602c9d9deca307e151b87c047508a433fdf5 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Fri, 28 Jun 2019 11:44:23 +0100
Subject: [PATCH 13/15] container/lxc: Updates removeNetworkFilters to read IP
 info from volatile data

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/container_lxc.go | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/lxd/container_lxc.go b/lxd/container_lxc.go
index 3641e97ef4..eff53954f3 100644
--- a/lxd/container_lxc.go
+++ b/lxd/container_lxc.go
@@ -8801,6 +8801,9 @@ func (c *containerLXC) removeNetworkFilters(deviceName string, m types.Device) {
 		logger.Error("Failed to clear ip6tables ipv6_filter rules: ", log.Ctx{"container": c.Name(), "device": deviceName, "err": err})
 	}
 
+	IPv4 := net.ParseIP(c.localConfig[fmt.Sprintf("volatile.%s.ipv4_address", deviceName)])
+	IPv6 := net.ParseIP(c.localConfig[fmt.Sprintf("volatile.%s.ipv6_address", deviceName)])
+
 	// Get a current list of rules active on the host.
 	out, err := shared.RunCommand("ebtables", "-L", "--Lmac2", "--Lx")
 	if err != nil {
@@ -8809,7 +8812,7 @@ func (c *containerLXC) removeNetworkFilters(deviceName string, m types.Device) {
 	}
 
 	// Get a list of rules that we would have applied on container start.
-	rules := c.generateNetworkFilterEbtablesRules(m)
+	rules := c.generateNetworkFilterEbtablesRules(m, IPv4, IPv6)
 
 	// Iterate through each active rule on the host and try and match it to one the LXD rules.
 	for _, line := range strings.Split(out, "\n") {

From b88e64ed786d7cb8e59d87237e6e37f26d87b949 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Fri, 28 Jun 2019 13:32:40 +0100
Subject: [PATCH 14/15] container/lxc: Queues requests for
 setupHostVethDevice() during Update()

Queues requests for setupHostVethDevice() during Update() until after dnsmasq config has been updated (including any new IP allocations), so that IP filtering can be configured correctly.

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/container_lxc.go | 32 ++++++++++++++++++++++++--------
 1 file changed, 24 insertions(+), 8 deletions(-)

diff --git a/lxd/container_lxc.go b/lxd/container_lxc.go
index eff53954f3..60bea89a2f 100644
--- a/lxd/container_lxc.go
+++ b/lxd/container_lxc.go
@@ -5081,6 +5081,11 @@ func (c *containerLXC) Update(args db.ContainerArgs, userRequested bool) error {
 		}
 	}
 
+	// vethSetupQueue stores request to run veth setup functions after the dnsmasq config
+	// has been regenerated. This ensures that any dynamic IPs allocated for IP filtering are
+	// availble to the veth setup functions.
+	vethSetupQueue := make([]func() error, 0)
+
 	// Apply the live changes
 	if isRunning {
 		// Live update the container config
@@ -5534,10 +5539,10 @@ func (c *containerLXC) Update(args db.ContainerArgs, userRequested bool) error {
 						return err
 					}
 
-					err = c.setupHostVethDevice(k, m, types.Device{})
-					if err != nil {
-						return err
-					}
+					// Add a call to setupHostVethDevice to do later after dnsmasq update.
+					vethSetupQueue = append(vethSetupQueue, func() error {
+						return c.setupHostVethDevice(k, m, types.Device{})
+					})
 				}
 			} else if m["type"] == "usb" {
 				if usbs == nil {
@@ -5650,10 +5655,10 @@ func (c *containerLXC) Update(args db.ContainerArgs, userRequested bool) error {
 					return err
 				}
 
-				err = c.setupHostVethDevice(k, m, oldExpandedDevices[k])
-				if err != nil {
-					return err
-				}
+				// Add a call to setupHostVethDevice to do later after dnsmasq update.
+				vethSetupQueue = append(vethSetupQueue, func() error {
+					return c.setupHostVethDevice(k, m, oldExpandedDevices[k])
+				})
 			} else if m["type"] == "proxy" {
 				err = c.updateProxyDevice(k, m)
 				if err != nil {
@@ -5804,6 +5809,17 @@ func (c *containerLXC) Update(args db.ContainerArgs, userRequested bool) error {
 
 	if needsUpdate {
 		networkUpdateStatic(c.state, "")
+
+		// Once the dnsmasq config is updated (including allocating any new IPs), then if
+		// any of the devices that have changed earlier have queued a request to run
+		// veth host setup then run these now. This way we can be sure if IP filtering is
+		// being setup that it will have the correct IP to work with.
+		for _, f := range vethSetupQueue {
+			err := f()
+			if err != nil {
+				return nil
+			}
+		}
 	}
 
 	// Send devlxd notifications

From 62b0e644d35f5e9e7ec90430a706597cc7723e96 Mon Sep 17 00:00:00 2001
From: Thomas Parrott <thomas.parrott at canonical.com>
Date: Fri, 28 Jun 2019 13:44:29 +0100
Subject: [PATCH 15/15] container/lxc: Adds more prominent error about missing
 br_netfilter module on start

Signed-off-by: Thomas Parrott <thomas.parrott at canonical.com>
---
 lxd/container_lxc.go | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/lxd/container_lxc.go b/lxd/container_lxc.go
index 60bea89a2f..83c00bb1d2 100644
--- a/lxd/container_lxc.go
+++ b/lxd/container_lxc.go
@@ -2134,6 +2134,14 @@ func (c *containerLXC) startCommon() (string, error) {
 			if m["parent"] != "" && !shared.PathExists(fmt.Sprintf("/sys/class/net/%s", m["parent"])) {
 				return "", fmt.Errorf("Missing parent '%s' for nic '%s'", m["parent"], name)
 			}
+
+			if shared.IsTrue(m["security.ipv6_filtering"]) {
+				// Check br_netfilter is loaded and enabled for IPv6.
+				sysctlVal, err := networkSysctlGet("bridge/bridge-nf-call-ip6tables")
+				if err != nil || sysctlVal != "1\n" {
+					return "", errors.Wrapf(err, "security.ipv6_filtering requires br_netfilter and sysctl net.bridge.bridge-nf-call-ip6tables=1")
+				}
+			}
 		case "unix-char", "unix-block":
 			srcPath, exist := m["source"]
 			if !exist {


More information about the lxc-devel mailing list