Skip to content

Commit 884d81e

Browse files
committed
nat64: T160: Implement Jool-based NAT64 translator
Signed-off-by: Joe Groocock <me@frebib.net>
1 parent 834a786 commit 884d81e

File tree

4 files changed

+331
-0
lines changed

4 files changed

+331
-0
lines changed

debian/control

+1
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ Depends:
7878
isc-dhcp-relay,
7979
isc-dhcp-server,
8080
iw,
81+
jool,
8182
keepalived (>=2.0.5),
8283
lcdproc,
8384
lcdproc-extra-drivers,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<!-- include start from nat64-protocol.xml.i -->
2+
<node name="protocol">
3+
<properties>
4+
<help>Apply translation address to a specfic protocol</help>
5+
</properties>
6+
<children>
7+
<leafNode name="tcp">
8+
<properties>
9+
<help>Transmission Control Protocol</help>
10+
<valueless/>
11+
</properties>
12+
</leafNode>
13+
<leafNode name="udp">
14+
<properties>
15+
<help>User Datagram Protocol</help>
16+
<valueless/>
17+
</properties>
18+
</leafNode>
19+
<leafNode name="icmp">
20+
<properties>
21+
<help>Internet Message Control Protocol</help>
22+
<valueless/>
23+
</properties>
24+
</leafNode>
25+
</children>
26+
</node>
27+
<!-- include end -->

interface-definitions/nat64.xml.in

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?xml version="1.0"?>
2+
<interfaceDefinition>
3+
<node name="nat64" owner="${vyos_conf_scripts_dir}/nat64.py">
4+
<properties>
5+
<help>IPv6-to-IPv4 Network Address Translation (NAT64) Settings</help>
6+
<priority>500</priority>
7+
</properties>
8+
<children>
9+
<node name="source">
10+
<properties>
11+
<help>IPv6 source to IPv4 destination address translation</help>
12+
</properties>
13+
<children>
14+
<tagNode name="rule">
15+
<properties>
16+
<help>Source NAT64 rule number</help>
17+
<valueHelp>
18+
<format>u32:1-999999</format>
19+
<description>Number for this rule</description>
20+
</valueHelp>
21+
<constraint>
22+
<validator name="numeric" argument="--range 1-999999"/>
23+
</constraint>
24+
<constraintErrorMessage>NAT64 rule number must be between 1 and 999999</constraintErrorMessage>
25+
</properties>
26+
<children>
27+
#include <include/generic-description.xml.i>
28+
#include <include/generic-disable-node.xml.i>
29+
<node name="source">
30+
<properties>
31+
<help>IPv6 source prefix options</help>
32+
</properties>
33+
<children>
34+
<leafNode name="prefix">
35+
<properties>
36+
<help>IPv6 prefix to be translated</help>
37+
<valueHelp>
38+
<format>ipv6net</format>
39+
<description>IPv6 prefix</description>
40+
</valueHelp>
41+
<constraint>
42+
<validator name="ipv6-prefix"/>
43+
</constraint>
44+
</properties>
45+
</leafNode>
46+
</children>
47+
</node>
48+
<node name="translation">
49+
<properties>
50+
<help>Translated IPv4 address options</help>
51+
</properties>
52+
<children>
53+
<tagNode name="pool">
54+
<properties>
55+
<help>Translation IPv4 pool number</help>
56+
<valueHelp>
57+
<format>u32:1-999999</format>
58+
<description>Number for this rule</description>
59+
</valueHelp>
60+
<constraint>
61+
<validator name="numeric" argument="--range 1-999999"/>
62+
</constraint>
63+
<constraintErrorMessage>NAT64 pool number must be between 1 and 999999</constraintErrorMessage>
64+
</properties>
65+
<children>
66+
<leafNode name="address">
67+
<properties>
68+
<help>IPv4 address or prefix to translate to</help>
69+
<valueHelp>
70+
<format>ipv4</format>
71+
<description>IPv4 address</description>
72+
</valueHelp>
73+
<valueHelp>
74+
<format>ipv4net</format>
75+
<description>IPv4 prefix</description>
76+
</valueHelp>
77+
<constraint>
78+
<validator name="ipv4-address"/>
79+
<validator name="ipv4-prefix"/>
80+
</constraint>
81+
</properties>
82+
</leafNode>
83+
#include <include/nat-translation-port.xml.i>
84+
#include <include/nat64-protocol.xml.i>
85+
</children>
86+
</tagNode>
87+
</children>
88+
</node>
89+
</children>
90+
</tagNode>
91+
</children>
92+
</node>
93+
</children>
94+
</node>
95+
</interfaceDefinition>

