From dfca06b0584116ac88bcb1585e8750ecfeeb4dd4 Mon Sep 17 00:00:00 2001
From: Joe Groocock <me@frebib.net>
Date: Sun, 20 Aug 2023 14:40:38 +0100
Subject: nat64: T160: Implement Jool-based NAT64 translator

Signed-off-by: Joe Groocock <me@frebib.net>
(cherry picked from commit 7d49f7079f1129c2fadc7f38ceb230804d89e177)

# Conflicts:
#	debian/control
---
 debian/control                                     |  67 ++++++
 interface-definitions/include/nat64-protocol.xml.i |  27 +++
 interface-definitions/nat64.xml.in                 |  97 +++++++++
 python/vyos/utils/file.py                          |  14 ++
 src/conf_mode/nat64.py                             | 226 +++++++++++++++++++++
 5 files changed, 431 insertions(+)
 create mode 100644 interface-definitions/include/nat64-protocol.xml.i
 create mode 100644 interface-definitions/nat64.xml.in
 create mode 100755 src/conf_mode/nat64.py

diff --git a/debian/control b/debian/control
index ae54d6ed6..bd748dd6e 100644
--- a/debian/control
+++ b/debian/control
@@ -163,6 +163,73 @@ Depends:
   sstp-client,
   strongswan (>= 5.9),
   strongswan-swanctl (>= 5.9),
+<<<<<<< HEAD
+=======
+  charon-systemd,
+  libcharon-extra-plugins (>=5.9),
+  libcharon-extauth-plugins (>=5.9),
+  libstrongswan-extra-plugins (>=5.9),
+  libstrongswan-standard-plugins (>=5.9),
+  python3-vici (>= 5.7.2),
+# End "vpn ipsec"
+# For "nat64"
+  jool,
+# End "nat64"
+# For nat66
+  ndppd,
+# End nat66
+# For "system ntp"
+  chrony,
+# End "system ntp"
+# For "vpn openconnect"
+  ocserv,
+# End "vpn openconnect"
+# For "set system flow-accounting"
+  pmacct (>= 1.6.0),
+# End "set system flow-accounting"
+# For container
+  podman,
+  netavark,
+  aardvark-dns,
+# iptables is only used for containers now, not the the firewall CLI
+  iptables,
+# End container
+## End Configuration mode
+## Operational mode
+# Used for hypervisor model in "run show version"
+  hvinfo,
+# For "run traceroute"
+  traceroute,
+# For "run monitor traffic"
+  tcpdump,
+# End "run monitor traffic"
+# For "run show hardware storage smart"
+  smartmontools,
+# For "run show hardware scsi"
+  lsscsi,
+# For "run show hardware pci"
+  pciutils,
+# For "show hardware usb"
+  usbutils,
+# For "run show hardware storage nvme"
+  nvme-cli,
+# For "run monitor bandwidth-test"
+  iperf,
+  iperf3,
+# End "run monitor bandwidth-test"
+# For "run wake-on-lan"
+  etherwake,
+# For "run force ipv6-nd"
+  ndisc6,
+# For "run monitor bandwidth"
+  bmon,
+# End Operational mode
+## Optional utilities
+  easy-rsa,
+  tcptraceroute,
+  mtr-tiny,
+  telnet,
+>>>>>>> 7d49f7079 (nat64: T160: Implement Jool-based NAT64 translator)
   stunnel4,
   sudo,
   systemd,
