diff --git a/debian/control b/debian/control
index 4a2706fc3a8..713888e63fc 100644
--- a/debian/control
+++ b/debian/control
@@ -78,6 +78,7 @@ Depends:
isc-dhcp-relay,
isc-dhcp-server,
iw,
+ jool,
keepalived (>=2.0.5),
lcdproc,
lcdproc-extra-drivers,
diff --git a/interface-definitions/include/nat64-protocol.xml.i b/interface-definitions/include/nat64-protocol.xml.i
new file mode 100644
index 00000000000..b61d77e34ad
--- /dev/null
+++ b/interface-definitions/include/nat64-protocol.xml.i
@@ -0,0 +1,27 @@
+
+
+
+ Apply translation address to a specfic protocol
+
+
+
+
+ Transmission Control Protocol
+
+
+
+
+
+ User Datagram Protocol
+
+
+
+
+
+ Internet Message Control Protocol
+
+
+
+
+
+
diff --git a/interface-definitions/nat64.xml.in b/interface-definitions/nat64.xml.in
new file mode 100644
index 00000000000..f2b9ee424e9
--- /dev/null
+++ b/interface-definitions/nat64.xml.in
@@ -0,0 +1,95 @@
+
+
+
+
+ IPv6-to-IPv4 Network Address Translation (NAT64) Settings
+ 500
+
+
+
+
+ IPv6 source to IPv4 destination address translation
+
+
+
+
+ Source NAT64 rule number
+
+ u32:1-999999
+ Number for this rule
+
+
+
+
+ NAT64 rule number must be between 1 and 999999
+
+
+ #include
+ #include
+
+
+ IPv6 source prefix options
+
+
+
+
+ IPv6 prefix to be translated
+
+ ipv6net
+ IPv6 prefix
+
+
+
+
+
+
+
+
+
+
+ Translated IPv4 address options
+
+
+
+
+ Translation IPv4 pool number
+
+ u32:1-999999
+ Number for this rule
+
+
+
+
+ NAT64 pool number must be between 1 and 999999
+
+
+
+
+ IPv4 address or prefix to translate to
+
+ ipv4
+ IPv4 address
+
+
+ ipv4net
+ IPv4 prefix
+
+
+
+
+
+
+
+ #include
+ #include
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/conf_mode/nat64.py b/src/conf_mode/nat64.py
new file mode 100755
index 00000000000..78d243efb0b
--- /dev/null
+++ b/src/conf_mode/nat64.py
@@ -0,0 +1,235 @@
+#!/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 .
+
+# pylint: disable=empty-docstring,missing-module-docstring
+
+import csv
+import json
+import os
+import re
+from ipaddress import IPv6Network
+
+from vyos import ConfigError, airbag
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.util import check_kmod, cmd, dict_search, run
+from vyos.xml import defaults
+
+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:
+ conf = Config()
+
+ base = ["nat64"]
+ nat64 = conf.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"
+
+ # Load in existing instances so we can destroy any unknown
+ lines = cmd("jool instance display --csv").splitlines()
+ for namespace, instance, mode 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:
+ print(f"Deleting unknown instance: {num}")
+ 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 rules[num]["mode"] != mode:
+ print(
+ f"Recreating instance {num} due to changed mode: {mode} -> {rules[num]['mode']}"
+ )
+ rules[num]["recreate"] = True
+
+ lines = cmd(f"jool -i instance-{num} global display --csv --no-header")
+ existing = dict(csv.reader(lines.splitlines()))
+
+ # 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 rules[num]["source"]["prefix"] != existing["pool6"]
+ ):
+ print(
+ f"Recreating instance {num} due to changed source prefix: "
+ f"{existing['pool6']} -> {rules[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):
+ for rule, instance in dict_search("source.rule", nat64).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")
+
+ for num, pool in dict_search("translation.pool", instance).items():
+ if "address" not in pool:
+ raise ConfigError(
+ f"Source NAT64 rule {rule} translation pool {num} missing address/prefix"
+ )
+ if "port" not in pool:
+ raise ConfigError(
+ f"Source NAT64 rule {rule} translation pool {num} missing port(-range)"
+ )
+
+ # Ensure only 1 netfilter instance per namespace
+
+ return
+
+
+def generate(nat64) -> None:
+ """ """
+ os.makedirs(JOOL_CONFIG_DIR, exist_ok=True)
+
+ tokeep = set()
+ if dict_search("source.rule", nat64):
+ for rule, instance in dict_search("source.rule", nat64).items():
+ if "deleted" in instance:
+ continue
+
+ name = f"instance-{rule}"
+ config = {
+ "instance": name,
+ "framework": "netfilter",
+ "global": {
+ "pool6": instance["source"]["prefix"],
+ "manually-enabled": "disable" not in instance,
+ },
+ # "pool4": [],
+ # "bib": [],
+ }
+
+ if "description" in instance:
+ config["comment"] = instance["description"]
+
+ pool4 = []
+ for pool in dict_search("translation.pool", instance).values():
+ 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
+
+ # pylint: disable=invalid-name
+ with open(f"{JOOL_CONFIG_DIR}/{name}.json", "w", encoding="utf-8") as f:
+ json.dump(config, f, indent=2)
+
+ tokeep.add(name)
+
+ instfiles = {
+ file
+ for file in os.listdir(JOOL_CONFIG_DIR)
+ if INSTANCE_REGEX.fullmatch(file)
+ and os.path.isfile(os.path.join(JOOL_CONFIG_DIR, file))
+ }
+ for file in instfiles - tokeep:
+ print(f"Removing instance file {file}")
+ os.unlink(os.path.join(JOOL_CONFIG_DIR, file))
+
+
+def apply(nat64) -> None:
+ """ """
+ if not nat64:
+ return
+
+ if dict_search("source.rule", nat64):
+ # Deletions first to avoid conflicts
+ for rule, instance in dict_search("source.rule", nat64).items():
+ if not any(k in instance for k in ("deleted", "recreate")):
+ continue
+
+ print(f"jool instance remove instance-{rule}")
+ 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 dict_search("source.rule", nat64).items():
+ if "deleted" in instance:
+ continue
+
+ name = f"instance-{rule}"
+ print(f"jool -i {name} file handle {JOOL_CONFIG_DIR}/{name}.json")
+ 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}")
+
+ return
+
+
+if __name__ == "__main__":
+ import sys
+
+ try:
+ check_kmod(["jool"])
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ sys.exit(1)