[Patch][RFC] Flexible NAT rules

The current implementation of the NAT feature in LXD causes some
issues when it should collaborate with firewall/IPsec packages. This
patch adds therefore some flexibility within LXD on how firewall
rules are handled. This especially comes into play during restarts
of lxd(8). Please see Firewall changes during (snap) updates for more information.

The ipv[46].nat configuration options are advanced to accept the
following options:

  • true|insert: the firewall rule is insert at the beginning of the
    POSTROUTING table
  • false: no rule is added
  • append: the iptables rule is appended at the end of the POSTROUTING
    table

To keep the configuration options backward compatible the old value
“true” and “false” are kept.

Is there any chance to get this into mainline?

From f5f7f9cf70f4a82a29fcc0e18f69a57857dc3d3d Mon Sep 17 00:00:00 2001
From: oms-kauz <oms@kauz.net>
Date: Sat, 21 Jul 2018 07:13:56 +0000
Subject: [PATCH 1/1] Allow NAT rule to be appended to POSTROUTING table

The current implementation of the NAT feature in LXD causes some
issues when it should collaborate with firewall/IPsec packages. This
patch adds therefore some flexibility within LXD on how firewall
rules are handled. This especially comes into play during restarts
of lxd(8).

The ipv[46].nat configuration options are advanced to accept the
following options:
- true|insert: the firewall rule is insert at the beginning of the
  POSTROUTING table
- false: no rule is added
- append: the iptables rule is appended at the end of the POSTROUTING
  table

To keep the configuration options backward compatible the old value
"true" and "false" are kept.
---
 doc/networks.md          |  4 ++--
 lxd/networks.go          | 16 ++++++++++++----
 lxd/networks_config.go   |  8 ++++++--
 lxd/networks_iptables.go | 13 ++++++++++---
 4 files changed, 30 insertions(+), 11 deletions(-)

diff --git a/doc/networks.md b/doc/networks.md
index 2b29b7a0..76770837 100644
--- a/doc/networks.md
+++ b/doc/networks.md
@@ -36,7 +36,7 @@ ipv4.dhcp.expiry                | string    | ipv4 dhcp             | 1h
 ipv4.dhcp.gateway               | string    | ipv4 dhcp             | ipv4.address              | Address of the gateway for the subnet
 ipv4.dhcp.ranges                | string    | ipv4 dhcp             | all addresses             | Comma separated list of IP ranges to use for DHCP (FIRST-LAST format)
 ipv4.firewall                   | boolean   | ipv4 address          | true                      | Whether to generate filtering firewall rules for this network
-ipv4.nat                        | boolean   | ipv4 address          | false                     | Whether to NAT (will default to true if unset and a random ipv4.address is generated)
+ipv4.nat                        | string    | ipv4 address          | false                     | Whether and how to NAT ("true" / "insert" a NAT rule is inserted into the NAT table, "append" a NAT rule is appended into the NAT table, "false" no rule generated). It will default to "true"/"insert" if unset and a random ipv4.address is generated)
 ipv4.routes                     | string    | ipv4 address          | -                         | Comma separated list of additional IPv4 CIDR subnets to route to the bridge
 ipv4.routing                    | boolean   | ipv4 address          | true                      | Whether to route traffic in and out of the bridge
 ipv6.address                    | string    | standard mode         | random unused subnet      | IPv6 address for the bridge (CIDR notation). Use "none" to turn off IPv6 or "auto" to generate a new one
@@ -45,7 +45,7 @@ ipv6.dhcp.expiry                | string    | ipv6 dhcp             | 1h
 ipv6.dhcp.ranges                | string    | ipv6 stateful dhcp    | all addresses             | Comma separated list of IPv6 ranges to use for DHCP (FIRST-LAST format)
 ipv6.dhcp.stateful              | boolean   | ipv6 dhcp             | false                     | Whether to allocate addresses using DHCP
 ipv6.firewall                   | boolean   | ipv6 address          | true                      | Whether to generate filtering firewall rules for this network
-ipv6.nat                        | boolean   | ipv6 address          | false                     | Whether to NAT (will default to true if unset and a random ipv6.address is generated)
+ipv6.nat                        | string    | ipv6 address          | false                     | Whether and how to NAT ("true" / "insert" a NAT rule is inserted into the NAT table, "append" a NAT rule is appended into the NAT table, "false" no rule generated). It will default to "true"/"insert" if unset and a random ipv4.address is generated)
 ipv6.routes                     | string    | ipv6 address          | -                         | Comma separated list of additional IPv6 CIDR subnets to route to the bridge
 ipv6.routing                    | boolean   | ipv6 address          | true                      | Whether to route traffic in and out of the bridge
 raw.dnsmasq                     | string    | -                     | -                         | Additional dnsmasq configuration to append to the configuration
diff --git a/lxd/networks.go b/lxd/networks.go
index 7b863f0d..70c61510 100644
--- a/lxd/networks.go
+++ b/lxd/networks.go
@@ -1225,8 +1225,12 @@ func (n *network) Start() error {
        }
 
        // Configure NAT