diff --git a/interface-definitions/include/nat64-protocol.xml.i b/interface-definitions/include/nat64-protocol.xml.i
new file mode 100644
index 000000000..9432e6a87
--- /dev/null
+++ b/interface-definitions/include/nat64-protocol.xml.i
@@ -0,0 +1,27 @@
+<!-- include start from nat64-protocol.xml.i -->
+<node name="protocol">
+  <properties>
+    <help>Apply translation address to a specfic protocol</help>
+  </properties>
+  <children>
+    <leafNode name="tcp">
+      <properties>
+        <help>Transmission Control Protocol</help>
+        <valueless/>
+      </properties>
+    </leafNode>
+    <leafNode name="udp">
+      <properties>
+        <help>User Datagram Protocol</help>
+        <valueless/>
+      </properties>
+    </leafNode>
+    <leafNode name="icmp">
+      <properties>
+        <help>Internet Control Message Protocol</help>
+        <valueless/>
+      </properties>
+    </leafNode>
+  </children>
+</node>
+<!-- include end -->
diff --git a/interface-definitions/nat64.xml.in b/interface-definitions/nat64.xml.in
new file mode 100644
index 000000000..1522395e4
--- /dev/null
+++ b/interface-definitions/nat64.xml.in
@@ -0,0 +1,97 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+  <node name="nat64" owner="${vyos_conf_scripts_dir}/nat64.py">
+    <properties>
+      <help>IPv6-to-IPv4 Network Address Translation (NAT64) Settings</help>
+      <priority>501</priority>
+    </properties>
+    <children>
+      <node name="source">
+        <properties>
+          <help>IPv6 source to IPv4 destination address translation</help>
+        </properties>
+        <children>
+          <tagNode name="rule">
+            <properties>
+              <help>Source NAT64 rule number</help>
+              <valueHelp>
+                <format>u32:1-999999</format>
+                <description>Number for this rule</description>
+              </valueHelp>
+              <constraint>
+                <validator name="numeric" argument="--range 1-999999"/>
+              </constraint>
+              <constraintErrorMessage>NAT64 rule number must be between 1 and 999999</constraintErrorMessage>
+            </properties>
+            <children>
+              #include <include/generic-description.xml.i>
+              #include <include/generic-disable-node.xml.i>
+              <node name="source">
+                <properties>
+                  <help>IPv6 source prefix options</help>
+                </properties>
+                <children>
+                  <leafNode name="prefix">
+                    <properties>
+                      <help>IPv6 prefix to be translated</help>
+                      <valueHelp>
+                        <format>ipv6net</format>
+                        <description>IPv6 prefix</description>
+                      </valueHelp>
+                      <constraint>
+                        <validator name="ipv6-prefix"/>
+                      </constraint>
+                    </properties>
+                  </leafNode>
+                </children>
+              </node>
+              <node name="translation">
+                <properties>
+                  <help>Translated IPv4 address options</help>
+                </properties>
+                <children>
+                  <tagNode name="pool">
+                    <properties>
+                      <help>Translation IPv4 pool number</help>
+                      <valueHelp>
+                        <format>u32:1-999999</format>
+                        <description>Number for this rule</description>
+                      </valueHelp>
+                      <constraint>
+                        <validator name="numeric" argument="--range 1-999999"/>
+                      </constraint>
+                      <constraintErrorMessage>NAT64 pool number must be between 1 and 999999</constraintErrorMessage>
+                    </properties>
+                    <children>
+                      #include <include/generic-description.xml.i>
+                      #include <include/generic-disable-node.xml.i>
+                      #include <include/nat-translation-port.xml.i>
+                      #include <include/nat64-protocol.xml.i>
+                      <leafNode name="address">
+                        <properties>
+                          <help>IPv4 address or prefix to translate to</help>
+                          <valueHelp>
+                            <format>ipv4</format>
+                            <description>IPv4 address</description>
+                          </valueHelp>
+                          <valueHelp>
+                            <format>ipv4net</format>
+                            <description>IPv4 prefix</description>
+                          </valueHelp>
+                          <constraint>
+                            <validator name="ipv4-address"/>
+                            <validator name="ipv4-prefix"/>
+                          </constraint>
+                        </properties>
+                      </leafNode>
+                    </children>
+                  </tagNode>
+                </children>
+              </node>
+            </children>
+          </tagNode>
+        </children>
+      </node>
+    </children>
+  </node>
+</interfaceDefinition>
diff --git a/python/vyos/utils/file.py b/python/vyos/utils/file.py
index 667a2464b..e20899fe7 100644
--- a/python/vyos/utils/file.py
+++ b/python/vyos/utils/file.py
@@ -83,6 +83,20 @@ def read_json(fname, defaultonfailure=None):
             return defaultonfailure
         raise e
 
+def write_json(fname, data, indent=2, defaultonfailure=None):
+    """
+    encode data to json and write to a file
+    should defaultonfailure be not None, it is returned on failure to write
+    """
+    import json
+    try:
+        with open(fname, 'w') as f:
+            json.dump(data, f, indent=indent)
+    except Exception as e:
+        if defaultonfailure is not None:
+            return defaultonfailure
+        raise e
+
 def chown(path, user, group):
     """ change file/directory owner """
     from pwd import getpwnam
