Skip to content

Commit

Permalink
nat64: T160: Implement Jool-based NAT64 translator
Browse files Browse the repository at this point in the history
Signed-off-by: Joe Groocock <me@frebib.net>
  • Loading branch information
frebib committed Aug 20, 2023
1 parent ffb798b commit 3812109
Show file tree
Hide file tree
Showing 4 changed files with 348 additions and 0 deletions.
1 change: 1 addition & 0 deletions debian/control
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ Depends:
isc-dhcp-relay,
isc-dhcp-server,
iw,
jool,
keepalived (>=2.0.5),
lcdproc,
lcdproc-extra-drivers,
Expand Down
27 changes: 27 additions & 0 deletions interface-definitions/include/nat64-protocol.xml.i
Original file line number Diff line number Diff line change
@@ -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 -->
97 changes: 97 additions & 0 deletions interface-definitions/nat64.xml.in
Original file line number Diff line number Diff line change
@@ -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>500</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>
223 changes: 223 additions & 0 deletions src/conf_mode/nat64.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
#!/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 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, is_node_changed
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:
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

# pylint: disable=invalid-name
with open(f"{JOOL_CONFIG_DIR}/{name}.json", "w", encoding="utf-8") as f:
json.dump(config, f, indent=2)


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)

0 comments on commit 3812109

Please sign in to comment.