summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Breunig <christian@breunig.cc>2023-12-19 07:49:03 +0100
committerChristian Breunig <christian@breunig.cc>2023-12-20 23:21:07 +0100
commit4d721a58020971d00ab854c37b68e88359999f9c (patch)
tree75bb9f8bdf08ccb5cdc793e434c374070704312b
parentb873112dd7253b64d323e183758dbabaa0f28b6e (diff)
downloadvyos-1x-4d721a58020971d00ab854c37b68e88359999f9c.tar.gz
vyos-1x-4d721a58020971d00ab854c37b68e88359999f9c.zip
T2898: add ndp-proxy service
VyOS CLI command set service ndp-proxy interface eth0 prefix 2001:db8::/64 mode 'static' Will generate the following NDP proxy configuration $ cat /run/ndppd/ndppd.conf # autogenerated by service_ndp-proxy.py # This tells 'ndppd' how often to reload the route file /proc/net/ipv6_route route-ttl 30000 # This sets up a listener, that will listen for any Neighbor Solicitation # messages, and respond to them according to a set of rules proxy eth0 { # Turn on or off the router flag for Neighbor Advertisements router no # Control how long to wait for a Neighbor Advertisment message before invalidating the entry (milliseconds) timeout 500 # Control how long a valid or invalid entry remains in the cache (milliseconds) ttl 30000 # This is a rule that the target address is to match against. If no netmask # is provided, /128 is assumed. You may have several rule sections, and the # addresses may or may not overlap. rule 2001:db8::/64 { static } }
-rw-r--r--data/configd-include.json1
-rw-r--r--data/templates/ndppd/ndppd.conf.j271
-rw-r--r--debian/control12
-rw-r--r--interface-definitions/include/version/nat66-version.xml.i2
-rw-r--r--interface-definitions/service_ndp-proxy.xml.in132
-rw-r--r--op-mode-definitions/monitor-log.xml.in6
-rw-r--r--op-mode-definitions/show-log.xml.in6
-rwxr-xr-xsmoketest/scripts/cli/test_service_ndp-proxy.py70
-rwxr-xr-xsrc/conf_mode/nat66.py12
-rwxr-xr-xsrc/conf_mode/service_ndp-proxy.py91
-rwxr-xr-xsrc/migration-scripts/nat66/2-to-361
11 files changed, 406 insertions, 58 deletions
diff --git a/data/configd-include.json b/data/configd-include.json
index 92d3863ce..6d7261b73 100644
--- a/data/configd-include.json
+++ b/data/configd-include.json
@@ -63,6 +63,7 @@
"service_ipoe-server.py",
"service_mdns-repeater.py",
"service_monitoring_telegraf.py",
+"service_ndp-proxy.py",
"service_pppoe-server.py",
"service_router-advert.py",
"service_upnp.py",
diff --git a/data/templates/ndppd/ndppd.conf.j2 b/data/templates/ndppd/ndppd.conf.j2
index 1297f36be..6369dbdeb 100644
--- a/data/templates/ndppd/ndppd.conf.j2
+++ b/data/templates/ndppd/ndppd.conf.j2
@@ -1,44 +1,35 @@
-########################################################
-#
-# autogenerated by nat66.py
-#
-# The configuration file must define one upstream
-# interface.
-#
-# For some services, such as nat66, because it runs
-# stateless, it needs to rely on NDP Proxy to respond
-# to NDP requests.
-#
-# When using nat66 source rules, NDP Proxy needs
-# to be enabled
-#
-########################################################
+# autogenerated by service_ndp-proxy.py
-{% set global = namespace(ndppd_interfaces = [],ndppd_prefixs = []) %}
-{% if source.rule is vyos_defined %}
-{% for rule, config in source.rule.items() if config.disable is not defined %}
-{% if config.outbound_interface.name is vyos_defined %}
-{% if config.outbound_interface.name not in global.ndppd_interfaces %}
-{% set global.ndppd_interfaces = global.ndppd_interfaces + [config.outbound_interface.name] %}
-{% endif %}
-{% if config.translation.address is vyos_defined and config.translation.address | is_ip_network %}
-{% set global.ndppd_prefixs = global.ndppd_prefixs + [{'interface':config.outbound_interface.name,'rule':config.translation.address}] %}
-{% endif %}
-{% endif %}
-{% endfor %}
-{% endif %}
+# This tells 'ndppd' how often to reload the route file /proc/net/ipv6_route
+route-ttl {{ route_refresh }}
+
+{% if interface is vyos_defined %}
+# This sets up a listener, that will listen for any Neighbor Solicitation
+# messages, and respond to them according to a set of rules
+{% for iface, iface_config in interface.items() if iface_config.disable is not vyos_defined %}
+proxy {{ iface }} {
+ # Turn on or off the router flag for Neighbor Advertisements
+ router {{ 'yes' if iface_config.enable_router_bit is vyos_defined else 'no' }}
+ # Control how long to wait for a Neighbor Advertisment message before invalidating the entry (milliseconds)
+ timeout {{ iface_config.timeout }}
+ # Control how long a valid or invalid entry remains in the cache (milliseconds)
+ ttl {{ iface_config.ttl }}
-{% for interface in global.ndppd_interfaces %}
-proxy {{ interface }} {
- router yes
- timeout 500
- ttl 30000
-{% for map in global.ndppd_prefixs %}
-{% if map.interface == interface %}
- rule {{ map.rule }} {
- static
+{% if iface_config.prefix is vyos_defined %}
+ # This is a rule that the target address is to match against. If no netmask
+ # is provided, /128 is assumed. You may have several rule sections, and the
+ # addresses may or may not overlap.
+{% for prefix, prefix_config in iface_config.prefix.items() if prefix_config.disable is not vyos_defined %}
+ rule {{ prefix }} {
+{% if prefix_config.mode is vyos_defined('interface') %}
+ iface {{ prefix_config.interface }}
+{% else %}
+ {{ prefix_config.mode }}
+{% endif %}
}
-{% endif %}
-{% endfor %}
+{% endfor %}
+{% endif %}
}
-{% endfor %}
+
+{% endfor %}
+{% endif %}
diff --git a/debian/control b/debian/control
index 08adc8a68..af42202fc 100644
--- a/debian/control
+++ b/debian/control
@@ -152,7 +152,7 @@ Depends:
console-data,
dropbear,
# End "service console-server"
-# For "set service aws glb"
+# For "service aws glb"
aws-gwlbtun,
# For "service dns dynamic"
ddclient (>= 3.11.1),
@@ -160,6 +160,9 @@ Depends:
# # For "service ids"
fastnetmon [amd64],
# End "service ids"
+# # For "service ndp-proxy"
+ ndppd,
+# End "service ndp-proxy"
# For "service router-advert"
radvd,
# End "service route-advert"
@@ -251,18 +254,15 @@ Depends:
# For "nat64"
jool,
# End "nat64"
-# For nat66
- ndppd,
-# End nat66
# For "system ntp"
chrony,
# End "system ntp"
# For "vpn openconnect"
ocserv,
# End "vpn openconnect"
-# For "set system flow-accounting"
+# For "system flow-accounting"
pmacct (>= 1.6.0),
-# End "set system flow-accounting"
+# End "system flow-accounting"
# For container
podman,
netavark,
diff --git a/interface-definitions/include/version/nat66-version.xml.i b/interface-definitions/include/version/nat66-version.xml.i
index 478ca080f..43a54c969 100644
--- a/interface-definitions/include/version/nat66-version.xml.i
+++ b/interface-definitions/include/version/nat66-version.xml.i
@@ -1,3 +1,3 @@
<!-- include start from include/version/nat66-version.xml.i -->
-<syntaxVersion component='nat66' version='2'></syntaxVersion>
+<syntaxVersion component='nat66' version='3'></syntaxVersion>
<!-- include end -->
diff --git a/interface-definitions/service_ndp-proxy.xml.in b/interface-definitions/service_ndp-proxy.xml.in
new file mode 100644
index 000000000..9801c99ab
--- /dev/null
+++ b/interface-definitions/service_ndp-proxy.xml.in
@@ -0,0 +1,132 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="service">
+ <children>
+ <node name="ndp-proxy" owner="${vyos_conf_scripts_dir}/service_ndp-proxy.py">
+ <properties>
+ <help>Neighbor Discovery Protocol (NDP) Proxy</help>
+ </properties>
+ <children>
+ <leafNode name="route-refresh">
+ <properties>
+ <help>Refresh interval for IPv6 routes</help>
+ <valueHelp>
+ <format>u32:10000-120000</format>
+ <description>Time in milliseconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 10000-120000"/>
+ </constraint>
+ <constraintErrorMessage>Route-refresh must be between 10000 and 120000 milliseconds</constraintErrorMessage>
+ </properties>
+ <defaultValue>30000</defaultValue>
+ </leafNode>
+ <tagNode name="interface">
+ <properties>
+ <help>NDP proxy listener interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces</script>
+ </completionHelp>
+ <constraint>
+ #include <include/constraint/interface-name.xml.i>
+ </constraint>
+ </properties>
+ <children>
+ #include <include/generic-disable-node.xml.i>
+ <leafNode name="enable-router-bit">
+ <properties>
+ <help>Enable router bit in Neighbor Advertisement messages</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="timeout">
+ <properties>
+ <help>Timeout for Neighbor Advertisement after Neighbor Solicitation message</help>
+ <valueHelp>
+ <format>u32:500-120000</format>
+ <description>Timeout in milliseconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 500-120000"/>
+ </constraint>
+ <constraintErrorMessage>Timeout must be between 500 and 120000 milliseconds</constraintErrorMessage>
+ </properties>
+ <defaultValue>500</defaultValue>
+ </leafNode>
+ <leafNode name="ttl">
+ <properties>
+ <help>Proxy entry cache Time-To-Live</help>
+ <valueHelp>
+ <format>u32:10000-120000</format>
+ <description>Time in milliseconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 10000-120000"/>
+ </constraint>
+ <constraintErrorMessage>TTL must be between 10000 and 120000 milliseconds</constraintErrorMessage>
+ </properties>
+ <defaultValue>30000</defaultValue>
+ </leafNode>
+ <tagNode name="prefix">
+ <properties>
+ <help>Prefix target addresses are matched against</help>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 network prefix</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6-prefix"/>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ <children>
+ #include <include/generic-disable-node.xml.i>
+ <leafNode name="mode">
+ <properties>
+ <help>Specify the running mode of the rule</help>
+ <completionHelp>
+ <list>static auto interface</list>
+ </completionHelp>
+ <valueHelp>
+ <format>static</format>
+ <description>Immediately answer any Neighbor Solicitation Messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>auto</format>
+ <description>Check for a matching route in /proc/net/ipv6_route</description>
+ </valueHelp>
+ <valueHelp>
+ <format>interface</format>
+ <description>Forward Neighbor Solicitation message through specified interface</description>
+ </valueHelp>
+ <constraint>
+ <regex>(static|auto|interface)</regex>
+ </constraint>
+ <constraintErrorMessage>Mode must be either one of: static, auto or interface</constraintErrorMessage>
+ </properties>
+ <defaultValue>static</defaultValue>
+ </leafNode>
+ <leafNode name="interface">
+ <properties>
+ <help>Interface to forward Neighbor Solicitation message through. Required for "iface" mode</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces</script>
+ </completionHelp>
+ <constraint>
+ #include <include/constraint/interface-name.xml.i>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/monitor-log.xml.in b/op-mode-definitions/monitor-log.xml.in
index 3a8118dcb..c03ec4cce 100644
--- a/op-mode-definitions/monitor-log.xml.in
+++ b/op-mode-definitions/monitor-log.xml.in
@@ -120,6 +120,12 @@
</properties>
<command>journalctl --no-hostname --boot --follow --dmesg</command>
</leafNode>
+ <leafNode name="ndp-proxy">
+ <properties>
+ <help>Monitor last lines of Neighbor Discovery Protocol (NDP) Proxy</help>
+ </properties>
+ <command>journalctl --no-hostname --boot --follow --unit ndppd.service</command>
+ </leafNode>
<leafNode name="nhrp">
<properties>
<help>Monitor last lines of Next Hop Resolution Protocol log</help>
diff --git a/op-mode-definitions/show-log.xml.in b/op-mode-definitions/show-log.xml.in
index 399c6acf8..b013bdfe4 100644
--- a/op-mode-definitions/show-log.xml.in
+++ b/op-mode-definitions/show-log.xml.in
@@ -464,6 +464,12 @@
</properties>
<command>egrep -i "kernel:.*\[NAT-[A-Z]{3,}-[0-9]+(-MASQ)?\]" $(find /var/log -maxdepth 1 -type f -name messages\* | sort -t. -k2nr)</command>
</leafNode>
+ <leafNode name="ndp-proxy">
+ <properties>
+ <help>Show log for Neighbor Discovery Protocol (NDP) Proxy</help>
+ </properties>
+ <command>journalctl --no-hostname --boot --unit ndppd.service</command>
+ </leafNode>
<leafNode name="nhrp">
<properties>
<help>Show log for Next Hop Resolution Protocol (NHRP)</help>
diff --git a/smoketest/scripts/cli/test_service_ndp-proxy.py b/smoketest/scripts/cli/test_service_ndp-proxy.py
new file mode 100755
index 000000000..a947ec478
--- /dev/null
+++ b/smoketest/scripts/cli/test_service_ndp-proxy.py
@@ -0,0 +1,70 @@
+#!/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/>.
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.ifconfig import Section
+from vyos.utils.process import cmd
+from vyos.utils.process import process_named_running
+
+PROCESS_NAME = 'ndppd'
+NDPPD_CONF = '/run/ndppd/ndppd.conf'
+base_path = ['service', 'ndp-proxy']
+
+def getConfigSection(string=None, end=' {', endsection='^}'):
+ tmp = f'cat {NDPPD_CONF} | sed -n "/^{string}{end}/,/{endsection}/p"'
+ out = cmd(tmp)
+ return out
+
+class TestServiceNDPProxy(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestServiceNDPProxy, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ def tearDown(self):
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
+ # delete testing SSH config
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ self.assertFalse(process_named_running(PROCESS_NAME))
+
+ def test_basic(self):
+ interfaces = Section.interfaces('ethernet')
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface])
+ self.cli_set(base_path + ['interface', interface, 'enable-router-bit'])
+
+ self.cli_commit()
+
+ for interface in interfaces:
+ config = getConfigSection(f'proxy {interface}')
+ self.assertIn(f'proxy {interface}', config)
+ self.assertIn(f'router yes', config)
+ self.assertIn(f'timeout 500', config) # default value
+ self.assertIn(f'ttl 30000', config) # default value
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/src/conf_mode/nat66.py b/src/conf_mode/nat66.py
index 0ba08aef3..dee1551fe 100755
--- a/src/conf_mode/nat66.py
+++ b/src/conf_mode/nat66.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-2021 VyOS maintainers and contributors
+# Copyright (C) 2020-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
@@ -36,7 +36,6 @@ airbag.enable()
k_mod = ['nft_nat', 'nft_chain_nat']
nftables_nat66_config = '/run/nftables_nat66.nft'
-ndppd_config = '/run/ndppd/ndppd.conf'
def get_config(config=None):
if config:
@@ -101,7 +100,6 @@ def generate(nat):
nat['first_install'] = True
render(nftables_nat66_config, 'firewall/nftables-nat66.j2', nat, permission=0o755)
- render(ndppd_config, 'ndppd/ndppd.conf.j2', nat, permission=0o755)
return None
def apply(nat):
@@ -109,14 +107,6 @@ def apply(nat):
return None
cmd(f'nft -f {nftables_nat66_config}')
-
- if 'deleted' in nat or not dict_search('source.rule', nat):
- cmd('systemctl stop ndppd')
- if os.path.isfile(ndppd_config):
- os.unlink(ndppd_config)
- else:
- cmd('systemctl restart ndppd')
-
call_dependents()
return None
diff --git a/src/conf_mode/service_ndp-proxy.py b/src/conf_mode/service_ndp-proxy.py
new file mode 100755
index 000000000..aa2374f4c
--- /dev/null
+++ b/src/conf_mode/service_ndp-proxy.py
@@ -0,0 +1,91 @@
+#!/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/>.
+
+import os
+
+from sys import exit
+
+from vyos.config import Config
+from vyos.configverify import verify_interface_exists
+from vyos.utils.process import call
+from vyos.template import render
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+systemd_service = 'ndppd.service'
+ndppd_config = '/run/ndppd/ndppd.conf'
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['service', 'ndp-proxy']
+ if not conf.exists(base):
+ return None
+
+ ndpp = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True,
+ with_recursive_defaults=True)
+
+ return ndpp
+
+def verify(ndpp):
+ if not ndpp:
+ return None
+
+ if 'interface' in ndpp:
+ for interface, interface_config in ndpp['interface'].items():
+ verify_interface_exists(interface)
+
+ if 'rule' in interface_config:
+ for rule, rule_config in interface_config['rule'].items():
+ if rule_config['mode'] == 'interface' and 'interface' not in rule_config:
+ raise ConfigError(f'Rule "{rule}" uses interface mode but no interface defined!')
+
+ if rule_config['mode'] != 'interface' and 'interface' in rule_config:
+ if interface_config['mode'] != 'interface' and 'interface' in interface_config:
+ raise ConfigError(f'Rule "{rule}" does not use interface mode, thus interface can not be defined!')
+
+ return None
+
+def generate(ndpp):
+ if not ndpp:
+ return None
+
+ render(ndppd_config, 'ndppd/ndppd.conf.j2', ndpp)
+ return None
+
+def apply(ndpp):
+ if not ndpp:
+ call(f'systemctl stop {systemd_service}')
+ if os.path.isfile(ndppd_config):
+ os.unlink(ndppd_config)
+ return None
+
+ call(f'systemctl reload-or-restart {systemd_service}')
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/migration-scripts/nat66/2-to-3 b/src/migration-scripts/nat66/2-to-3
new file mode 100755
index 000000000..f34f170b3
--- /dev/null
+++ b/src/migration-scripts/nat66/2-to-3
@@ -0,0 +1,61 @@
+#!/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/>.
+
+from sys import argv,exit
+from vyos.configtree import ConfigTree
+
+if len(argv) < 2:
+ print("Must specify file name!")
+ exit(1)
+
+file_name = argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+base = ['nat66', 'source']
+new_base = ['service', 'ndp-proxy', 'interface']
+
+config = ConfigTree(config_file)
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+
+for rule in config.list_nodes(base + ['rule']):
+ base_rule = base + ['rule', rule]
+
+ interface = None
+ if config.exists(base_rule + ['outbound-interface', 'name']):
+ interface = config.return_value(base_rule + ['outbound-interface', 'name'])
+ else:
+ continue
+
+ prefix_base = base_rule + ['source', 'prefix']
+ if config.exists(prefix_base):
+ prefix = config.return_value(prefix_base)
+ config.set(new_base + [interface, 'prefix', prefix, 'mode'], value='static')
+ config.set_tag(new_base)
+ config.set_tag(new_base + [interface, 'prefix'])
+
+ if config.exists(base_rule + ['disable']):
+ config.set(new_base + [interface, 'prefix', prefix, 'disable'])
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ exit(1)