diff --git a/src/conf_mode/nat64.py b/src/conf_mode/nat64.py
new file mode 100755
index 000000000..d4df479ac
--- /dev/null
+++ b/src/conf_mode/nat64.py
@@ -0,0 +1,226 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# pylint: disable=empty-docstring,missing-module-docstring
+
+import csv
+import os
+import re
+
+from ipaddress import IPv6Network
+
+from vyos import ConfigError
+from vyos import airbag
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.configdict import is_node_changed
+from vyos.utils.dict import dict_search
+from vyos.utils.file import write_json
+from vyos.utils.kernel import check_kmod
+from vyos.utils.process import cmd
+from vyos.utils.process import run
+
+airbag.enable()
+
+INSTANCE_REGEX = re.compile(r"instance-(\d+)")
+JOOL_CONFIG_DIR = "/run/jool"
+
+
+def get_config(config: Config | None = None) -> None:
+    """ """
+    if config is None:
+        config = Config()
+
+    base = ["nat64"]
+    nat64 = config.get_config_dict(base, key_mangling=("-", "_"), get_first_key=True)
+
+    # T2665: we must add the tagNode defaults individually until this is
+    # moved to the base class
+    for direction in ["source"]:
+        if direction in nat64:
+            default_values = defaults(base + [direction, "rule"])
+            if "rule" in nat64[direction]:
+                for rule in nat64[direction]["rule"]:
+                    nat64[direction]["rule"][rule] = dict_merge(
+                        default_values, nat64[direction]["rule"][rule]
+                    )
+
+                    # Only support netfilter for now
+                    nat64[direction]["rule"][rule]["mode"] = "netfilter"
+
+    base_src = base + ["source", "rule"]
+
+    # Load in existing instances so we can destroy any unknown
+    lines = cmd("jool instance display --csv").splitlines()
+    for _, instance, _ in csv.reader(lines):
+        match = INSTANCE_REGEX.fullmatch(instance)
+        if not match:
+            # FIXME: Instances that don't match should be ignored but WARN'ed to the user
+            continue
+        num = match.group(1)
+
+        rules = nat64.setdefault("source", {}).setdefault("rule", {})
+        # Mark it for deletion
+        if num not in rules:
+            rules[num] = {"deleted": True}
+            continue
+
+        # If the user changes the mode, recreate the instance else Jool fails with:
+        # Jool error: Sorry; you can't change an instance's framework for now.
+        if is_node_changed(config, base_src + [f"instance-{num}", "mode"]):
+            rules[num]["recreate"] = True
+
+        # If the user changes the pool6, recreate the instance else Jool fails with:
+        # Jool error: Sorry; you can't change a NAT64 instance's pool6 for now.
+        if dict_search("source.prefix", rules[num]) and is_node_changed(
+            config,
+            base_src + [num, "source", "prefix"],
+        ):
+            rules[num]["recreate"] = True
+
+    return nat64
+
+
+def verify(nat64) -> None:
+    """ """
+    if not nat64:
+        # no need to verify the CLI as nat64 is going to be deactivated
+        return
+
+    if dict_search("source.rule", nat64):
+        # Ensure only 1 netfilter instance per namespace
+        nf_rules = filter(
+            lambda i: "deleted" not in i and i["mode"] == "netfilter",
+            nat64["source"]["rule"].values(),
+        )
+        next(nf_rules, None)  # Discard the first element
+        if next(nf_rules, None) is not None:
+            raise ConfigError(
+                "Jool permits only 1 NAT64 netfilter instance (per network namespace)"
+            )
+
+        for rule, instance in nat64["source"]["rule"].items():
+            if "deleted" in instance:
+                continue
+
+            # Verify that source.prefix is set and is a /96
+            if not dict_search("source.prefix", instance):
+                raise ConfigError(f"Source NAT64 rule {rule} missing source prefix")
+            if IPv6Network(instance["source"]["prefix"]).prefixlen != 96:
+                raise ConfigError(f"Source NAT64 rule {rule} source prefix must be /96")
+
+            pools = dict_search("translation.pool", instance)
+            if pools:
+                for num, pool in pools.items():
+                    if "address" not in pool:
+                        raise ConfigError(
+                            f"Source NAT64 rule {rule} translation pool "
+                            f"{num} missing address/prefix"
+                        )
+                    if "port" not in pool:
+                        raise ConfigError(
+                            f"Source NAT64 rule {rule} translation pool "
+                            f"{num} missing port(-range)"
+                        )
+
+
+def generate(nat64) -> None:
+    """ """
+    os.makedirs(JOOL_CONFIG_DIR, exist_ok=True)
+
+    if dict_search("source.rule", nat64):
+        for rule, instance in nat64["source"]["rule"].items():
+            if "deleted" in instance:
+                # Delete the unused instance file
+                os.unlink(os.path.join(JOOL_CONFIG_DIR, f"instance-{rule}.json"))
+                continue
+
+            name = f"instance-{rule}"
+            config = {
+                "instance": name,
+                "framework": "netfilter",
+                "global": {
+                    "pool6": instance["source"]["prefix"],
+                    "manually-enabled": "disable" not in instance,
+                },
+                # "bib": [],
+            }
+
+            if "description" in instance:
+                config["comment"] = instance["description"]
+
+            if dict_search("translation.pool", instance):
+                pool4 = []
+                for pool in instance["translation"]["pool"].values():
+                    if "disable" in pool:
+                        continue
+
+                    protos = pool.get("protocol", {}).keys() or ("tcp", "udp", "icmp")
+                    for proto in protos:
+                        obj = {
+                            "protocol": proto.upper(),
+                            "prefix": pool["address"],
+                            "port range": pool["port"],
+                        }
+                        if "description" in pool:
+                            obj["comment"] = pool["description"]
+
+                        pool4.append(obj)
+
+                if pool4:
+                    config["pool4"] = pool4
+
+            write_json(f"{JOOL_CONFIG_DIR}/{name}.json", config)
+
+
+def apply(nat64) -> None:
+    """ """
+    if not nat64:
+        return
+
+    if dict_search("source.rule", nat64):
+        # Deletions first to avoid conflicts
+        for rule, instance in nat64["source"]["rule"].items():
+            if not any(k in instance for k in ("deleted", "recreate")):
+                continue
+
+            ret = run(f"jool instance remove instance-{rule}")
+            if ret != 0:
+                raise ConfigError(
+                    f"Failed to remove nat64 source rule {rule} (jool instance instance-{rule})"
+                )
+
+        # Now creations
+        for rule, instance in nat64["source"]["rule"].items():
+            if "deleted" in instance:
+                continue
+
+            name = f"instance-{rule}"
+            ret = run(f"jool -i {name} file handle {JOOL_CONFIG_DIR}/{name}.json")
+            if ret != 0:
+                raise ConfigError(f"Failed to set jool instance {name}")
+
+
+if __name__ == "__main__":
+    try:
+        check_kmod(["jool"])
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
-- 
cgit v1.2.3


