diff --git a/data/templates/accel-ppp/ipoe.config.j2 b/data/templates/accel-ppp/ipoe.config.j2 index 9729b295e..81f63c53b 100644 --- a/data/templates/accel-ppp/ipoe.config.j2 +++ b/data/templates/accel-ppp/ipoe.config.j2 @@ -1,109 +1,109 @@ {# j2lint: disable=operator-enclosed-by-spaces #} ### generated by ipoe.py ### [modules] log_syslog ipoe shaper {# Common authentication backend definitions #} {% include 'accel-ppp/config_modules_auth_mode.j2' %} ippool ipv6pool ipv6_nd ipv6_dhcp {% if snmp is vyos_defined %} net-snmp {% endif %} {% if limits is vyos_defined %} connlimit {% endif %} {% if extended_scripts is vyos_defined %} pppd_compat {% endif %} [core] thread-count={{ thread_count }} [common] {% if max_concurrent_sessions is vyos_defined %} max-starting={{ max_concurrent_sessions }} {% endif %} [log] syslog=accel-ipoe,daemon copy=1 {% if log.level is vyos_defined %} level={{ log.level }} {% endif %} [ipoe] verbose=1 {% if interface is vyos_defined %} {% for iface, iface_config in interface.items() %} {% set tmp = 'interface=' %} {% if iface_config.vlan is vyos_defined %} {% set tmp = tmp ~ 're:^' ~ iface ~ '\.' ~ iface_config.vlan | range_to_regex ~ '$' %} {% else %} {% set tmp = tmp ~ iface %} {% endif %} {% set shared = '' %} {% if iface_config.network is vyos_defined('shared') %} {% set shared = 'shared=1,' %} {% elif iface_config.network is vyos_defined('vlan') %} {% set shared = 'shared=0,' %} {% endif %} {% set range = 'range=' ~ iface_config.client_subnet ~ ',' if iface_config.client_subnet is vyos_defined else '' %} {% set relay = ',' ~ 'relay=' ~ iface_config.external_dhcp.dhcp_relay if iface_config.external_dhcp.dhcp_relay is vyos_defined else '' %} {% set giaddr = ',' ~ 'giaddr=' ~ iface_config.external_dhcp.giaddr if iface_config.external_dhcp.giaddr is vyos_defined else '' %} {{ tmp }},{{ shared }}mode={{ iface_config.mode | upper }},ifcfg=1,{{ range }}start=dhcpv4,ipv6=1{{ relay }}{{ giaddr }} -{% if iface_config.vlan is vyos_defined %} +{% if iface_config.vlan_mon is vyos_defined %} vlan-mon={{ iface }},{{ iface_config.vlan | join(',') }} {% endif %} {% endfor %} {% endif %} {% if authentication.mode is vyos_defined('noauth') %} noauth=1 {% elif authentication.mode is vyos_defined('local') %} username=ifname password=csid {% endif %} {% if default_pool is vyos_defined %} ip-pool={{ default_pool }} {% endif %} {% if default_ipv6_pool is vyos_defined %} ipv6-pool={{ default_ipv6_pool }} ipv6-pool-delegate={{ default_ipv6_pool }} {% endif %} {% if gateway_address is vyos_defined %} {% for gw_addr in gateway_address %} gw-ip-address={{ gw_addr }} {% endfor %} {% endif %} proxy-arp=1 {# Common IP pool definitions #} {% include 'accel-ppp/config_ip_pool.j2' %} {# Common IPv6 pool definitions #} {% include 'accel-ppp/config_ipv6_pool.j2' %} {# Common DNS name-server definition #} {% include 'accel-ppp/config_name_server.j2' %} {# Common chap-secrets and RADIUS server/option definitions #} {% include 'accel-ppp/config_chap_secrets_radius.j2' %} {# Common RADIUS shaper configuration #} {% include 'accel-ppp/config_shaper_radius.j2' %} {# Common Extended scripts configuration #} {% include 'accel-ppp/config_extended_scripts.j2' %} {# Common Limits configuration #} {% include 'accel-ppp/config_limits.j2' %} {# Common SNMP definitions #} {% include 'accel-ppp/config_snmp.j2' %} [cli] tcp=127.0.0.1:2002 diff --git a/data/templates/accel-ppp/pppoe.config.j2 b/data/templates/accel-ppp/pppoe.config.j2 index 6711f2ec9..6a387bd46 100644 --- a/data/templates/accel-ppp/pppoe.config.j2 +++ b/data/templates/accel-ppp/pppoe.config.j2 @@ -1,125 +1,127 @@ ### generated by accel_pppoe.py ### [modules] log_syslog pppoe shaper {# Common authentication backend definitions #} {% include 'accel-ppp/config_modules_auth_mode.j2' %} ippool {# Common IPv6 definitions #} {% include 'accel-ppp/config_modules_ipv6.j2' %} {# Common authentication protocols (pap, chap ...) #} {% include 'accel-ppp/config_modules_auth_protocols.j2' %} {% if snmp is vyos_defined %} net-snmp {% endif %} {% if limits is vyos_defined %} connlimit {% endif %} {% if extended_scripts is vyos_defined %} sigchld pppd_compat {% endif %} [core] thread-count={{ thread_count }} [log] syslog=accel-pppoe,daemon copy=1 {% if log.level is vyos_defined %} level={{ log.level }} {% endif %} {% if authentication.mode is vyos_defined("noauth") %} [auth] noauth=1 {% endif %} [client-ip-range] 0.0.0.0/0 [common] {% if session_control is vyos_defined and session_control is not vyos_defined('disable') %} single-session={{ session_control }} {% endif %} {% if max_concurrent_sessions is vyos_defined %} max-starting={{ max_concurrent_sessions }} {% endif %} [pppoe] verbose=1 ac-name={{ access_concentrator }} {% if interface is vyos_defined %} {% for iface, iface_config in interface.items() %} {% if iface_config.vlan is not vyos_defined %} interface={{ iface }} {% else %} {% for vlan in iface_config.vlan %} interface=re:^{{ iface }}\.{{ vlan | range_to_regex }}$ {% endfor %} +{% if iface_config.vlan_mon is vyos_defined %} vlan-mon={{ iface }},{{ iface_config.vlan | join(',') }} +{% endif %} {% endif %} {% endfor %} {% endif %} {% if service_name %} service-name={{ service_name | join(',') }} {% endif %} {% if pado_delay %} {% set delay_without_sessions = pado_delay.delays_without_sessions[0] | default('0') %} {% set pado_delay_param = namespace(value=delay_without_sessions) %} {% for delay, sessions in pado_delay.delays_with_sessions | sort(attribute='1') %} {% if not delay == 'disable' %} {% set pado_delay_param.value = pado_delay_param.value + ',' + delay + ':' + sessions | string %} {% else %} {% set pado_delay_param.value = pado_delay_param.value + ',-1:' + sessions | string %} {% endif %} {% endfor %} pado-delay={{ pado_delay_param.value }} {% endif %} {% if authentication.radius.called_sid_format is vyos_defined %} called-sid={{ authentication.radius.called_sid_format }} {% endif %} {% if authentication.mode is vyos_defined("noauth") %} noauth=1 {% endif %} {% if default_pool is vyos_defined %} ip-pool={{ default_pool }} {% endif %} {% if default_ipv6_pool is vyos_defined %} ipv6-pool={{ default_ipv6_pool }} ipv6-pool-delegate={{ default_ipv6_pool }} {% endif %} {# Common IP pool definitions #} {% include 'accel-ppp/config_ip_pool.j2' %} {# Common IPv6 pool definitions #} {% include 'accel-ppp/config_ipv6_pool.j2' %} {# Common DNS name-server definition #} {% include 'accel-ppp/config_name_server.j2' %} {# Common wins-server definition #} {% include 'accel-ppp/config_wins_server.j2' %} {# Common chap-secrets and RADIUS server/option definitions #} {% include 'accel-ppp/config_chap_secrets_radius.j2' %} {# Common ppp-options definitions #} {% include 'accel-ppp/ppp-options.j2' %} {# Common RADIUS shaper configuration #} {% include 'accel-ppp/config_shaper_radius.j2' %} {# Common Extended scripts configuration #} {% include 'accel-ppp/config_extended_scripts.j2' %} {# Common Limits configuration #} {% include 'accel-ppp/config_limits.j2' %} {# Common SNMP definitions #} {% include 'accel-ppp/config_snmp.j2' %} [cli] tcp=127.0.0.1:2001 diff --git a/interface-definitions/include/accel-ppp/vlan-mon.xml.i b/interface-definitions/include/accel-ppp/vlan-mon.xml.i new file mode 100644 index 000000000..d5bacb0d1 --- /dev/null +++ b/interface-definitions/include/accel-ppp/vlan-mon.xml.i @@ -0,0 +1,8 @@ +<!-- include start from accel-ppp/vlan-mon.xml.i --> +<leafNode name="vlan-mon"> + <properties> + <help>Automatically create VLAN interfaces</help> + <valueless/> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/version/ipoe-server-version.xml.i b/interface-definitions/include/version/ipoe-server-version.xml.i index 659433382..b7718fc5e 100644 --- a/interface-definitions/include/version/ipoe-server-version.xml.i +++ b/interface-definitions/include/version/ipoe-server-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/ipoe-server-version.xml.i --> -<syntaxVersion component='ipoe-server' version='3'></syntaxVersion> +<syntaxVersion component='ipoe-server' version='4'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/include/version/pppoe-server-version.xml.i b/interface-definitions/include/version/pppoe-server-version.xml.i index 61de1277a..2e020faa3 100644 --- a/interface-definitions/include/version/pppoe-server-version.xml.i +++ b/interface-definitions/include/version/pppoe-server-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/pppoe-server-version.xml.i --> -<syntaxVersion component='pppoe-server' version='10'></syntaxVersion> +<syntaxVersion component='pppoe-server' version='11'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/service_ipoe-server.xml.in b/interface-definitions/service_ipoe-server.xml.in index c7542f0d0..25bc43cc6 100644 --- a/interface-definitions/service_ipoe-server.xml.in +++ b/interface-definitions/service_ipoe-server.xml.in @@ -1,197 +1,198 @@ <?xml version="1.0"?> <interfaceDefinition> <node name="service"> <children> <node name="ipoe-server" owner="${vyos_conf_scripts_dir}/service_ipoe-server.py"> <properties> <help>Internet Protocol over Ethernet (IPoE) Server</help> <priority>900</priority> </properties> <children> <node name="authentication"> <properties> <help>Client authentication methods</help> </properties> <children> #include <include/accel-ppp/auth-mode.xml.i> <tagNode name="interface"> <properties> <help>Network interface for client MAC addresses</help> <completionHelp> <script>${vyos_completion_dir}/list_interfaces</script> </completionHelp> </properties> <children> <tagNode name="mac"> <properties> <help>Media Access Control (MAC) address</help> <valueHelp> <format>macaddr</format> <description>Hardware (MAC) address</description> </valueHelp> <constraint> <validator name="mac-address"/> </constraint> </properties> <children> <node name="rate-limit"> <properties> <help>Upload/Download speed limits</help> </properties> <children> <leafNode name="upload"> <properties> <help>Upload bandwidth limit in kbits/sec</help> <constraint> <validator name="numeric" argument="--range 1-4294967295"/> </constraint> </properties> </leafNode> <leafNode name="download"> <properties> <help>Download bandwidth limit in kbits/sec</help> <constraint> <validator name="numeric" argument="--range 1-4294967295"/> </constraint> </properties> </leafNode> </children> </node> <leafNode name="vlan"> <properties> <help>VLAN monitor for automatic creation of VLAN interfaces</help> <valueHelp> <format>u32:1-4094</format> <description>Client VLAN id</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-4094"/> </constraint> <constraintErrorMessage>VLAN IDs need to be in range 1-4094</constraintErrorMessage> </properties> </leafNode> </children> </tagNode> </children> </tagNode> #include <include/radius-auth-server-ipv4.xml.i> #include <include/accel-ppp/radius-additions.xml.i> <node name="radius"> <children> #include <include/accel-ppp/radius-additions-rate-limit.xml.i> </children> </node> </children> </node> <tagNode name="interface"> <properties> <help>Interface to listen dhcp or unclassified packets</help> <completionHelp> <script>${vyos_completion_dir}/list_interfaces</script> </completionHelp> </properties> <children> <leafNode name="mode"> <properties> <help>Client connectivity mode</help> <completionHelp> <list>l2 l3</list> </completionHelp> <valueHelp> <format>l2</format> <description>Client located on same interface as server</description> </valueHelp> <valueHelp> <format>l3</format> <description>Client located behind a router</description> </valueHelp> <constraint> <regex>(l2|l3)</regex> </constraint> </properties> <defaultValue>l2</defaultValue> </leafNode> <leafNode name="network"> <properties> <help>Enables clients to share the same network or each client has its own vlan</help> <completionHelp> <list>shared vlan</list> </completionHelp> <constraint> <regex>(shared|vlan)</regex> </constraint> <valueHelp> <format>shared</format> <description>Multiple clients share the same network</description> </valueHelp> <valueHelp> <format>vlan</format> <description>One VLAN per client</description> </valueHelp> </properties> <defaultValue>shared</defaultValue> </leafNode> <leafNode name="client-subnet"> <properties> <help>Client address pool</help> <valueHelp> <format>ipv4net</format> <description>IPv4 address and prefix length</description> </valueHelp> <constraint> <validator name="ipv4-prefix"/> </constraint> </properties> </leafNode> <node name="external-dhcp"> <properties> <help>DHCP requests will be forwarded</help> </properties> <children> <leafNode name="dhcp-relay"> <properties> <help>DHCP Server the request will be redirected to.</help> <valueHelp> <format>ipv4</format> <description>IPv4 address of the DHCP Server</description> </valueHelp> <constraint> <validator name="ipv4-address"/> </constraint> </properties> </leafNode> <leafNode name="giaddr"> <properties> <help>Relay Agent IPv4 Address</help> <valueHelp> <format>ipv4</format> <description>Gateway IP address</description> </valueHelp> <constraint> <validator name="ipv4-address"/> </constraint> </properties> </leafNode> </children> </node> #include <include/accel-ppp/vlan.xml.i> + #include <include/accel-ppp/vlan-mon.xml.i> </children> </tagNode> #include <include/accel-ppp/client-ip-pool.xml.i> #include <include/accel-ppp/client-ipv6-pool.xml.i> #include <include/accel-ppp/default-pool.xml.i> #include <include/accel-ppp/default-ipv6-pool.xml.i> #include <include/accel-ppp/extended-scripts.xml.i> #include <include/accel-ppp/gateway-address-multi.xml.i> #include <include/accel-ppp/limits.xml.i> #include <include/accel-ppp/max-concurrent-sessions.xml.i> #include <include/accel-ppp/shaper.xml.i> #include <include/accel-ppp/snmp.xml.i> #include <include/generic-description.xml.i> #include <include/name-server-ipv4-ipv6.xml.i> #include <include/accel-ppp/log.xml.i> </children> </node> </children> </node> </interfaceDefinition> diff --git a/interface-definitions/service_pppoe-server.xml.in b/interface-definitions/service_pppoe-server.xml.in index 81228938f..31562e278 100644 --- a/interface-definitions/service_pppoe-server.xml.in +++ b/interface-definitions/service_pppoe-server.xml.in @@ -1,161 +1,162 @@ <?xml version="1.0"?> <interfaceDefinition> <node name="service"> <children> <node name="pppoe-server" owner="${vyos_conf_scripts_dir}/service_pppoe-server.py"> <properties> <help>Point to Point over Ethernet (PPPoE) Server</help> <priority>900</priority> </properties> <children> #include <include/pppoe-access-concentrator.xml.i> <leafNode name="access-concentrator"> <defaultValue>vyos-ac</defaultValue> </leafNode> <node name="authentication"> <properties> <help>Authentication for remote access PPPoE Server</help> </properties> <children> #include <include/accel-ppp/auth-local-users.xml.i> #include <include/accel-ppp/auth-mode.xml.i> #include <include/accel-ppp/auth-protocols.xml.i> #include <include/radius-auth-server-ipv4.xml.i> #include <include/accel-ppp/radius-additions.xml.i> <node name="radius"> <children> #include <include/accel-ppp/radius-additions-rate-limit.xml.i> <leafNode name="called-sid-format"> <properties> <help>Format of Called-Station-Id attribute</help> <completionHelp> <list>ifname ifname:mac</list> </completionHelp> <constraint> <regex>(ifname|ifname:mac)</regex> </constraint> <constraintErrorMessage>Invalid Called-Station-Id format</constraintErrorMessage> <valueHelp> <format>ifname</format> <description>NAS-Port-Id - should contain root interface name (NAS-Port-Id=eth1)</description> </valueHelp> <valueHelp> <format>ifname:mac</format> <description>NAS-Port-Id - should contain root interface name and mac address (NAS-Port-Id=eth1:00:00:00:00:00:00)</description> </valueHelp> </properties> </leafNode> </children> </node> </children> </node> <tagNode name="interface"> <properties> <help>interface(s) to listen on</help> <completionHelp> <script>${vyos_completion_dir}/list_interfaces</script> </completionHelp> </properties> <children> #include <include/accel-ppp/vlan.xml.i> + #include <include/accel-ppp/vlan-mon.xml.i> </children> </tagNode> <leafNode name="service-name"> <properties> <help>Service name</help> <constraint> <regex>[a-zA-Z0-9\-]{1,100}</regex> </constraint> <constraintErrorMessage>Service-name can contain aplhanumerical characters and dashes only (max. 100)</constraintErrorMessage> <multi/> </properties> </leafNode> <tagNode name="pado-delay"> <properties> <help>PADO delays</help> <valueHelp> <format>disable</format> <description>Disable new connections</description> </valueHelp> <completionHelp> <list>disable</list> </completionHelp> <valueHelp> <format>u32:1-999999</format> <description>Number in ms</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-999999"/> <regex>disable</regex> </constraint> <constraintErrorMessage>Invalid PADO delay</constraintErrorMessage> </properties> <children> <leafNode name="sessions"> <properties> <help>Number of sessions</help> <valueHelp> <format>u32:1-999999</format> <description>Number of sessions</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-999999"/> </constraint> <constraintErrorMessage>Invalid number of delayed sessions</constraintErrorMessage> </properties> </leafNode> </children> </tagNode> <leafNode name="session-control"> <properties> <help>control sessions count</help> <constraint> <regex>(deny|disable|replace)</regex> </constraint> <constraintErrorMessage>Invalid value</constraintErrorMessage> <valueHelp> <format>disable</format> <description>Disables session control</description> </valueHelp> <valueHelp> <format>deny</format> <description>Deny second session authorization</description> </valueHelp> <valueHelp> <format>replace</format> <description>Terminate first session when second is authorized</description> </valueHelp> <completionHelp> <list>deny disable replace</list> </completionHelp> </properties> <defaultValue>replace</defaultValue> </leafNode> #include <include/accel-ppp/client-ip-pool.xml.i> #include <include/accel-ppp/client-ipv6-pool.xml.i> #include <include/accel-ppp/default-pool.xml.i> #include <include/accel-ppp/default-ipv6-pool.xml.i> #include <include/accel-ppp/extended-scripts.xml.i> #include <include/accel-ppp/gateway-address.xml.i> #include <include/accel-ppp/limits.xml.i> #include <include/accel-ppp/max-concurrent-sessions.xml.i> #include <include/accel-ppp/mtu-128-16384.xml.i> #include <include/accel-ppp/ppp-options.xml.i> <node name="ppp-options"> <children> <leafNode name="min-mtu"> <defaultValue>1280</defaultValue> </leafNode> </children> </node> #include <include/accel-ppp/shaper.xml.i> #include <include/accel-ppp/snmp.xml.i> #include <include/accel-ppp/wins-server.xml.i> #include <include/generic-description.xml.i> #include <include/name-server-ipv4-ipv6.xml.i> #include <include/accel-ppp/log.xml.i> </children> </node> </children> </node> </interfaceDefinition> diff --git a/smoketest/scripts/cli/test_service_ipoe-server.py b/smoketest/scripts/cli/test_service_ipoe-server.py index 5f1cf9ad1..be03179bf 100755 --- a/smoketest/scripts/cli/test_service_ipoe-server.py +++ b/smoketest/scripts/cli/test_service_ipoe-server.py @@ -1,240 +1,272 @@ #!/usr/bin/env python3 # # Copyright (C) 2022-2024 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 re import unittest from collections import OrderedDict from base_accel_ppp_test import BasicAccelPPPTest from vyos.configsession import ConfigSessionError from vyos.utils.process import cmd +from vyos.template import range_to_regex from configparser import ConfigParser from configparser import RawConfigParser ac_name = "ACN" interface = "eth0" class MultiOrderedDict(OrderedDict): # Accel-ppp has duplicate keys in config file (gw-ip-address) # This class is used to define dictionary which can contain multiple values # in one key. def __setitem__(self, key, value): if isinstance(value, list) and key in self: self[key].extend(value) else: super(OrderedDict, self).__setitem__(key, value) class TestServiceIPoEServer(BasicAccelPPPTest.TestCase): @classmethod def setUpClass(cls): cls._base_path = ["service", "ipoe-server"] cls._config_file = "/run/accel-pppd/ipoe.conf" cls._chap_secrets = "/run/accel-pppd/ipoe.chap-secrets" cls._protocol_section = "ipoe" # call base-classes classmethod super(TestServiceIPoEServer, cls).setUpClass() def verify(self, conf): super().verify(conf) # Validate configuration values accel_modules = list(conf["modules"].keys()) self.assertIn("log_syslog", accel_modules) self.assertIn("ipoe", accel_modules) self.assertIn("shaper", accel_modules) self.assertIn("ipv6pool", accel_modules) self.assertIn("ipv6_nd", accel_modules) self.assertIn("ipv6_dhcp", accel_modules) self.assertIn("ippool", accel_modules) def initial_gateway_config(self): self._gateway = "192.0.2.1/24" super().initial_gateway_config() def initial_auth_config(self): self.set(["authentication", "mode", "noauth"]) def basic_protocol_specific_config(self): self.set(["interface", interface, "client-subnet", "192.168.0.0/24"]) def test_accel_local_authentication(self): mac_address = "08:00:27:2f:d8:06" self.set(["authentication", "interface", interface, "mac", mac_address]) self.set(["authentication", "mode", "local"]) # No IPoE interface configured with self.assertRaises(ConfigSessionError): self.cli_commit() # Test configuration of local authentication for PPPoE server self.basic_config() # Rewrite authentication from basic_config self.set(["authentication", "interface", interface, "mac", mac_address]) self.set(["authentication", "mode", "local"]) # commit changes self.cli_commit() # Validate configuration values conf = ConfigParser(allow_no_value=True, delimiters="=", strict=False) conf.read(self._config_file) # check proper path to chap-secrets file self.assertEqual(conf["chap-secrets"]["chap-secrets"], self._chap_secrets) accel_modules = list(conf["modules"].keys()) self.assertIn("chap-secrets", accel_modules) # basic verification self.verify(conf) # check local users tmp = cmd(f"sudo cat {self._chap_secrets}") regex = f"{interface}\s+\*\s+{mac_address}\s+\*" tmp = re.findall(regex, tmp) self.assertTrue(tmp) def test_accel_ipv4_pool(self): self.basic_config(is_gateway=False, is_client_pool=False) gateway = ["172.16.0.1/25", "192.0.2.1/24"] subnet = "172.16.0.0/24" first_pool = "POOL1" second_pool = "POOL2" range = "192.0.2.10-192.0.2.20" range_config = "192.0.2.10-20" for gw in gateway: self.set(["gateway-address", gw]) self.set(["client-ip-pool", first_pool, "range", subnet]) self.set(["client-ip-pool", first_pool, "next-pool", second_pool]) self.set(["client-ip-pool", second_pool, "range", range]) self.set(["default-pool", first_pool]) # commit changes self.cli_commit() # Validate configuration values conf = RawConfigParser( allow_no_value=True, delimiters="=", strict=False, dict_type=MultiOrderedDict, ) conf.read(self._config_file) self.assertIn( f"{first_pool},next={second_pool}", conf["ip-pool"][f"{subnet},name"] ) self.assertIn(second_pool, conf["ip-pool"][f"{range_config},name"]) gw_pool_config_list = conf.get("ip-pool", "gw-ip-address") gw_ipoe_config_list = conf.get(self._protocol_section, "gw-ip-address") for gw in gateway: self.assertIn(gw.split("/")[0], gw_pool_config_list) self.assertIn(gw, gw_ipoe_config_list) self.assertIn(first_pool, conf[self._protocol_section]["ip-pool"]) def test_accel_next_pool(self): self.basic_config(is_gateway=False, is_client_pool=False) first_pool = "VyOS-pool1" first_subnet = "192.0.2.0/25" first_gateway = "192.0.2.1/24" second_pool = "Vyos-pool2" second_subnet = "203.0.113.0/25" second_gateway = "203.0.113.1/24" third_pool = "Vyos-pool3" third_subnet = "198.51.100.0/24" third_gateway = "198.51.100.1/24" self.set(["gateway-address", f"{first_gateway}"]) self.set(["gateway-address", f"{second_gateway}"]) self.set(["gateway-address", f"{third_gateway}"]) self.set(["client-ip-pool", first_pool, "range", first_subnet]) self.set(["client-ip-pool", first_pool, "next-pool", second_pool]) self.set(["client-ip-pool", second_pool, "range", second_subnet]) self.set(["client-ip-pool", second_pool, "next-pool", third_pool]) self.set(["client-ip-pool", third_pool, "range", third_subnet]) # commit changes self.cli_commit() config = self.getConfig("ip-pool") # T5099 required specific order pool_config = f"""gw-ip-address={first_gateway.split('/')[0]} gw-ip-address={second_gateway.split('/')[0]} gw-ip-address={third_gateway.split('/')[0]} {third_subnet},name={third_pool} {second_subnet},name={second_pool},next={third_pool} {first_subnet},name={first_pool},next={second_pool}""" self.assertIn(pool_config, config) def test_accel_ipv6_pool(self): # Test configuration of IPv6 client pools self.basic_config(is_gateway=False, is_client_pool=False) pool_name = 'ipv6_test_pool' prefix_1 = '2001:db8:fffe::/56' prefix_mask = '64' prefix_2 = '2001:db8:ffff::/56' client_prefix_1 = f'{prefix_1},{prefix_mask}' client_prefix_2 = f'{prefix_2},{prefix_mask}' self.set(['client-ipv6-pool', pool_name, 'prefix', prefix_1, 'mask', prefix_mask]) self.set(['client-ipv6-pool', pool_name, 'prefix', prefix_2, 'mask', prefix_mask]) delegate_1_prefix = '2001:db8:fff1::/56' delegate_2_prefix = '2001:db8:fff2::/56' delegate_mask = '64' self.set(['client-ipv6-pool', pool_name, 'delegate', delegate_1_prefix, 'delegation-prefix', delegate_mask]) self.set(['client-ipv6-pool', pool_name, 'delegate', delegate_2_prefix, 'delegation-prefix', delegate_mask]) # commit changes self.cli_commit() # Validate configuration values conf = ConfigParser(allow_no_value=True, delimiters='=', strict=False) conf.read(self._config_file) for tmp in ['ipv6pool', 'ipv6_nd', 'ipv6_dhcp']: self.assertEqual(conf['modules'][tmp], None) config = self.getConfig("ipv6-pool") pool_config = f"""{client_prefix_1},name={pool_name} {client_prefix_2},name={pool_name} delegate={delegate_1_prefix},{delegate_mask},name={pool_name} delegate={delegate_2_prefix},{delegate_mask},name={pool_name}""" self.assertIn(pool_config, config) + def test_ipoe_server_vlan(self): + vlans = ['100', '200', '300-310'] + + # Test configuration of local authentication for PPPoE server + self.basic_config() + # cannot use "client-subnet" option with "vlan" option + # have to delete it + self.delete(['interface', interface, 'client-subnet']) + self.cli_commit() + + self.set(['interface', interface, 'vlan-mon']) + + # cannot use option "vlan-mon" if no "vlan" set + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + for vlan in vlans: + self.set(['interface', interface, 'vlan', vlan]) + + # commit changes + self.cli_commit() + + # Validate configuration values + conf = ConfigParser(allow_no_value=True, delimiters='=', strict=False) + conf.read(self._config_file) + tmp = range_to_regex(vlans) + self.assertIn(f're:^{interface}\.{tmp}$', conf['ipoe']['interface']) + + tmp = ','.join(vlans) + self.assertIn(f'{interface},{tmp}', conf['ipoe']['vlan-mon']) + @unittest.skip("PPP is not a part of IPoE") def test_accel_ppp_options(self): pass @unittest.skip("WINS server is not used in IPoE") def test_accel_wins_server(self): pass if __name__ == "__main__": unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_pppoe-server.py b/smoketest/scripts/cli/test_service_pppoe-server.py index 97c63d4cb..3cc1b08e0 100755 --- a/smoketest/scripts/cli/test_service_pppoe-server.py +++ b/smoketest/scripts/cli/test_service_pppoe-server.py @@ -1,182 +1,189 @@ #!/usr/bin/env python3 # # Copyright (C) 2022-2024 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_accel_ppp_test import BasicAccelPPPTest from configparser import ConfigParser from vyos.utils.file import read_file from vyos.template import range_to_regex +from vyos.configsession import ConfigSessionError local_if = ['interfaces', 'dummy', 'dum667'] ac_name = 'ACN' interface = 'eth0' class TestServicePPPoEServer(BasicAccelPPPTest.TestCase): @classmethod def setUpClass(cls): cls._base_path = ['service', 'pppoe-server'] cls._config_file = '/run/accel-pppd/pppoe.conf' cls._chap_secrets = '/run/accel-pppd/pppoe.chap-secrets' cls._protocol_section = 'pppoe' # call base-classes classmethod super(TestServicePPPoEServer, cls).setUpClass() def tearDown(self): self.cli_delete(local_if) super().tearDown() def verify(self, conf): mtu = '1492' # validate some common values in the configuration for tmp in ['log_syslog', 'pppoe', 'ippool', 'auth_mschap_v2', 'auth_mschap_v1', 'auth_chap_md5', 'auth_pap', 'shaper']: # Settings without values provide None self.assertEqual(conf['modules'][tmp], None) # check Access Concentrator setting self.assertTrue(conf['pppoe']['ac-name'] == ac_name) self.assertTrue(conf['pppoe'].getboolean('verbose')) self.assertTrue(conf['pppoe']['interface'], interface) # check ppp self.assertTrue(conf['ppp'].getboolean('verbose')) self.assertTrue(conf['ppp'].getboolean('check-ip')) self.assertEqual(conf['ppp']['mtu'], mtu) super().verify(conf) def basic_protocol_specific_config(self): self.cli_set(local_if + ['address', '192.0.2.1/32']) self.set(['access-concentrator', ac_name]) self.set(['interface', interface]) def test_pppoe_limits(self): self.basic_config() self.set(['limits', 'connection-limit', '20/min']) self.cli_commit() conf = ConfigParser(allow_no_value=True, delimiters='=') conf.read(self._config_file) self.assertEqual(conf['connlimit']['limit'], '20/min') def test_pppoe_server_authentication_protocols(self): # Test configuration of local authentication for PPPoE server self.basic_config() # explicitly test mschap-v2 - no special reason self.set( ['authentication', 'protocols', 'mschap-v2']) # commit changes self.cli_commit() # Validate configuration values conf = ConfigParser(allow_no_value=True) conf.read(self._config_file) self.assertEqual(conf['modules']['auth_mschap_v2'], None) def test_pppoe_server_shaper(self): fwmark = '223' limiter = 'tbf' self.basic_config() self.set(['shaper', 'fwmark', fwmark]) # commit changes self.cli_commit() # Validate configuration values conf = ConfigParser(allow_no_value=True, delimiters='=') conf.read(self._config_file) # basic verification self.verify(conf) self.assertEqual(conf['shaper']['fwmark'], fwmark) self.assertEqual(conf['shaper']['down-limiter'], limiter) def test_accel_radius_authentication(self): radius_called_sid = 'ifname:mac' self.set(['authentication', 'radius', 'called-sid-format', radius_called_sid]) # run common tests super().test_accel_radius_authentication() # Validate configuration values conf = ConfigParser(allow_no_value=True, delimiters='=') conf.read(self._config_file) # Validate configuration self.assertEqual(conf['pppoe']['called-sid'], radius_called_sid) def test_pppoe_server_vlan(self): vlans = ['100', '200', '300-310'] # Test configuration of local authentication for PPPoE server self.basic_config() + self.set(['interface', interface, 'vlan-mon']) + + # cannot use option "vlan-mon" if no "vlan" set + with self.assertRaises(ConfigSessionError): + self.cli_commit() + for vlan in vlans: self.set(['interface', interface, 'vlan', vlan]) # commit changes self.cli_commit() # Validate configuration values config = read_file(self._config_file) for vlan in vlans: tmp = range_to_regex(vlan) self.assertIn(f'interface=re:^{interface}\.{tmp}$', config) tmp = ','.join(vlans) self.assertIn(f'vlan-mon={interface},{tmp}', config) def test_pppoe_server_pado_delay(self): delay_without_sessions = '10' delays = {'20': '200', '30': '300'} self.basic_config() self.set(['pado-delay', delay_without_sessions]) self.cli_commit() conf = ConfigParser(allow_no_value=True, delimiters='=') conf.read(self._config_file) self.assertEqual(conf['pppoe']['pado-delay'], delay_without_sessions) for delay, sessions in delays.items(): self.set(['pado-delay', delay, 'sessions', sessions]) self.cli_commit() conf = ConfigParser(allow_no_value=True, delimiters='=') conf.read(self._config_file) self.assertEqual(conf['pppoe']['pado-delay'], '10,20:200,30:300') self.set(['pado-delay', 'disable', 'sessions', '400']) self.cli_commit() conf = ConfigParser(allow_no_value=True, delimiters='=') conf.read(self._config_file) self.assertEqual(conf['pppoe']['pado-delay'], '10,20:200,30:300,-1:400') if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/src/conf_mode/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py index 16c82e591..c7e3ef033 100755 --- a/src/conf_mode/service_ipoe-server.py +++ b/src/conf_mode/service_ipoe-server.py @@ -1,114 +1,116 @@ #!/usr/bin/env python3 # # Copyright (C) 2018-2024 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.configdict import get_accel_dict from vyos.configverify import verify_interface_exists from vyos.template import render from vyos.utils.process import call from vyos.utils.dict import dict_search from vyos.accel_ppp_util import get_pools_in_order from vyos.accel_ppp_util import verify_accel_ppp_name_servers from vyos.accel_ppp_util import verify_accel_ppp_wins_servers from vyos.accel_ppp_util import verify_accel_ppp_ip_pool from vyos.accel_ppp_util import verify_accel_ppp_authentication from vyos import ConfigError from vyos import airbag airbag.enable() ipoe_conf = '/run/accel-pppd/ipoe.conf' ipoe_chap_secrets = '/run/accel-pppd/ipoe.chap-secrets' def get_config(config=None): if config: conf = config else: conf = Config() base = ['service', 'ipoe-server'] if not conf.exists(base): return None # retrieve common dictionary keys ipoe = get_accel_dict(conf, base, ipoe_chap_secrets) if dict_search('client_ip_pool', ipoe): # Multiple named pools require ordered values T5099 ipoe['ordered_named_pools'] = get_pools_in_order(dict_search('client_ip_pool', ipoe)) ipoe['server_type'] = 'ipoe' return ipoe def verify(ipoe): if not ipoe: return None if 'interface' not in ipoe: raise ConfigError('No IPoE interface configured') for interface, iface_config in ipoe['interface'].items(): verify_interface_exists(ipoe, interface, warning_only=True) if 'client_subnet' in iface_config and 'vlan' in iface_config: raise ConfigError('Option "client-subnet" and "vlan" are mutually exclusive, ' 'use "client-ip-pool" instead!') + if 'vlan_mon' in iface_config and not 'vlan' in iface_config: + raise ConfigError('Option "vlan-mon" requires "vlan" to be set!') verify_accel_ppp_authentication(ipoe, local_users=False) verify_accel_ppp_ip_pool(ipoe) verify_accel_ppp_name_servers(ipoe) verify_accel_ppp_wins_servers(ipoe) return None def generate(ipoe): if not ipoe: return None render(ipoe_conf, 'accel-ppp/ipoe.config.j2', ipoe) if dict_search('authentication.mode', ipoe) == 'local': render(ipoe_chap_secrets, 'accel-ppp/chap-secrets.ipoe.j2', ipoe, permission=0o640) return None def apply(ipoe): systemd_service = 'accel-ppp@ipoe.service' if ipoe == None: call(f'systemctl stop {systemd_service}') for file in [ipoe_conf, ipoe_chap_secrets]: if os.path.exists(file): os.unlink(file) return None call(f'systemctl reload-or-restart {systemd_service}') 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/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py index 566a7b149..ac697c509 100755 --- a/src/conf_mode/service_pppoe-server.py +++ b/src/conf_mode/service_pppoe-server.py @@ -1,164 +1,167 @@ #!/usr/bin/env python3 # # Copyright (C) 2018-2024 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.configdict import get_accel_dict from vyos.configdict import is_node_changed from vyos.configverify import verify_interface_exists from vyos.template import render from vyos.utils.process import call from vyos.utils.dict import dict_search from vyos.accel_ppp_util import verify_accel_ppp_name_servers from vyos.accel_ppp_util import verify_accel_ppp_wins_servers from vyos.accel_ppp_util import verify_accel_ppp_authentication from vyos.accel_ppp_util import verify_accel_ppp_ip_pool from vyos.accel_ppp_util import get_pools_in_order from vyos import ConfigError from vyos import airbag airbag.enable() pppoe_conf = r'/run/accel-pppd/pppoe.conf' pppoe_chap_secrets = r'/run/accel-pppd/pppoe.chap-secrets' def convert_pado_delay(pado_delay): new_pado_delay = {'delays_without_sessions': [], 'delays_with_sessions': []} for delay, sessions in pado_delay.items(): if not sessions: new_pado_delay['delays_without_sessions'].append(delay) else: new_pado_delay['delays_with_sessions'].append((delay, int(sessions['sessions']))) return new_pado_delay def get_config(config=None): if config: conf = config else: conf = Config() base = ['service', 'pppoe-server'] if not conf.exists(base): return None # retrieve common dictionary keys pppoe = get_accel_dict(conf, base, pppoe_chap_secrets) if dict_search('client_ip_pool', pppoe): # Multiple named pools require ordered values T5099 pppoe['ordered_named_pools'] = get_pools_in_order(dict_search('client_ip_pool', pppoe)) if dict_search('pado_delay', pppoe): pado_delay = dict_search('pado_delay', pppoe) pppoe['pado_delay'] = convert_pado_delay(pado_delay) # reload-or-restart does not implemented in accel-ppp # use this workaround until it will be implemented # https://phabricator.accel-ppp.org/T3 conditions = [is_node_changed(conf, base + ['client-ip-pool']), is_node_changed(conf, base + ['client-ipv6-pool']), is_node_changed(conf, base + ['interface'])] if any(conditions): pppoe.update({'restart_required': {}}) pppoe['server_type'] = 'pppoe' return pppoe def verify_pado_delay(pppoe): if 'pado_delay' in pppoe: pado_delay = pppoe['pado_delay'] delays_without_sessions = pado_delay['delays_without_sessions'] if 'disable' in delays_without_sessions: raise ConfigError( 'Number of sessions must be specified for "pado-delay disable"' ) if len(delays_without_sessions) > 1: raise ConfigError( f'Cannot add more then ONE pado-delay without sessions, ' f'but {len(delays_without_sessions)} were set' ) if 'disable' in [delay[0] for delay in pado_delay['delays_with_sessions']]: # need to sort delays by sessions to verify if there is no delay # for sessions after disabling sorted_pado_delay = sorted(pado_delay['delays_with_sessions'], key=lambda k_v: k_v[1]) last_delay = sorted_pado_delay[-1] if last_delay[0] != 'disable': raise ConfigError( f'Cannot add pado-delay after disabled sessions, but ' f'"pado-delay {last_delay[0]} sessions {last_delay[1]}" was set' ) def verify(pppoe): if not pppoe: return None verify_accel_ppp_authentication(pppoe) verify_accel_ppp_ip_pool(pppoe) verify_accel_ppp_name_servers(pppoe) verify_accel_ppp_wins_servers(pppoe) verify_pado_delay(pppoe) if 'interface' not in pppoe: raise ConfigError('At least one listen interface must be defined!') # Check is interface exists in the system - for interface in pppoe['interface']: + for interface, interface_config in pppoe['interface'].items(): verify_interface_exists(pppoe, interface, warning_only=True) + if 'vlan_mon' in interface_config and not 'vlan' in interface_config: + raise ConfigError('Option "vlan-mon" requires "vlan" to be set!') + return None def generate(pppoe): if not pppoe: return None render(pppoe_conf, 'accel-ppp/pppoe.config.j2', pppoe) if dict_search('authentication.mode', pppoe) == 'local': render(pppoe_chap_secrets, 'accel-ppp/chap-secrets.config_dict.j2', pppoe, permission=0o640) return None def apply(pppoe): systemd_service = 'accel-ppp@pppoe.service' if not pppoe: call(f'systemctl stop {systemd_service}') for file in [pppoe_conf, pppoe_chap_secrets]: if os.path.exists(file): os.unlink(file) return None if 'restart_required' in pppoe: call(f'systemctl restart {systemd_service}') else: call(f'systemctl reload-or-restart {systemd_service}') 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/ipoe-server/3-to-4 b/src/migration-scripts/ipoe-server/3-to-4 new file mode 100644 index 000000000..3bad9756d --- /dev/null +++ b/src/migration-scripts/ipoe-server/3-to-4 @@ -0,0 +1,30 @@ +# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. + +# Add the "vlan-mon" option to the configuration to prevent it +# from disappearing from the configuration file + +from vyos.configtree import ConfigTree + +base = ['service', 'ipoe-server'] + +def migrate(config: ConfigTree) -> None: + if not config.exists(base): + return + + for interface in config.list_nodes(base + ['interface']): + base_path = base + ['interface', interface] + if config.exists(base_path + ['vlan']): + config.set(base_path + ['vlan-mon']) diff --git a/src/migration-scripts/pppoe-server/10-to-11 b/src/migration-scripts/pppoe-server/10-to-11 new file mode 100644 index 000000000..6bc138b5c --- /dev/null +++ b/src/migration-scripts/pppoe-server/10-to-11 @@ -0,0 +1,30 @@ +# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. + +# Add the "vlan-mon" option to the configuration to prevent it +# from disappearing from the configuration file + +from vyos.configtree import ConfigTree + +base = ['service', 'pppoe-server'] + +def migrate(config: ConfigTree) -> None: + if not config.exists(base): + return + + for interface in config.list_nodes(base + ['interface']): + base_path = base + ['interface', interface] + if config.exists(base_path + ['vlan']): + config.set(base_path + ['vlan-mon'])