diff options
| -rw-r--r-- | data/configd-include.json | 1 | ||||
| -rw-r--r-- | data/templates/ndppd/ndppd.conf.j2 | 71 | ||||
| -rw-r--r-- | debian/control | 12 | ||||
| -rw-r--r-- | interface-definitions/include/version/nat66-version.xml.i | 2 | ||||
| -rw-r--r-- | interface-definitions/service_ndp-proxy.xml.in | 132 | ||||
| -rw-r--r-- | op-mode-definitions/monitor-log.xml.in | 6 | ||||
| -rw-r--r-- | op-mode-definitions/show-log.xml.in | 6 | ||||
| -rwxr-xr-x | smoketest/scripts/cli/test_service_ndp-proxy.py | 70 | ||||
| -rwxr-xr-x | src/conf_mode/nat66.py | 12 | ||||
| -rwxr-xr-x | src/conf_mode/service_ndp-proxy.py | 91 | ||||
| -rwxr-xr-x | src/migration-scripts/nat66/2-to-3 | 61 | 
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) | 