From 3aad7e75112d6e065d72d79dbdf61902cf19b63f Mon Sep 17 00:00:00 2001
From: Viacheslav Hletenko <v.gletenko@vyos.io>
Date: Wed, 6 Dec 2023 07:10:46 +0000
Subject: T160: Rebase and fixes for NAT64

 - Update the base (rebase)
 - Move include/nat64-protocol.xml.i => include/nat64/protocol.xml.i
 - Delete unwanted `write_json`, use `write_file` instead
 - Remove unnecessary deleting of default values for tagNodes T2665
 - Add smoketest

Example:
```
set interfaces ethernet eth0 address '192.168.122.14/24'
set interfaces ethernet eth0 address '192.168.122.10/24'
set interfaces ethernet eth2 address '2001:db8::1/64'

set nat64 source rule 100 source prefix '64:ff9b::/96'
set nat64 source rule 100 translation pool 10 address '192.168.122.10'
set nat64 source rule 100 translation pool 10 port '1-65535'
```

(cherry picked from commit 336bb5a071b59264679be4f4f9bedbdecdbe2834)
---
 interface-definitions/include/nat64-protocol.xml.i |  27 ------
 interface-definitions/include/nat64/protocol.xml.i |  27 ++++++
 interface-definitions/nat64.xml.in                 |   2 +-
 python/vyos/utils/file.py                          |  14 ---
 smoketest/scripts/cli/test_nat64.py                | 102 +++++++++++++++++++++
 src/conf_mode/nat64.py                             |  25 +----
 6 files changed, 134 insertions(+), 63 deletions(-)
 delete mode 100644 interface-definitions/include/nat64-protocol.xml.i
 create mode 100644 interface-definitions/include/nat64/protocol.xml.i
 create mode 100755 smoketest/scripts/cli/test_nat64.py

