diff --git a/data/templates/pppoe/peer.tmpl b/data/templates/pppoe/peer.tmpl index b681bea77..536d484de 100644 --- a/data/templates/pppoe/peer.tmpl +++ b/data/templates/pppoe/peer.tmpl @@ -1,80 +1,80 @@ ### Autogenerated by interfaces-pppoe.py ### {{ '# ' ~ description if description is defined else '' }} # Require peer to provide the local IP address if it is not # specified explicitly in the config file. noipdefault # Don't show the password in logfiles: hide-password # Standard Link Control Protocol (LCP) parameters: lcp-echo-interval 20 lcp-echo-failure 3 # RFC 2516, paragraph 7 mandates that the following options MUST NOT be # requested and MUST be rejected if requested by the peer: # Address-and-Control-Field-Compression (ACFC) noaccomp # Asynchronous-Control-Character-Map (ACCM) default-asyncmap # Override any connect script that may have been set in /etc/ppp/options. connect /bin/true # Don't try to authenticate the remote node noauth # Don't try to proxy ARP for the remote endpoint. User can set proxy # arp entries up manually if they wish. More importantly, having # the "proxyarp" parameter set disables the "defaultroute" option. noproxyarp # Unlimited connection attempts maxfail 0 plugin rp-pppoe.so {{ source_interface }} {% if access_concentrator is defined and access_concentrator is not none %} rp_pppoe_ac "{{ access_concentrator }}" {% endif %} {% if service_name is defined and service_name is not none %} rp_pppoe_service "{{ service_name }}" {% endif %} {% if host_uniq is defined and host_uniq is not none %} host-uniq "{{ host_uniq }}" {% endif %} persist ifname {{ ifname }} ipparam {{ ifname }} debug mtu {{ mtu }} -mru {{ mtu }} +mru {{ mru }} {% if authentication is defined %} {{ 'user "' + authentication.user + '"' if authentication.user is defined }} {{ 'password "' + authentication.password + '"' if authentication.password is defined }} {% endif %} {{ "usepeerdns" if no_peer_dns is not defined }} {% if ipv6 is defined %} +ipv6 {% if ipv6.address is defined and ipv6.address.autoconf is defined %} ipv6cp-use-ipaddr {% endif %} {% endif %} {% if connect_on_demand is defined %} demand # See T2249. PPP default route options should only be set when in on-demand # mode. As soon as we are not in on-demand mode the default-route handling is # passed to the ip-up.d/ip-down.s scripts which is required for VRF support. {% if 'auto' in default_route %} defaultroute {% elif 'force' in default_route %} defaultroute replacedefaultroute {% endif %} {% endif %} diff --git a/interface-definitions/interfaces-pppoe.xml.in b/interface-definitions/interfaces-pppoe.xml.in index 581a8a59c..5e39ac65d 100644 --- a/interface-definitions/interfaces-pppoe.xml.in +++ b/interface-definitions/interfaces-pppoe.xml.in @@ -1,148 +1,162 @@ <?xml version="1.0"?> <interfaceDefinition> <node name="interfaces"> <children> <tagNode name="pppoe" owner="${vyos_conf_scripts_dir}/interfaces-pppoe.py"> <properties> <help>Point-to-Point Protocol over Ethernet (PPPoE)</help> <priority>322</priority> <constraint> <regex>^pppoe[0-9]+$</regex> </constraint> <constraintErrorMessage>PPPoE interface must be named pppoeN</constraintErrorMessage> <valueHelp> <format>pppoeN</format> <description>PPPoE dialer interface name</description> </valueHelp> </properties> <children> #include <include/pppoe-access-concentrator.xml.i> #include <include/interface/authentication.xml.i> #include <include/interface/dial-on-demand.xml.i> <leafNode name="default-route"> <properties> <help>Default route insertion behaviour (default: auto)</help> <completionHelp> <list>auto none force</list> </completionHelp> <constraint> <regex>^(auto|none|force)$</regex> </constraint> <constraintErrorMessage>PPPoE default-route option must be 'auto', 'none', or 'force'</constraintErrorMessage> <valueHelp> <format>auto</format> <description>Automatically install a default route</description> </valueHelp> <valueHelp> <format>none</format> <description>Do not install a default route</description> </valueHelp> <valueHelp> <format>force</format> <description>Replace existing default route</description> </valueHelp> </properties> <defaultValue>auto</defaultValue> </leafNode> #include <include/interface/dhcpv6-options.xml.i> #include <include/interface/description.xml.i> #include <include/interface/disable.xml.i> #include <include/interface/vrf.xml.i> <leafNode name="idle-timeout"> <properties> <help>Delay before disconnecting idle session (in seconds)</help> <valueHelp> <format>u32:0-86400</format> <description>Idle timeout in seconds</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 0-86400"/> </constraint> <constraintErrorMessage>Timeout must be in range 0 to 86400</constraintErrorMessage> </properties> </leafNode> <leafNode name="host-uniq"> <properties> <help>PPPoE RFC2516 host-uniq tag</help> <valueHelp> <format>txt</format> <description>Host-uniq tag as byte string in HEX</description> </valueHelp> <constraint> <regex>([a-fA-F0-9][a-fA-F0-9]){1,18}</regex> </constraint> <constraintErrorMessage>Host-uniq must be specified as hex-adecimal byte-string (even number of HEX characters)</constraintErrorMessage> </properties> </leafNode> <node name="ip"> <properties> <help>IPv4 routing parameters</help> </properties> <children> #include <include/interface/source-validation.xml.i> </children> </node> <node name="ipv6"> <properties> <help>IPv6 routing parameters</help> </properties> <children> <node name="address"> <properties> <help>IPv6 address configuration modes</help> </properties> <children> #include <include/interface/ipv6-address-autoconf.xml.i> </children> </node> </children> </node> #include <include/source-interface.xml.i> <leafNode name="local-address"> <properties> <help>IPv4 address of local end of the PPPoE link</help> <valueHelp> <format>ipv4</format> <description>Address of local end of the PPPoE link</description> </valueHelp> <constraint> <validator name="ipv4-address"/> </constraint> </properties> </leafNode> #include <include/interface/mtu-68-1500.xml.i> <leafNode name="mtu"> <defaultValue>1492</defaultValue> </leafNode> + <leafNode name="mru"> + <properties> + <help>Maximum Receive Unit (MRU)</help> + <valueHelp> + <format>u32:128-16384</format> + <description>Maximum Receive Unit in byte</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 128-16384"/> + </constraint> + <constraintErrorMessage>MRU must be between 128 and 16384</constraintErrorMessage> + </properties> + <defaultValue>1492</defaultValue> + </leafNode> <leafNode name="no-peer-dns"> <properties> <help>Do not use DNS servers provided by the peer</help> <valueless/> </properties> </leafNode> <leafNode name="remote-address"> <properties> <help>IPv4 address of remote end of the PPPoE link</help> <valueHelp> <format>ipv4</format> <description>Address of remote end of the PPPoE link</description> </valueHelp> <constraint> <validator name="ipv4-address"/> </constraint> </properties> </leafNode> <leafNode name="service-name"> <properties> <help>Service name, only connect to access concentrators advertising this</help> <constraint> <regex>[a-zA-Z0-9]+$</regex> </constraint> <constraintErrorMessage>Service name must be alphanumeric only</constraintErrorMessage> </properties> </leafNode> </children> </tagNode> </children> </node> </interfaceDefinition> diff --git a/smoketest/scripts/cli/test_interfaces_pppoe.py b/smoketest/scripts/cli/test_interfaces_pppoe.py index 8dcac4d7d..2aaccbb13 100755 --- a/smoketest/scripts/cli/test_interfaces_pppoe.py +++ b/smoketest/scripts/cli/test_interfaces_pppoe.py @@ -1,228 +1,237 @@ #!/usr/bin/env python3 # # Copyright (C) 2019-2020 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 psutil import process_iter from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError from vyos.util import read_file config_file = '/etc/ppp/peers/{}' dhcp6c_config_file = '/run/dhcp6c/dhcp6c.{}.conf' base_path = ['interfaces', 'pppoe'] def get_config_value(interface, key): with open(config_file.format(interface), 'r') as f: for line in f: if line.startswith(key): return list(line.split()) return [] def get_dhcp6c_config_value(interface, key): tmp = read_file(dhcp6c_config_file.format(interface)) tmp = re.findall(r'\n?{}\s+(.*)'.format(key), tmp) out = [] for item in tmp: out.append(item.replace(';','')) return out class PPPoEInterfaceTest(VyOSUnitTestSHIM.TestCase): def setUp(self): self._interfaces = ['pppoe10', 'pppoe20', 'pppoe30'] self._source_interface = 'eth0' def tearDown(self): self.cli_delete(base_path) self.cli_commit() def test_pppoe_client(self): # Check if PPPoE dialer can be configured and runs for interface in self._interfaces: user = 'VyOS-user-' + interface passwd = 'VyOS-passwd-' + interface mtu = '1400' + mru = '1300' self.cli_set(base_path + [interface, 'authentication', 'user', user]) self.cli_set(base_path + [interface, 'authentication', 'password', passwd]) self.cli_set(base_path + [interface, 'default-route', 'auto']) self.cli_set(base_path + [interface, 'mtu', mtu]) + self.cli_set(base_path + [interface, 'mru', '9000']) self.cli_set(base_path + [interface, 'no-peer-dns']) # check validate() - a source-interface is required with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_set(base_path + [interface, 'source-interface', self._source_interface]) - # commit changes - self.cli_commit() + # check validate() - MRU needs to be less or equal then MTU + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set(base_path + [interface, 'mru', mru]) + + # commit changes + self.cli_commit() # verify configuration file(s) for interface in self._interfaces: user = 'VyOS-user-' + interface password = 'VyOS-passwd-' + interface tmp = get_config_value(interface, 'mtu')[1] self.assertEqual(tmp, mtu) + tmp = get_config_value(interface, 'mru')[1] + self.assertEqual(tmp, mru) tmp = get_config_value(interface, 'user')[1].replace('"', '') self.assertEqual(tmp, user) tmp = get_config_value(interface, 'password')[1].replace('"', '') self.assertEqual(tmp, password) tmp = get_config_value(interface, 'ifname')[1] self.assertEqual(tmp, interface) # Check if ppp process is running in the interface in question running = False for p in process_iter(): if "pppd" in p.name(): if interface in p.cmdline(): running = True self.assertTrue(running) def test_pppoe_clent_disabled_interface(self): # Check if PPPoE Client can be disabled for interface in self._interfaces: self.cli_set(base_path + [interface, 'authentication', 'user', 'vyos']) self.cli_set(base_path + [interface, 'authentication', 'password', 'vyos']) self.cli_set(base_path + [interface, 'source-interface', self._source_interface]) self.cli_set(base_path + [interface, 'disable']) self.cli_commit() # Validate PPPoE client process running = False for interface in self._interfaces: for proc in process_iter(): if interface in proc.cmdline(): running = True self.assertFalse(running) def test_pppoe_dhcpv6pd(self): # Check if PPPoE dialer can be configured with DHCPv6-PD address = '1' sla_id = '0' sla_len = '8' for interface in self._interfaces: self.cli_set(base_path + [interface, 'authentication', 'user', 'vyos']) self.cli_set(base_path + [interface, 'authentication', 'password', 'vyos']) self.cli_set(base_path + [interface, 'default-route', 'none']) self.cli_set(base_path + [interface, 'no-peer-dns']) self.cli_set(base_path + [interface, 'source-interface', self._source_interface]) self.cli_set(base_path + [interface, 'ipv6', 'address', 'autoconf']) # prefix delegation stuff dhcpv6_pd_base = base_path + [interface, 'dhcpv6-options', 'pd', '0'] self.cli_set(dhcpv6_pd_base + ['length', '56']) self.cli_set(dhcpv6_pd_base + ['interface', self._source_interface, 'address', address]) self.cli_set(dhcpv6_pd_base + ['interface', self._source_interface, 'sla-id', sla_id]) # commit changes self.cli_commit() # verify "normal" PPPoE value - 1492 is default MTU tmp = get_config_value(interface, 'mtu')[1] self.assertEqual(tmp, '1492') tmp = get_config_value(interface, 'user')[1].replace('"', '') self.assertEqual(tmp, 'vyos') tmp = get_config_value(interface, 'password')[1].replace('"', '') self.assertEqual(tmp, 'vyos') for param in ['+ipv6', 'ipv6cp-use-ipaddr']: tmp = get_config_value(interface, param)[0] self.assertEqual(tmp, param) # verify DHCPv6 prefix delegation # will return: ['delegation', '::/56 infinity;'] tmp = get_dhcp6c_config_value(interface, 'prefix')[1].split()[0] # mind the whitespace self.assertEqual(tmp, '::/56') tmp = get_dhcp6c_config_value(interface, 'prefix-interface')[0].split()[0] self.assertEqual(tmp, self._source_interface) tmp = get_dhcp6c_config_value(interface, 'ifid')[0] self.assertEqual(tmp, address) tmp = get_dhcp6c_config_value(interface, 'sla-id')[0] self.assertEqual(tmp, sla_id) tmp = get_dhcp6c_config_value(interface, 'sla-len')[0] self.assertEqual(tmp, sla_len) # Check if ppp process is running in the interface in question running = False for p in process_iter(): if "pppd" in p.name(): running = True self.assertTrue(running) # We can not check if wide-dhcpv6 process is running as it is started # after the PPP interface gets a link to the ISP - but we can see if # it would be started by the scripts tmp = read_file(f'/etc/ppp/ipv6-up.d/1000-vyos-pppoe-{interface}') tmp = re.findall(f'systemctl restart dhcp6c@{interface}.service', tmp) self.assertTrue(tmp) def test_pppoe_authentication(self): # When username or password is set - so must be the other interface = 'pppoe0' self.cli_set(base_path + [interface, 'authentication', 'user', 'vyos']) self.cli_set(base_path + [interface, 'source-interface', self._source_interface]) self.cli_set(base_path + [interface, 'ipv6', 'address', 'autoconf']) # check validate() - if user is set, so must be the password with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_set(base_path + [interface, 'authentication', 'password', 'vyos']) self.cli_commit() def test_pppoe_options(self): # Check if PPPoE dialer can be configured with DHCPv6-PD for interface in self._interfaces: user = f'VyOS-user-{interface}' passwd = f'VyOS-passwd-{interface}' ac_name = f'AC{interface}' service_name = f'SRV{interface}' host_uniq = 'cafebeefBABE123456' self.cli_set(base_path + [interface, 'authentication', 'user', user]) self.cli_set(base_path + [interface, 'authentication', 'password', passwd]) self.cli_set(base_path + [interface, 'source-interface', self._source_interface]) self.cli_set(base_path + [interface, 'access-concentrator', ac_name]) self.cli_set(base_path + [interface, 'service-name', service_name]) self.cli_set(base_path + [interface, 'host-uniq', host_uniq]) # commit changes self.cli_commit() for interface in self._interfaces: ac_name = f'AC{interface}' service_name = f'SRV{interface}' host_uniq = 'cafebeefBABE123456' tmp = get_config_value(interface, 'rp_pppoe_ac')[1] self.assertEqual(tmp, f'"{ac_name}"') tmp = get_config_value(interface, 'rp_pppoe_service')[1] self.assertEqual(tmp, f'"{service_name}"') tmp = get_config_value(interface, 'host-uniq')[1] self.assertEqual(tmp, f'"{host_uniq}"') if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py index 6c4c6c95b..49714c558 100755 --- a/src/conf_mode/interfaces-pppoe.py +++ b/src/conf_mode/interfaces-pppoe.py @@ -1,131 +1,136 @@ #!/usr/bin/env python3 # # Copyright (C) 2019-2020 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 copy import deepcopy from netifaces import interfaces from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configverify import verify_authentication from vyos.configverify import verify_source_interface from vyos.configverify import verify_vrf from vyos.configverify import verify_mtu_ipv6 from vyos.template import render from vyos.util import call from vyos import ConfigError from vyos import airbag airbag.enable() def get_config(config=None): """ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the interface name will be added or a deleted flag """ if config: conf = config else: conf = Config() base = ['interfaces', 'pppoe'] pppoe = get_interface_dict(conf, base) return pppoe def verify(pppoe): if 'deleted' in pppoe: # bail out early return None verify_source_interface(pppoe) verify_authentication(pppoe) verify_vrf(pppoe) verify_mtu_ipv6(pppoe) if {'connect_on_demand', 'vrf'} <= set(pppoe): raise ConfigError('On-demand dialing and VRF can not be used at the same time') + # both MTU and MRU have default values, thus we do not need to check + # if the key exists + if int(pppoe['mru']) > int(pppoe['mtu']): + raise ConfigError('PPPoE MRU needs to be lower then MTU!') + return None def generate(pppoe): # set up configuration file path variables where our templates will be # rendered into ifname = pppoe['ifname'] config_pppoe = f'/etc/ppp/peers/{ifname}' script_pppoe_pre_up = f'/etc/ppp/ip-pre-up.d/1000-vyos-pppoe-{ifname}' script_pppoe_ip_up = f'/etc/ppp/ip-up.d/1000-vyos-pppoe-{ifname}' script_pppoe_ip_down = f'/etc/ppp/ip-down.d/1000-vyos-pppoe-{ifname}' script_pppoe_ipv6_up = f'/etc/ppp/ipv6-up.d/1000-vyos-pppoe-{ifname}' config_wide_dhcp6c = f'/run/dhcp6c/dhcp6c.{ifname}.conf' config_files = [config_pppoe, script_pppoe_pre_up, script_pppoe_ip_up, script_pppoe_ip_down, script_pppoe_ipv6_up, config_wide_dhcp6c] if 'deleted' in pppoe or 'disable' in pppoe: # stop DHCPv6-PD client call(f'systemctl stop dhcp6c@{ifname}.service') # Hang-up PPPoE connection call(f'systemctl stop ppp@{ifname}.service') # Delete PPP configuration files for file in config_files: if os.path.exists(file): os.unlink(file) return None # Create PPP configuration files render(config_pppoe, 'pppoe/peer.tmpl', pppoe, permission=0o755) # Create script for ip-pre-up.d render(script_pppoe_pre_up, 'pppoe/ip-pre-up.script.tmpl', pppoe, permission=0o755) # Create script for ip-up.d render(script_pppoe_ip_up, 'pppoe/ip-up.script.tmpl', pppoe, permission=0o755) # Create script for ip-down.d render(script_pppoe_ip_down, 'pppoe/ip-down.script.tmpl', pppoe, permission=0o755) # Create script for ipv6-up.d render(script_pppoe_ipv6_up, 'pppoe/ipv6-up.script.tmpl', pppoe, permission=0o755) if 'dhcpv6_options' in pppoe and 'pd' in pppoe['dhcpv6_options']: # ipv6.tmpl relies on ifname - this should be made consitent in the # future better then double key-ing the same value render(config_wide_dhcp6c, 'dhcp-client/ipv6.tmpl', pppoe) return None def apply(pppoe): if 'deleted' in pppoe or 'disable' in pppoe: call('systemctl stop ppp@{ifname}.service'.format(**pppoe)) return None call('systemctl restart ppp@{ifname}.service'.format(**pppoe)) return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1)