-        if shared.IsTrue(n.config["ipv4.nat"]) {
-            err = networkIptablesPrepend("ipv4", n.name, "nat", "POSTROUTING", "-s", subnet.String(), "!", "-d", subnet.String(), "-j", "MASQUERADE")
+        if !shared.StringInSlice(n.config["ipv4.nat"], []string{"", "false"}) {
+            if shared.StringInSlice(n.config["ipv4.nat"], []string{"true", "insert"}) {
+                err = networkIptablesPrepend("ipv4", n.name, "nat", "POSTROUTING", "-s", subnet.String(), "!", "-d", subnet.String(), "-j", "MASQUERADE")
+            } else { // "append"
+                err = networkIptablesAppend("ipv4", n.name, "nat", "POSTROUTING", "-s", subnet.String(), "!", "-d", subnet.String(), "-j", "MASQUERADE")
+            }
            if err != nil {
                return err
            }
@@ -1387,8 +1391,12 @@ func (n *network) Start() error {
        }
 
        // Configure NAT
-        if shared.IsTrue(n.config["ipv6.nat"]) {
-            err = networkIptablesPrepend("ipv6", n.name, "nat", "POSTROUTING", "-s", subnet.String(), "!", "-d", subnet.String(), "-j", "MASQUERADE")
+        if !shared.StringInSlice(n.config["ipv6.nat"], []string{"", "false"}) {
+            if shared.StringInSlice(n.config["ipv6.nat"], []string{"true", "insert"}) {
+                err = networkIptablesPrepend("ipv6", n.name, "nat", "POSTROUTING", "-s", subnet.String(), "!", "-d", subnet.String(), "-j", "MASQUERADE")
+            } else { // "append"
+                err = networkIptablesAppend("ipv6", n.name, "nat", "POSTROUTING", "-s", subnet.String(), "!", "-d", subnet.String(), "-j", "MASQUERADE")
+            }
            if err != nil {
                return err
            }
diff --git a/lxd/networks_config.go b/lxd/networks_config.go
index 5c79a9d8..c6dfcf61 100644
--- a/lxd/networks_config.go
+++ b/lxd/networks_config.go
@@ -61,7 +61,9 @@ var networkConfigKeys = map[string]func(value string) error{
        return networkValidAddressCIDRV4(value)
    },
    "ipv4.firewall":     shared.IsBool,
-    "ipv4.nat":          shared.IsBool,
+    "ipv4.nat": func(value string) error {
+        return shared.IsOneOf(value, []string{"true", "append", "insert", "false"})
+    },
    "ipv4.dhcp":         shared.IsBool,
    "ipv4.dhcp.gateway": networkValidAddressV4,
    "ipv4.dhcp.expiry":  shared.IsAny,
@@ -77,7 +79,9 @@ var networkConfigKeys = map[string]func(value string) error{
        return networkValidAddressCIDRV6(value)
    },
    "ipv6.firewall":      shared.IsBool,
-    "ipv6.nat":           shared.IsBool,
+    "ipv6.nat": func(value string) error {
+        return shared.IsOneOf(value, []string{"true", "append", "insert", "false"})
+    },
    "ipv6.dhcp":          shared.IsBool,
    "ipv6.dhcp.expiry":   shared.IsAny,
    "ipv6.dhcp.stateful": shared.IsBool,
diff --git a/lxd/networks_iptables.go b/lxd/networks_iptables.go
index 1c0c2bc8..1dbaf65d 100644
--- a/lxd/networks_iptables.go
+++ b/lxd/networks_iptables.go
@@ -8,7 +8,7 @@ import (
    "github.com/lxc/lxd/shared"
 )
 
-func networkIptablesPrepend(protocol string, netName string, table string, chain string, rule ...string) error {
+func networkIptablesConfig(protocol string, netName string, table string, method string, chain string, rule ...string) error {
    cmd := "iptables"
    if protocol == "ipv6" {
        cmd = "ip6tables"
@@ -34,8 +34,7 @@ func networkIptablesPrepend(protocol string, netName string, table string, chain
        return nil
    }
 
-    // Add the rule
-    args = append(baseArgs, []string{"-I", chain}...)
+    args = append(baseArgs, []string{method, chain}...)
    args = append(args, rule...)
    args = append(args, "-m", "comment", "--comment", fmt.Sprintf("generated for LXD network %s", netName))
 
@@ -47,6 +46,14 @@ func networkIptablesPrepend(protocol string, netName string, table string, chain
    return nil
 }
 
+func networkIptablesAppend(protocol string, netName string, table string, chain string, rule ...string) error {
+    return networkIptablesConfig(protocol, netName, table, "-A", chain, rule...)
+}
+
+func networkIptablesPrepend(protocol string, netName string, table string, chain string, rule ...string) error {
+    return networkIptablesConfig(protocol, netName, table, "-I", chain, rule...)
+}
+
 func networkIptablesClear(protocol string, netName string, table string) error {
    // Detect kernels that lack IPv6 support
    if !shared.PathExists("/proc/sys/net/ipv6") && protocol == "ipv6" {
-- 
2.17.1

Hi!

A developer will give you input about this.

I’m adding below some input about the process of submitting a patch to LXD.
LXD is developed on github, and code submissions are done by Pull Requests (PR).
Specifically,

  1. Visit https://github.com/lxc/lxd/ and click on Fork to create a clone of LXD into your own Github account.
  2. Follow the guide at https://gist.github.com/Chaser324/ce0505fbed06b947d962 to create a separate branch in your clone of LXD, then commit your changes into that branch and push them to your clone of LXD at your github account.
  3. When you visit your clone of LXD at your github account, you will see that there is a green frame that asks you to click there in order to create a Pull Request of your change. This Pull Request will then appear automatically on https://github.com/lxc/lxd/pulls and will be taken care off.
  4. You might be asked to make changes/adjustments to your patch. You add more commits to your local clone, push the commits and they will automatically appear on https://github.com/lxc/lxd/pulls
1 Like

Thanks, I’ve created a pull request.