diff --git a/interface-definitions/include/nat64-protocol.xml.i b/interface-definitions/include/nat64-protocol.xml.i
deleted file mode 100644
index 9432e6a87..000000000
--- a/interface-definitions/include/nat64-protocol.xml.i
+++ /dev/null
@@ -1,27 +0,0 @@
-<!-- include start from nat64-protocol.xml.i -->
-<node name="protocol">
-  <properties>
-    <help>Apply translation address to a specfic protocol</help>
-  </properties>
-  <children>
-    <leafNode name="tcp">
-      <properties>
-        <help>Transmission Control Protocol</help>
-        <valueless/>
-      </properties>
-    </leafNode>
-    <leafNode name="udp">
-      <properties>
-        <help>User Datagram Protocol</help>
-        <valueless/>
-      </properties>
-    </leafNode>
-    <leafNode name="icmp">
-      <properties>
-        <help>Internet Control Message Protocol</help>
-        <valueless/>
-      </properties>
-    </leafNode>
-  </children>
-</node>
-<!-- include end -->
diff --git a/interface-definitions/include/nat64/protocol.xml.i b/interface-definitions/include/nat64/protocol.xml.i
new file mode 100644
index 000000000..a640873b5
--- /dev/null
+++ b/interface-definitions/include/nat64/protocol.xml.i
@@ -0,0 +1,27 @@
+<!-- include start from nat64/protocol.xml.i -->
+<node name="protocol">
+  <properties>
+    <help>Apply translation address to a specfic protocol</help>
+  </properties>
+  <children>
+    <leafNode name="tcp">
+      <properties>
+        <help>Transmission Control Protocol</help>
+        <valueless/>
+      </properties>
+    </leafNode>
+    <leafNode name="udp">
+      <properties>
+        <help>User Datagram Protocol</help>
+        <valueless/>
+      </properties>
+    </leafNode>
+    <leafNode name="icmp">
+      <properties>
+        <help>Internet Control Message Protocol</help>
+        <valueless/>
+      </properties>
+    </leafNode>
+  </children>
+</node>
+<!-- include end -->
diff --git a/interface-definitions/nat64.xml.in b/interface-definitions/nat64.xml.in
index 1522395e4..baf13e6cb 100644
--- a/interface-definitions/nat64.xml.in
+++ b/interface-definitions/nat64.xml.in
@@ -66,7 +66,7 @@
                       #include <include/generic-description.xml.i>
                       #include <include/generic-disable-node.xml.i>
                       #include <include/nat-translation-port.xml.i>
-                      #include <include/nat64-protocol.xml.i>
+                      #include <include/nat64/protocol.xml.i>
                       <leafNode name="address">
                         <properties>
                           <help>IPv4 address or prefix to translate to</help>
diff --git a/python/vyos/utils/file.py b/python/vyos/utils/file.py
index e20899fe7..667a2464b 100644
--- a/python/vyos/utils/file.py
+++ b/python/vyos/utils/file.py
@@ -83,20 +83,6 @@ def read_json(fname, defaultonfailure=None):
             return defaultonfailure
         raise e
 
-def write_json(fname, data, indent=2, defaultonfailure=None):
-    """
-    encode data to json and write to a file
-    should defaultonfailure be not None, it is returned on failure to write
-    """
-    import json
-    try:
-        with open(fname, 'w') as f:
-            json.dump(data, f, indent=indent)
-    except Exception as e:
-        if defaultonfailure is not None:
-            return defaultonfailure
-        raise e
-
 def chown(path, user, group):
     """ change file/directory owner """
     from pwd import getpwnam