src/conf_mode/nat64.py

+208
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Copyright (C) 2023 VyOS maintainers and contributors
4+
#
5+
# This program is free software; you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License version 2 or later as
7+
# published by the Free Software Foundation.
8+
#
9+
# This program is distributed in the hope that it will be useful,
10+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
# GNU General Public License for more details.
13+
#
14+
# You should have received a copy of the GNU General Public License
15+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
17+
# pylint: disable=empty-docstring,missing-module-docstring
18+
19+
import csv
20+
import json
21+
import os
22+
import re
23+
from ipaddress import IPv6Network
24+
25+
from vyos import ConfigError, airbag
26+
from vyos.config import Config
27+
from vyos.configdict import dict_merge
28+
from vyos.util import check_kmod, cmd, dict_search, run
29+
from vyos.xml import defaults
30+
31+
airbag.enable()
32+
33+
INSTANCE_REGEX = re.compile(r"instance-(\d+)")
34+
JOOL_CONFIG_DIR = "/run/jool"
35+
36+
37+
def get_config(config: Config | None = None) -> None:
38+
""" """
39+
if config is None:
40+
conf = Config()
41+
42+
base = ["nat64"]
43+
nat64 = conf.get_config_dict(base, key_mangling=("-", "_"), get_first_key=True)
44+
45+
# T2665: we must add the tagNode defaults individually until this is
46+
# moved to the base class
47+
for direction in ["source"]:
48+
if direction in nat64:
49+
default_values = defaults(base + [direction, "rule"])
50+
if "rule" in nat64[direction]:
51+
for rule in nat64[direction]["rule"]:
52+
nat64[direction]["rule"][rule] = dict_merge(
53+
default_values, nat64[direction]["rule"][rule]
54+
)
55+
56+
# Only support netfilter for now
57+
nat64[direction]["rule"][rule]["mode"] = "netfilter"
58+
59+
# Load in existing instances so we can destroy any unknown
60+
lines = cmd("jool instance display --csv").splitlines()
61+
for namespace, instance, mode in csv.reader(lines):
62+
match = INSTANCE_REGEX.fullmatch(instance)
63+
if not match:
64+
# FIXME: Instances that don't match should be ignored but WARN'ed to the user
65+
continue
66+
num = match.group(1)
67+
68+
rules = nat64.setdefault("source", {}).setdefault("rule", {})
69+
# Mark it for deletion
70+
if num not in rules:
71+
print(f"Deleting unknown instance: {num}")
72+
rules[num] = {"deleted": True}
73+
continue
74+
75+
# If the user changes the mode, recreate the instance else Jool fails with:
76+
# Jool error: Sorry; you can't change an instance's framework for now.
77+
if rules[num]["mode"] != mode:
78+
print(
79+
f"Recreating instance {num} due to changed mode: {mode} -> {rules[num]['mode']}"
80+
)
81+
rules[num]["recreate"] = True
82+
83+
lines = cmd(f"jool -i instance-{num} global display --csv --no-header")
84+
existing = dict(csv.reader(lines.splitlines()))
85+
86+
# If the user changes the pool6, recreate the instance else Jool fails with:
87+
# Jool error: Sorry; you can't change a NAT64 instance's pool6 for now.
88+
if (
89+
dict_search("source.prefix", rules[num])
90+
and rules[num]["source"]["prefix"] != existing["pool6"]
91+
):
92+
print(
93+
f"Recreating instance {num} due to changed source prefix: "
94+
f"{existing['pool6']} -> {rules[num]['source']['prefix']}",
95+
)
96+
rules[num]["recreate"] = True
97+
98+
return nat64
99+
100+
101+
def verify(nat64) -> None:
102+
""" """
103+
if not nat64:
104+
# no need to verify the CLI as nat64 is going to be deactivated
105+
return
106+
107+
if dict_search("source.rule", nat64):
108+
for rule, instance in dict_search("source.rule", nat64).items():
109+
if "deleted" in instance:
110+
continue
111+
112+
# Verify that source.prefix is set and is a /96
113+
if not dict_search("source.prefix", instance):
114+
raise ConfigError(f"Source NAT64 rule {rule} missing source prefix")
115+
if IPv6Network(instance["source"]["prefix"]).prefixlen != 96:
116+
raise ConfigError(f"Source NAT64 rule {rule} source prefix must be /96")
117+
118+
# Ensure only 1 netfilter instance per namespace
119+
120+
return
121+
122+
123+
def generate(nat64) -> None:
124+
""" """
125+
os.makedirs(JOOL_CONFIG_DIR, exist_ok=True)
126+
127+
tokeep = set()
128+
if dict_search("source.rule", nat64):
129+
for rule, instance in dict_search("source.rule", nat64).items():
130+
if "deleted" in instance:
131+
continue
132+
133+
name = f"instance-{rule}"
134+
config = {
135+
"instance": name,
136+
"framework": "netfilter",
137+
"global": {
138+
"pool6": instance["source"]["prefix"],
139+
"manually-enabled": "disable" not in instance,
140+
},
141+
# "pool4": [],
142+
# "bib": [],
143+
}
144+
145+
if "description" in instance:
146+
config["comment"] = instance["description"]
147+
148+
# pylint: disable=invalid-name
149+
with open(f"{JOOL_CONFIG_DIR}/{name}.json", "w", encoding="utf-8") as f:
150+
json.dump(config, f, indent=2)
151+
152+
tokeep.add(name)
153+
154+
instfiles = {
155+
file
156+
for file in os.listdir(JOOL_CONFIG_DIR)
157+
if INSTANCE_REGEX.fullmatch(file)
158+
and os.path.isfile(os.path.join(JOOL_CONFIG_DIR, file))
159+
}
160+
for file in instfiles - tokeep:
161+
print(f"Removing instance file {file}")
162+
os.unlink(os.path.join(JOOL_CONFIG_DIR, file))
163+
164+
165+
def apply(nat64) -> None:
166+
""" """
167+
if not nat64:
168+
return
169+
170+
if dict_search("source.rule", nat64):
171+
# Deletions first to avoid conflicts
172+
for rule, instance in dict_search("source.rule", nat64).items():
173+
if not any(k in instance for k in ("deleted", "recreate")):
174+
continue
175+
176+
print(f"jool instance remove instance-{rule}")
177+
ret = run(f"jool instance remove instance-{rule}")
178+
if ret != 0:
179+
raise ConfigError(
180+
f"Failed to remove nat64 source rule {rule} (jool instance instance-{rule})"
181+
)
182+
183+
# Now creations
184+
for rule, instance in dict_search("source.rule", nat64).items():
185+
if "deleted" in instance:
186+
continue
187+
188+
name = f"instance-{rule}"
189+
print(f"jool -i {name} file handle {JOOL_CONFIG_DIR}/{name}.json")
190+
ret = run(f"jool -i {name} file handle {JOOL_CONFIG_DIR}/{name}.json")
191+
if ret != 0:
192+
raise ConfigError(f"Failed to set jool instance {name}")
193+
194+
return
195+
196+
197+
if __name__ == "__main__":
198+
import sys
199+
200+
try:
201+
check_kmod(["jool"])
202+
c = get_config()
203+
verify(c)
204+
generate(c)
205+
apply(c)
206+
except ConfigError as e:
207+
print(e)
208+
sys.exit(1)

0 commit comments

Comments
 (0)