Skip to content

Commit 8ccbfa5

Browse files
committed
nat64: T160: Implement Jool-based NAT64 translator
Signed-off-by: Joe Groocock <me@frebib.net>
1 parent 70e4767 commit 8ccbfa5

File tree

4 files changed

+303
-0
lines changed

4 files changed

+303
-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

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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+
<leafNode name="mode">
30+
<properties>
31+
<help>Operation mode of Jool packet translator</help>
32+
<valueHelp>
33+
<format>netfilter</format>
34+
<description>Use netfilter translation mode (default)</description>
35+
</valueHelp>
36+
<valueHelp>
37+
<format>iptables</format>
38+
<description>Use iptables translation mode</description>
39+
</valueHelp>
40+
<constraint>
41+
<regex>(netfilter|iptables)</regex>
42+
</constraint>
43+
</properties>
44+
<defaultValue>netfilter</defaultValue>
45+
</leafNode>
46+
<node name="source">
47+
<properties>
48+
<help>IPv6 source prefix options</help>
49+
</properties>
50+
<children>
51+
<leafNode name="prefix">
52+
<properties>
53+
<help>IPv6 prefix to be translated</help>
54+
<valueHelp>
55+
<format>ipv6net</format>
56+
<description>IPv6 prefix</description>
57+
</valueHelp>
58+
<constraint>
59+
<validator name="ipv6-prefix"/>
60+
</constraint>
61+
</properties>
62+
</leafNode>
63+
</children>
64+
</node>
65+
<node name="translation">
66+
<properties>
67+
<help>Translated IPv4 address options</help>
68+
</properties>
69+
<children>
70+
<tagNode name="pool">
71+
<properties>
72+
<help>Translation IPv4 pool number</help>
73+
<valueHelp>
74+
<format>u32:1-999999</format>
75+
<description>Number for this rule</description>
76+
</valueHelp>
77+
<constraint>
78+
<validator name="numeric" argument="--range 1-999999"/>
79+
</constraint>
80+
<constraintErrorMessage>NAT64 pool number must be between 1 and 999999</constraintErrorMessage>
81+
</properties>
82+
<children>
83+
<leafNode name="address">
84+
<properties>
85+
<help>IPv4 address or prefix to translate to</help>
86+
<valueHelp>
87+
<format>ipv4</format>
88+
<description>IPv4 address</description>
89+
</valueHelp>
90+
<valueHelp>
91+
<format>ipv4net</format>
92+
<description>IPv4 prefix</description>
93+
</valueHelp>
94+
<constraint>
95+
<validator name="ipv4-address"/>
96+
<validator name="ipv4-prefix"/>
97+
</constraint>
98+
</properties>
99+
</leafNode>
100+
#include <include/nat-translation-port.xml.i>
101+
#include <include/nat64-protocol.xml.i>
102+
</children>
103+
</tagNode>
104+
</children>
105+
</node>
106+
</children>
107+
</tagNode>
108+
</children>
109+
</node>
110+
</children>
111+
</node>
112+
</interfaceDefinition>

src/conf_mode/nat64.py

+163
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Copyright (C) 2020-2021 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+
from operator import itemgetter
20+
import csv
21+
import json
22+
import os
23+
import re
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+
# Load in existing instances so we can destroy any unknown
57+
lines = cmd("jool instance display --csv").splitlines()
58+
for namespace, instance, mode in csv.reader(lines):
59+
match = INSTANCE_REGEX.fullmatch(instance)
60+
if not match:
61+
# FIXME: Instances that don't match should be ignored but WARN'ed to the user
62+
continue
63+
num = match.group(1)
64+
65+
rules = nat64.setdefault("source", {}).setdefault("rule", {})
66+
# Mark it for deletion
67+
if num not in rules:
68+
print(f"Deleting unknown instance: {num}")
69+
rules[num] = {"deleted": True}
70+
continue
71+
72+
# If the user changes the mode, recreate the instance else Jool fails with:
73+
# Jool error: Sorry; you can't change an instance's framework for now.
74+
if rules[num]["mode"] != mode:
75+
print(f"Recreating instance {num} due to changed mode: {rules[num]['mode']} -> {mode}")
76+
rules[num]["recreate"] = True
77+
78+
return nat64
79+
80+
81+
def verify(nat64) -> None:
82+
""" """
83+
if not nat64 or "deleted" in nat64:
84+
# no need to verify the CLI as nat64 is going to be deactivated
85+
return
86+
87+
# Verify that source.prefix is set and is a /96
88+
89+
return
90+
91+
92+
def generate(nat64) -> None:
93+
""" """
94+
os.makedirs(JOOL_CONFIG_DIR, exist_ok=True)
95+
96+
if dict_search("source.rule", nat64):
97+
for rule, instance in dict_search("source.rule", nat64).items():
98+
if "deleted" in instance:
99+
continue
100+
101+
name = f"instance-{rule}"
102+
config = {
103+
"instance": name,
104+
"framework": instance["mode"],
105+
"global": {
106+
"pool6": instance["source"]["prefix"],
107+
},
108+
# "pool4": [],
109+
# "bib": [],
110+
}
111+
112+
if "description" in instance:
113+
config["comment"] = instance["description"]
114+
115+
# pylint: disable=invalid-name
116+
with open(f"{JOOL_CONFIG_DIR}/{name}.json", "w", encoding="utf-8") as f:
117+
json.dump(config, f, indent=2)
118+
119+
120+
def apply(nat64) -> None:
121+
""" """
122+
if not nat64:
123+
return
124+
125+
if dict_search("source.rule", nat64):
126+
# Deletions first to avoid conflicts
127+
for rule, instance in dict_search("source.rule", nat64).items():
128+
if not any(k in instance for k in ("deleted", "recreate")):
129+
continue
130+
131+
print(f"jool instance remove instance-{rule}")
132+
ret = run(f"jool instance remove instance-{rule}")
133+
if ret != 0:
134+
raise ConfigError(
135+
f"Failed to remove nat64 source rule {rule} (jool instance instance-{rule})"
136+
)
137+
138+
# Now creations
139+
for rule, instance in dict_search("source.rule", nat64).items():
140+
if "deleted" in instance:
141+
continue
142+
143+
name = f"instance-{rule}"
144+
print(f"jool -i {name} file handle {JOOL_CONFIG_DIR}/{name}.json")
145+
ret = run(f"jool -i {name} file handle {JOOL_CONFIG_DIR}/{name}.json")
146+
if ret != 0:
147+
raise ConfigError(f"Failed to set jool instance {name}")
148+
149+
return
150+
151+
152+
if __name__ == "__main__":
153+
import sys
154+
155+
try:
156+
check_kmod(["jool"])
157+
c = get_config()
158+
verify(c)
159+
generate(c)
160+
apply(c)
161+
except ConfigError as e:
162+
print(e)
163+
sys.exit(1)

0 commit comments

Comments
 (0)