diff --git a/smoketest/scripts/cli/test_nat64.py b/smoketest/scripts/cli/test_nat64.py
new file mode 100755
index 000000000..b5723ac7e
--- /dev/null
+++ b/smoketest/scripts/cli/test_nat64.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import json
+import os
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+from vyos.configsession import ConfigSessionError
+from vyos.utils.process import cmd
+from vyos.utils.dict import dict_search
+
+base_path = ['nat64']
+src_path = base_path + ['source']
+
+jool_nat64_config = '/run/jool/instance-100.json'
+
+
+class TestNAT64(VyOSUnitTestSHIM.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        super(TestNAT64, cls).setUpClass()
+
+        # ensure we can also run this test on a live system - so lets clean
+        # out the current configuration :)
+        cls.cli_delete(cls, base_path)
+
+    def tearDown(self):
+        self.cli_delete(base_path)
+        self.cli_commit()
+        self.assertFalse(os.path.exists(jool_nat64_config))
+
+    def test_snat64(self):
+        rule = '100'
+        translation_rule = '10'
+        prefix_v6 = '64:ff9b::/96'
+        pool = '192.0.2.10'
+        pool_port = '1-65535'
+
+        self.cli_set(src_path + ['rule', rule, 'source', 'prefix', prefix_v6])
+        self.cli_set(
+            src_path
+            + ['rule', rule, 'translation', 'pool', translation_rule, 'address', pool]
+        )
+        self.cli_set(
+            src_path
+            + ['rule', rule, 'translation', 'pool', translation_rule, 'port', pool_port]
+        )
+        self.cli_commit()
+
+        # Load the JSON file
+        with open(f'/run/jool/instance-{rule}.json', 'r') as json_file:
+            config_data = json.load(json_file)
+
+        # Assertions based on the content of the JSON file
+        self.assertEqual(config_data['instance'], f'instance-{rule}')
+        self.assertEqual(config_data['framework'], 'netfilter')
+        self.assertEqual(config_data['global']['pool6'], prefix_v6)
+        self.assertTrue(config_data['global']['manually-enabled'])
+
+        # Check the pool4 entries
+        pool4_entries = config_data.get('pool4', [])
+        self.assertIsInstance(pool4_entries, list)
+        self.assertGreater(len(pool4_entries), 0)
+
+        for entry in pool4_entries:
+            self.assertIn('protocol', entry)
+            self.assertIn('prefix', entry)
+            self.assertIn('port range', entry)
+
+            protocol = entry['protocol']
+            prefix = entry['prefix']
+            port_range = entry['port range']
+
+            if protocol == 'ICMP':
+                self.assertEqual(prefix, pool)
+                self.assertEqual(port_range, pool_port)
+            elif protocol == 'UDP':
+                self.assertEqual(prefix, pool)
+                self.assertEqual(port_range, pool_port)
+            elif protocol == 'TCP':
+                self.assertEqual(prefix, pool)
+                self.assertEqual(port_range, pool_port)
+            else:
+                self.fail(f'Unexpected protocol: {protocol}')
+
+
+if __name__ == '__main__':
+    unittest.main(verbosity=2)
diff --git a/src/conf_mode/nat64.py b/src/conf_mode/nat64.py
index d4df479ac..a8b90fb11 100755
--- a/src/conf_mode/nat64.py
+++ b/src/conf_mode/nat64.py
@@ -21,6 +21,7 @@ import os
 import re
 
 from ipaddress import IPv6Network
+from json import dumps as json_write
 
 from vyos import ConfigError
 from vyos import airbag
@@ -28,7 +29,7 @@ from vyos.config import Config
 from vyos.configdict import dict_merge
 from vyos.configdict import is_node_changed
 from vyos.utils.dict import dict_search
-from vyos.utils.file import write_json
+from vyos.utils.file import write_file
 from vyos.utils.kernel import check_kmod
 from vyos.utils.process import cmd
 from vyos.utils.process import run
@@ -40,27 +41,12 @@ JOOL_CONFIG_DIR = "/run/jool"
 
 
 def get_config(config: Config | None = None) -> None:
-    """ """
     if config is None:
         config = Config()
 
     base = ["nat64"]
     nat64 = config.get_config_dict(base, key_mangling=("-", "_"), get_first_key=True)
 
-    # T2665: we must add the tagNode defaults individually until this is
-    # moved to the base class
-    for direction in ["source"]:
-        if direction in nat64:
-            default_values = defaults(base + [direction, "rule"])
-            if "rule" in nat64[direction]:
-                for rule in nat64[direction]["rule"]:
-                    nat64[direction]["rule"][rule] = dict_merge(
-                        default_values, nat64[direction]["rule"][rule]
-                    )
-
-                    # Only support netfilter for now
-                    nat64[direction]["rule"][rule]["mode"] = "netfilter"
-
     base_src = base + ["source", "rule"]
 
     # Load in existing instances so we can destroy any unknown
@@ -95,7 +81,6 @@ def get_config(config: Config | None = None) -> None:
 
 
 def verify(nat64) -> None:
-    """ """
     if not nat64:
         # no need to verify the CLI as nat64 is going to be deactivated
         return
@@ -103,7 +88,7 @@ def verify(nat64) -> None:
     if dict_search("source.rule", nat64):
         # Ensure only 1 netfilter instance per namespace
         nf_rules = filter(
-            lambda i: "deleted" not in i and i["mode"] == "netfilter",
+            lambda i: "deleted" not in i and i.get('mode') == "netfilter",
             nat64["source"]["rule"].values(),
         )
         next(nf_rules, None)  # Discard the first element
@@ -138,7 +123,6 @@ def verify(nat64) -> None:
 
 
 def generate(nat64) -> None:
-    """ """
     os.makedirs(JOOL_CONFIG_DIR, exist_ok=True)
 
     if dict_search("source.rule", nat64):
@@ -183,11 +167,10 @@ def generate(nat64) -> None:
                 if pool4:
                     config["pool4"] = pool4
 
-            write_json(f"{JOOL_CONFIG_DIR}/{name}.json", config)
+            write_file(f'{JOOL_CONFIG_DIR}/{name}.json', json_write(config, indent=2))
 
 
 def apply(nat64) -> None:
-    """ """
     if not nat64:
         return
 
-- 
cgit v1.2.3


From 2076549112b5b65316123c54a68afa6bb3bf8611 Mon Sep 17 00:00:00 2001
From: Viacheslav Hletenko <v.gletenko@vyos.io>
Date: Thu, 7 Dec 2023 16:39:01 +0200
Subject: T160: Fix Debian control conflicts

---
 debian/control | 68 +---------------------------------------------------------
 1 file changed, 1 insertion(+), 67 deletions(-)

diff --git a/debian/control b/debian/control
index bd748dd6e..362f11009 100644
--- a/debian/control
+++ b/debian/control
@@ -79,6 +79,7 @@ Depends:
   isc-dhcp-relay,
   isc-dhcp-server,
   iw,
+  jool,
   keepalived (>=2.0.5),
   lcdproc,
   lcdproc-extra-drivers,
@@ -163,73 +164,6 @@ Depends:
   sstp-client,
   strongswan (>= 5.9),
   strongswan-swanctl (>= 5.9),
-<<<<<<< HEAD
-=======
-  charon-systemd,
-  libcharon-extra-plugins (>=5.9),
-  libcharon-extauth-plugins (>=5.9),
-  libstrongswan-extra-plugins (>=5.9),
-  libstrongswan-standard-plugins (>=5.9),
-  python3-vici (>= 5.7.2),
-# End "vpn ipsec"
-# For "nat64"
-  jool,
-# End "nat64"
-# For nat66
-  ndppd,
-# End nat66
-# For "system ntp"
-  chrony,
-# End "system ntp"
-# For "vpn openconnect"
-  ocserv,
-# End "vpn openconnect"
-# For "set system flow-accounting"
-  pmacct (>= 1.6.0),
-# End "set system flow-accounting"
-# For container
-  podman,
-  netavark,
-  aardvark-dns,
-# iptables is only used for containers now, not the the firewall CLI
-  iptables,
-# End container
-## End Configuration mode
-## Operational mode
-# Used for hypervisor model in "run show version"
-  hvinfo,
-# For "run traceroute"
-  traceroute,
-# For "run monitor traffic"
-  tcpdump,
-# End "run monitor traffic"
-# For "run show hardware storage smart"
-  smartmontools,
-# For "run show hardware scsi"
-  lsscsi,
-# For "run show hardware pci"
-  pciutils,
-# For "show hardware usb"
-  usbutils,
-# For "run show hardware storage nvme"
-  nvme-cli,
-# For "run monitor bandwidth-test"
-  iperf,
-  iperf3,
-# End "run monitor bandwidth-test"
-# For "run wake-on-lan"
-  etherwake,
-# For "run force ipv6-nd"
-  ndisc6,
-# For "run monitor bandwidth"
-  bmon,
-# End Operational mode
-## Optional utilities
-  easy-rsa,
-  tcptraceroute,
-  mtr-tiny,
-  telnet,
->>>>>>> 7d49f7079 (nat64: T160: Implement Jool-based NAT64 translator)
   stunnel4,
   sudo,
   systemd,
-- 
cgit v1.2.3