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 May 13, 2023
1 parent aea8922 commit 2b10e35
Show file tree
Hide file tree
Showing 4 changed files with 358 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 Message Control Protocol</help>
<valueless/>
</properties>
</leafNode>
</children>
</node>
<!-- include end -->
95 changes: 95 additions & 0 deletions interface-definitions/nat64.xml.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?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>
<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>
#include <include/nat-translation-port.xml.i>
#include <include/nat64-protocol.xml.i>
</children>
</tagNode>
</children>
</node>
</children>
</tagNode>
</children>
</node>
</children>
</node>
</interfaceDefinition>
235 changes: 235 additions & 0 deletions src/conf_mode/nat64.py
Original file line number Diff line number Diff line change
@@ -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 <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
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)

0 comments on commit 2b10e35

Please sign in to comment.