diff options
52 files changed, 3469 insertions, 2342 deletions
diff --git a/debian/vyos-1x.install b/debian/vyos-1x.install index d8388eecc..5bb7ea507 100644 --- a/debian/vyos-1x.install +++ b/debian/vyos-1x.install @@ -1,5 +1,6 @@ etc/dhcp etc/init.d +etc/ppp etc/rsyslog.d etc/systemd etc/vyos diff --git a/interface-definitions/include/interface-description.xml.i b/interface-definitions/include/interface-description.xml.i index 7a7a37871..961533e26 100644 --- a/interface-definitions/include/interface-description.xml.i +++ b/interface-definitions/include/interface-description.xml.i @@ -1,9 +1,9 @@ <leafNode name="description"> <properties> - <help>Interface description</help> + <help>Interface specific description</help> <constraint> <regex>.{1,256}$</regex> </constraint> - <constraintErrorMessage>Interface description too long (limit 256 characters)</constraintErrorMessage> + <constraintErrorMessage>Description too long (limit 256 characters)</constraintErrorMessage> </properties> </leafNode> diff --git a/interface-definitions/include/interface-vrf.xml.i b/interface-definitions/include/interface-vrf.xml.i new file mode 100644 index 000000000..355e7f0f3 --- /dev/null +++ b/interface-definitions/include/interface-vrf.xml.i @@ -0,0 +1,12 @@ +<leafNode name="vrf"> + <properties> + <help>VRF instance name</help> + <valueHelp> + <format>text</format> + <description>VRF instance name</description> + </valueHelp> + <completionHelp> + <path>vrf name</path> + </completionHelp> + </properties> +</leafNode> diff --git a/interface-definitions/include/radius-server.xml.i b/interface-definitions/include/radius-server.xml.i new file mode 100644 index 000000000..d1068b0e4 --- /dev/null +++ b/interface-definitions/include/radius-server.xml.i @@ -0,0 +1,56 @@ +<node name="radius"> + <properties> + <help>RADIUS based user authentication</help> + </properties> + <children> + <leafNode name="source-address"> + <properties> + <help>RADIUS client source address</help> + <valueHelp> + <format>ipv4</format> + <description>TFTP IPv4 listen address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <tagNode name="server"> + <properties> + <help>RADIUS server configuration</help> + <valueHelp> + <format>ipv4</format> + <description>RADIUS server IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Temporary disable this server</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="key"> + <properties> + <help>Shared secret key</help> + </properties> + </leafNode> + <leafNode name="port"> + <properties> + <help>Authentication port</help> + <valueHelp> + <format>1-65535</format> + <description>Numeric IP port (default: 1812)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> +</node> diff --git a/interface-definitions/include/vif.xml.i b/interface-definitions/include/vif.xml.i index 85e901852..88693e0d3 100644 --- a/interface-definitions/include/vif.xml.i +++ b/interface-definitions/include/vif.xml.i @@ -16,6 +16,7 @@ #include <include/dhcp-dhcpv6-options.xml.i> #include <include/interface-disable-link-detect.xml.i> #include <include/interface-disable.xml.i> + #include <include/interface-vrf.xml.i> <leafNode name="egress-qos"> <properties> <help>VLAN egress QoS</help> diff --git a/interface-definitions/interfaces-bonding.xml.in b/interface-definitions/interfaces-bonding.xml.in index 586a8437d..80943a1fd 100644 --- a/interface-definitions/interfaces-bonding.xml.in +++ b/interface-definitions/interfaces-bonding.xml.in @@ -53,6 +53,7 @@ #include <include/dhcp-dhcpv6-options.xml.i> #include <include/interface-disable-link-detect.xml.i> #include <include/interface-disable.xml.i> + #include <include/interface-vrf.xml.i> <leafNode name="hash-policy"> <properties> <help>Bonding transmit hash policy</help> diff --git a/interface-definitions/interfaces-bridge.xml.in b/interface-definitions/interfaces-bridge.xml.in index e8285b16c..d36a1abbc 100644 --- a/interface-definitions/interfaces-bridge.xml.in +++ b/interface-definitions/interfaces-bridge.xml.in @@ -37,6 +37,7 @@ #include <include/dhcp-dhcpv6-options.xml.i> #include <include/interface-disable-link-detect.xml.i> #include <include/interface-disable.xml.i> + #include <include/interface-vrf.xml.i> <leafNode name="forwarding-delay"> <properties> <help>Forwarding delay</help> diff --git a/interface-definitions/interfaces-dummy.xml.in b/interface-definitions/interfaces-dummy.xml.in index 39809a610..5229e602a 100644 --- a/interface-definitions/interfaces-dummy.xml.in +++ b/interface-definitions/interfaces-dummy.xml.in @@ -19,6 +19,7 @@ #include <include/address-ipv4-ipv6.xml.i> #include <include/interface-description.xml.i> #include <include/interface-disable.xml.i> + #include <include/interface-vrf.xml.i> </children> </tagNode> </children> diff --git a/interface-definitions/interfaces-ethernet.xml.in b/interface-definitions/interfaces-ethernet.xml.in index 8f5d7355b..5728d2f37 100644 --- a/interface-definitions/interfaces-ethernet.xml.in +++ b/interface-definitions/interfaces-ethernet.xml.in @@ -31,6 +31,7 @@ </leafNode> #include <include/interface-disable-link-detect.xml.i> #include <include/interface-disable.xml.i> + #include <include/interface-vrf.xml.i> <leafNode name="duplex"> <properties> <help>Duplex mode</help> diff --git a/interface-definitions/interfaces-pppoe.xml.in b/interface-definitions/interfaces-pppoe.xml.in index b6b54c915..bbaff5f04 100644 --- a/interface-definitions/interfaces-pppoe.xml.in +++ b/interface-definitions/interfaces-pppoe.xml.in @@ -75,6 +75,7 @@ </leafNode> #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> diff --git a/interface-definitions/interfaces-pseudo-ethernet.xml.in b/interface-definitions/interfaces-pseudo-ethernet.xml.in index c2dea438a..e6e8fd20c 100644 --- a/interface-definitions/interfaces-pseudo-ethernet.xml.in +++ b/interface-definitions/interfaces-pseudo-ethernet.xml.in @@ -21,6 +21,7 @@ #include <include/dhcp-dhcpv6-options.xml.i> #include <include/interface-disable-link-detect.xml.i> #include <include/interface-disable.xml.i> + #include <include/interface-vrf.xml.i> <node name="ip"> <children> #include <include/interface-arp-cache-timeout.xml.i> diff --git a/interface-definitions/interfaces-wireless.xml.in b/interface-definitions/interfaces-wireless.xml.in index d6b257978..8632bb881 100644 --- a/interface-definitions/interfaces-wireless.xml.in +++ b/interface-definitions/interfaces-wireless.xml.in @@ -454,6 +454,7 @@ </leafNode> #include <include/interface-disable-link-detect.xml.i> #include <include/interface-disable.xml.i> + #include <include/interface-vrf.xml.i> <leafNode name="expunge-failing-stations"> <properties> <help>Disassociate stations based on excessive transmission failures</help> @@ -666,28 +667,10 @@ <constraintErrorMessage>Invalid WPA pass phrase, must be 8 to 63 printable characters!</constraintErrorMessage> </properties> </leafNode> + #include <include/radius-server.xml.i> <node name="radius"> - <properties> - <help>RADIUS specific configuration</help> - </properties> <children> - <leafNode name="source-address"> - <properties> - <help>RADIUS client forced local IP address</help> - <valueHelp> - <format>ipv4</format> - <description>IPv4 address of RADIUS server</description> - </valueHelp> - </properties> - </leafNode> <tagNode name="server"> - <properties> - <help>IP address of RADIUS server</help> - <valueHelp> - <format>ipv4</format> - <description>IPv4 address of RADIUS server</description> - </valueHelp> - </properties> <children> <leafNode name="accounting"> <properties> @@ -695,23 +678,6 @@ <valueless/> </properties> </leafNode> - <leafNode name="port"> - <properties> - <help>RADIUS server port (default: 1812)</help> - <valueHelp> - <format>1-65535</format> - <description>RADIUS server port</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-65535"/> - </constraint> - </properties> - </leafNode> - <leafNode name="key"> - <properties> - <help>RADIUS shared secret key</help> - </properties> - </leafNode> </children> </tagNode> </children> diff --git a/interface-definitions/system-login.xml.in b/interface-definitions/system-login.xml.in index 3ed85b8d3..2499a192c 100644 --- a/interface-definitions/system-login.xml.in +++ b/interface-definitions/system-login.xml.in @@ -110,58 +110,11 @@ </leafNode> </children> </tagNode> + #include <include/radius-server.xml.i> <node name="radius"> - <properties> - <help>RADIUS based user authentication</help> - </properties> <children> - <leafNode name="source-address"> - <properties> - <help>RADIUS client source address</help> - <valueHelp> - <format>ipv4</format> - <description>TFTP IPv4 listen address</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> - </leafNode> <tagNode name="server"> - <properties> - <help>RADIUS server configuration</help> - <valueHelp> - <format>ipv4</format> - <description>RADIUS server IPv4 address</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> <children> - <leafNode name="disable"> - <properties> - <help>Temporary disable this server</help> - <valueless/> - </properties> - </leafNode> - <leafNode name="key"> - <properties> - <help>Shared secret key</help> - </properties> - </leafNode> - <leafNode name="port"> - <properties> - <help>Authentication port</help> - <valueHelp> - <format>1-65535</format> - <description>Numeric IP port (default: 1812)</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-65535"/> - </constraint> - </properties> - </leafNode> <leafNode name="timeout"> <properties> <help>Session timeout</help> diff --git a/interface-definitions/vrf.xml.in b/interface-definitions/vrf.xml.in new file mode 100644 index 000000000..f1895598e --- /dev/null +++ b/interface-definitions/vrf.xml.in @@ -0,0 +1,47 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="vrf" owner="${vyos_conf_scripts_dir}/vrf.py"> + <properties> + <help>Virtual Routing and Forwarding</help> + <!-- must be before any interface creation --> + <priority>210</priority> + </properties> + <children> + <leafNode name="bind-to-all"> + <properties> + <help>Enable binding services to all VRFs</help> + <valueless/> + </properties> + </leafNode> + <tagNode name="name"> + <properties> + <help>VRF instance name</help> + <constraint> + <regex>[^/\s]{1,16}$</regex> + </constraint> + <constraintErrorMessage>VRF instance name must be 16 characters or less</constraintErrorMessage> + <valueHelp> + <format>name</format> + <description>Instance name</description> + </valueHelp> + </properties> + <children> + <leafNode name="table"> + <properties> + <help>Routing table associated with this instance</help> + <constraint> + <validator name="numeric" argument="--range 1-2147483647"/> + </constraint> + <constraintErrorMessage>Invalid kernel table number</constraintErrorMessage> + <valueHelp> + <format>1-2147483647</format> + <description>Routing table ID</description> + </valueHelp> + </properties> + </leafNode> + #include <include/interface-description.xml.i> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition>
\ No newline at end of file diff --git a/op-mode-definitions/show-vrf.xml b/op-mode-definitions/show-vrf.xml new file mode 100644 index 000000000..360153d8e --- /dev/null +++ b/op-mode-definitions/show-vrf.xml @@ -0,0 +1,22 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="vrf"> + <properties> + <help>Show VRF information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_vrf.py -e</command> + </node> + <tagNode name="vrf"> + <properties> + <help>Show information on specific VRF instance</help> + <completionHelp> + <path>vrf name</path> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/show_vrf.py -e "$3"</command> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/python/setup.py b/python/setup.py index 304ea5cb7..ac7d0b573 100644 --- a/python/setup.py +++ b/python/setup.py @@ -10,7 +10,7 @@ setup( license = "LGPLv2+", keywords = "vyos", url = "http://www.vyos.io", - packages=['vyos'], + packages=["vyos","vyos.ifconfig"], long_description="VyOS configuration libraries", classifiers=[ "Development Status :: 4 - Beta", diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index 80e199907..a1499479a 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -126,7 +126,8 @@ def vlan_to_dict(conf): 'ingress_qos': '', 'ingress_qos_changed': False, 'mac': '', - 'mtu': 1500 + 'mtu': 1500, + 'vrf': '' } # retrieve configured interface addresses if conf.exists('address'): @@ -194,6 +195,10 @@ def vlan_to_dict(conf): if conf.exists('mtu'): vlan['mtu'] = int(conf.return_value('mtu')) + # retrieve VRF instance + if conf.exists('vrf'): + vlan['vrf'] = conf.return_value('vrf') + # VLAN egress QoS if conf.exists('egress-qos'): vlan['egress_qos'] = conf.return_value('egress-qos') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py deleted file mode 100644 index beeafa420..000000000 --- a/python/vyos/ifconfig.py +++ /dev/null @@ -1,2158 +0,0 @@ -# Copyright 2019 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/>. - -import os -import re -import jinja2 -import json -import glob -import time -from copy import deepcopy - -import vyos.interfaces - -from vyos.validate import * -from vyos.config import Config -from vyos import ConfigError - -from ipaddress import IPv4Network, IPv6Address -from netifaces import ifaddresses, AF_INET, AF_INET6 -from subprocess import Popen, PIPE, STDOUT -from time import sleep -from os.path import isfile -from tabulate import tabulate -from hurry.filesize import size,alternative -from datetime import timedelta - -dhclient_base = r'/var/lib/dhcp/dhclient_' -dhcp_cfg = """ -# generated by ifconfig.py -option rfc3442-classless-static-routes code 121 = array of unsigned integer 8; -timeout 60; -retry 300; - -interface "{{ intf }}" { - send host-name "{{ hostname }}"; - {% if client_id -%} - send dhcp-client-identifier "{{ client_id }}"; - {% endif -%} - {% if vendor_class_id -%} - send vendor-class-identifier "{{ vendor_class_id }}"; - {% endif -%} - request subnet-mask, broadcast-address, routers, domain-name-servers, - rfc3442-classless-static-routes, domain-name, interface-mtu; - require subnet-mask; -} - -""" - -dhcpv6_cfg = """ -# generated by ifconfig.py -interface "{{ intf }}" { - request routers, domain-name-servers, domain-name; -} - -""" - -class Control: - _command_get = {} - _command_set = {} - - def _debug_msg(self, msg): - if os.path.isfile('/tmp/vyos.ifconfig.debug'): - print('DEBUG/{:<6} {}'.format(self.config['ifname'], msg)) - - def _cmd(self, command): - p = Popen(command, stdout=PIPE, stderr=STDOUT, shell=True) - tmp = p.communicate()[0].strip() - self._debug_msg("cmd '{}'".format(command)) - if tmp.decode(): - self._debug_msg("returned:\n{}".format(tmp.decode())) - - # do we need some error checking code here? - return tmp.decode() - - def _get_command(self, config, name): - """ - Using the defined names, set data write to sysfs. - """ - cmd = self._command_get[name]['shellcmd'].format(**config) - return self._cmd(cmd) - - def _set_command(self, config, name, value): - """ - Using the defined names, set data write to sysfs. - """ - if not value: - return None - - # the code can pass int as int - value = str(value) - - validate = self._command_set[name].get('validate', None) - if validate: - validate(value) - - config = {**config, **{'value': value}} - - convert = self._command_set[name].get('convert', None) - if convert: - value = convert(value) - - cmd = self._command_set[name]['shellcmd'].format(**config) - return self._cmd(cmd) - - _sysfs_get = {} - _sysfs_set = {} - - def _read_sysfs(self, filename): - """ - Provide a single primitive w/ error checking for reading from sysfs. - """ - value = None - with open(filename, 'r') as f: - value = f.read().rstrip('\n') - - self._debug_msg("read '{}' < '{}'".format(value, filename)) - return value - - def _write_sysfs(self, filename, value): - """ - Provide a single primitive w/ error checking for writing to sysfs. - """ - self._debug_msg("write '{}' > '{}'".format(value, filename)) - if os.path.isfile(filename): - with open(filename, 'w') as f: - f.write(str(value)) - return True - return False - - def _get_sysfs(self, config, name): - """ - Using the defined names, get data write from sysfs. - """ - filename = self._sysfs_get[name]['location'].format(config) - if not filename: - return None - return self._read_sysfs(filename) - - def _set_sysfs(self, config, name, value): - """ - Using the defined names, set data write to sysfs. - """ - if not value: - return None - - # the code can pass int as int - value = str(value) - - validate = self._sysfs_set[name].get('validate', None) - if validate: - validate(value) - - config = {**config, **{'value': value}} - - convert = self._sysfs_set[name].get('convert', None) - if convert: - value = convert(value) - - commited = self._write_sysfs(self._sysfs_set[name]['location'].format(**config), value) - if not commited: - errmsg = self._sysfs_set.get('errormsg','') - if errmsg: - raise TypeError(errmsg.format(**config)) - return commited - - def get_interface(self, name): - if name in self._sysfs_get: - return self._get_sysfs(self.config, name) - if name in self._command_get: - return self._get_command(self.config, name) - raise KeyError(f'{name} is not a attribute of the interface we can get') - - def set_interface(self, name, value): - if name in self._sysfs_set: - return self._set_sysfs(self.config, name, value) - if name in self._command_set: - return self._set_command(self.config, name, value) - raise KeyError(f'{name} is not a attribute of the interface we can set') - - -class Interface(Control): - options = [] - required = [] - default = { - 'type': '', - } - - _command_set = { - 'mac': { - 'validate': assert_mac, - 'shellcmd': 'ip link set dev {ifname} address {value}', - }, - } - - _sysfs_get = { - 'mtu': { - 'location': '/sys/class/net/{ifname}/mtu', - }, - } - - _sysfs_set = { - 'alias': { - 'convert': lambda name: name if name else '\0', - 'location': '/sys/class/net/{ifname}/ifalias', - }, - 'mtu': { - 'validate': assert_mtu, - 'location': '/sys/class/net/{ifname}/mtu', - }, - 'arp_cache_tmo': { - 'convert': lambda tmo: (int(tmo) * 1000), - 'location': '/proc/sys/net/ipv4/neigh/{ifname}/base_reachable_time_ms', - }, - 'arp_filter': { - 'validate': assert_boolean, - 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_filter', - }, - 'arp_accept': { - 'validate': lambda arp: assert_range(arp,0,2), - 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_accept', - }, - 'arp_announce': { - 'validate': assert_boolean, - 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_announce', - }, - 'arp_ignore': { - 'validate': assert_boolean, - 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_ignore', - }, - 'proxy_arp': { - 'validate': assert_boolean, - 'location': '/proc/sys/net/ipv4/conf/{ifname}/proxy_arp', - }, - 'proxy_arp_pvlan': { - 'validate': assert_boolean, - 'location': '/proc/sys/net/ipv4/conf/{ifname}/proxy_arp_pvlan', - }, - # link_detect vs link_filter name weirdness - 'link_detect': { - 'validate': lambda link: assert_range(link,0,3), - 'location': '/proc/sys/net/ipv4/conf/{ifname}/link_filter', - }, - } - - def __init__(self, ifname, **kargs): - """ - This is the base interface class which supports basic IP/MAC address - operations as well as DHCP(v6). Other interface which represent e.g. - and ethernet bridge are implemented as derived classes adding all - additional functionality. - - For creation you will need to provide the interface type, otherwise - the existing interface is used - - DEBUG: - This class has embedded debugging (print) which can be enabled by - creating the following file: - vyos@vyos# touch /tmp/vyos.ifconfig.debug - - Example: - >>> from vyos.ifconfig import Interface - >>> i = Interface('eth0') - """ - - self.config = deepcopy(self.default) - self.config['ifname'] = ifname - - for k in kargs: - if k not in self.options: - raise ConfigError('invalid option {} for {}'.format(k,self.__class__)) - self.config[k] = kargs[k] - - for k in self.required: - if k not in kargs: - raise ConfigError('missing required option {} for {}'.format(k,self.__class__)) - - if not os.path.exists('/sys/class/net/{}'.format(self.config['ifname'])): - if not self.config['type']: - raise Exception('interface "{}" not found'.format(self.config['ifname'])) - self._create() - - # per interface DHCP config files - self._dhcp_cfg_file = dhclient_base + self.config['ifname'] + '.conf' - self._dhcp_pid_file = dhclient_base + self.config['ifname'] + '.pid' - self._dhcp_lease_file = dhclient_base + self.config['ifname'] + '.leases' - - # per interface DHCPv6 config files - self._dhcpv6_cfg_file = dhclient_base + self.config['ifname'] + '.v6conf' - self._dhcpv6_pid_file = dhclient_base + self.config['ifname'] + '.v6pid' - self._dhcpv6_lease_file = dhclient_base + self.config['ifname'] + '.v6leases' - - # DHCP options - self._dhcp_options = { - 'intf' : self.config['ifname'], - 'hostname' : '', - 'client_id' : '', - 'vendor_class_id' : '' - } - - # DHCPv6 options - self._dhcpv6_options = { - 'intf' : self.config['ifname'], - 'dhcpv6_prm_only' : False, - 'dhcpv6_temporary' : False - } - - # list of assigned IP addresses - self._addr = [] - - def _create(self): - cmd = 'ip link add dev {ifname} type {type}'.format(**self.config) - self._cmd(cmd) - - def remove(self): - """ - Remove interface from operating system. Removing the interface - deconfigures all assigned IP addresses and clear possible DHCP(v6) - client processes. - - Example: - >>> from vyos.ifconfig import Interface - >>> i = Interface('eth0') - >>> i.remove() - """ - # stop DHCP(v6) if running - self._del_dhcp() - self._del_dhcpv6() - - # remove all assigned IP addresses from interface - this is a bit redundant - # as the kernel will remove all addresses on interface deletion, but we - # can not delete ALL interfaces, see below - for addr in self.get_addr(): - self.del_addr(addr) - - # --------------------------------------------------------------------- - # A code refactoring is required as this type check is present as - # Interface implement behaviour for one of it's sub-class. - - # It is required as the current pattern for vlan is: - # Interface('name').remove() to delete an interface - # The code should be modified to have a class method called connect and - # have Interface.connect('name').remove() - - # each subclass should register within Interface the pattern for that - # interface ie: (ethX, etc.) and use this to create an instance of - # the right class (EthernetIf, ...) - - # Ethernet interfaces can not be removed - if self.__class__ == EthernetIf: - return - - # --------------------------------------------------------------------- - - self._delete() - - def _delete(self): - # NOTE (Improvement): - # after interface removal no other commands should be allowed - # to be called and instead should raise an Exception: - cmd = 'ip link del dev {}'.format(self.config['ifname']) - return self._cmd(cmd) - - def get_mtu(self): - """ - Get/set interface mtu in bytes. - - Example: - >>> from vyos.ifconfig import Interface - >>> Interface('eth0').get_mtu() - '1500' - """ - return self.get_interface('mtu') - - def set_mtu(self, mtu): - """ - Get/set interface mtu in bytes. - - Example: - >>> from vyos.ifconfig import Interface - >>> Interface('eth0').set_mtu(1400) - >>> Interface('eth0').get_mtu() - '1400' - """ - return self.set_interface('mtu', mtu) - - def set_mac(self, mac): - """ - Set interface MAC (Media Access Contrl) address to given value. - - Example: - >>> from vyos.ifconfig import Interface - >>> Interface('eth0').set_mac('00:50:ab:cd:ef:01') - """ - self.set_interface('mac', mac) - - def set_arp_cache_tmo(self, tmo): - """ - Set ARP cache timeout value in seconds. Internal Kernel representation - is in milliseconds. - - Example: - >>> from vyos.ifconfig import Interface - >>> Interface('eth0').set_arp_cache_tmo(40) - """ - return self.set_interface('arp_cache_tmo', tmo) - - def set_arp_filter(self, arp_filter): - """ - Filter ARP requests - - 1 - Allows you to have multiple network interfaces on the same - subnet, and have the ARPs for each interface be answered - based on whether or not the kernel would route a packet from - the ARP'd IP out that interface (therefore you must use source - based routing for this to work). In other words it allows control - of which cards (usually 1) will respond to an arp request. - - 0 - (default) The kernel can respond to arp requests with addresses - from other interfaces. This may seem wrong but it usually makes - sense, because it increases the chance of successful communication. - IP addresses are owned by the complete host on Linux, not by - particular interfaces. Only for more complex setups like load- - balancing, does this behaviour cause problems. - """ - return self.set_interface('arp_filter', arp_filter) - - def set_arp_accept(self, arp_accept): - """ - Define behavior for gratuitous ARP frames who's IP is not - already present in the ARP table: - 0 - don't create new entries in the ARP table - 1 - create new entries in the ARP table - - Both replies and requests type gratuitous arp will trigger the - ARP table to be updated, if this setting is on. - - If the ARP table already contains the IP address of the - gratuitous arp frame, the arp table will be updated regardless - if this setting is on or off. - """ - return self.set_interface('arp_accept', arp_accept) - - def set_arp_announce(self, arp_announce): - """ - Define different restriction levels for announcing the local - source IP address from IP packets in ARP requests sent on - interface: - 0 - (default) Use any local address, configured on any interface - 1 - Try to avoid local addresses that are not in the target's - subnet for this interface. This mode is useful when target - hosts reachable via this interface require the source IP - address in ARP requests to be part of their logical network - configured on the receiving interface. When we generate the - request we will check all our subnets that include the - target IP and will preserve the source address if it is from - such subnet. - - Increasing the restriction level gives more chance for - receiving answer from the resolved target while decreasing - the level announces more valid sender's information. - """ - return self.set_interface('arp_announce', arp_announce) - - def set_arp_ignore(self, arp_ignore): - """ - Define different modes for sending replies in response to received ARP - requests that resolve local target IP addresses: - - 0 - (default): reply for any local target IP address, configured - on any interface - 1 - reply only if the target IP address is local address - configured on the incoming interface - """ - return self.set_interface('arp_ignore', arp_ignore) - - def set_link_detect(self, link_filter): - """ - Configure kernel response in packets received on interfaces that are 'down' - - 0 - Allow packets to be received for the address on this interface - even if interface is disabled or no carrier. - - 1 - Ignore packets received if interface associated with the incoming - address is down. - - 2 - Ignore packets received if interface associated with the incoming - address is down or has no carrier. - - Default value is 0. Note that some distributions enable it in startup - scripts. - - Example: - >>> from vyos.ifconfig import Interface - >>> Interface('eth0').set_link_detect(1) - """ - return self.set_interface('link_detect', link_filter) - - def set_alias(self, ifalias=''): - """ - Set interface alias name used by e.g. SNMP - - Example: - >>> from vyos.ifconfig import Interface - >>> Interface('eth0').set_alias('VyOS upstream interface') - - to clear alias e.g. delete it use: - - >>> Interface('eth0').set_ifalias('') - """ - self.set_interface('alias', ifalias) - - def get_state(self): - """ - Enable (up) / Disable (down) an interface - - Example: - >>> from vyos.ifconfig import Interface - >>> Interface('eth0').get_state() - 'up' - """ - cmd = 'ip -json link show dev {}'.format(self.config['ifname']) - tmp = self._cmd(cmd) - out = json.loads(tmp) - return out[0]['operstate'].lower() - - def set_state(self, state): - """ - Enable (up) / Disable (down) an interface - - Example: - >>> from vyos.ifconfig import Interface - >>> Interface('eth0').set_state('down') - >>> Interface('eth0').get_state() - 'down' - """ - if state not in ['up', 'down']: - raise ValueError('state must be "up" or "down"') - - # Assemble command executed on system. Unfortunately there is no way - # to up/down an interface via sysfs - cmd = 'ip link set dev {} {}'.format(self.config['ifname'], state) - return self._cmd(cmd) - - def set_proxy_arp(self, enable): - """ - Set per interface proxy ARP configuration - - Example: - >>> from vyos.ifconfig import Interface - >>> Interface('eth0').set_proxy_arp(1) - """ - self.set_interface('proxy_arp', enable) - - def set_proxy_arp_pvlan(self, enable): - """ - Private VLAN proxy arp. - Basically allow proxy arp replies back to the same interface - (from which the ARP request/solicitation was received). - - This is done to support (ethernet) switch features, like RFC - 3069, where the individual ports are NOT allowed to - communicate with each other, but they are allowed to talk to - the upstream router. As described in RFC 3069, it is possible - to allow these hosts to communicate through the upstream - router by proxy_arp'ing. Don't need to be used together with - proxy_arp. - - This technology is known by different names: - In RFC 3069 it is called VLAN Aggregation. - Cisco and Allied Telesyn call it Private VLAN. - Hewlett-Packard call it Source-Port filtering or port-isolation. - Ericsson call it MAC-Forced Forwarding (RFC Draft). - - Example: - >>> from vyos.ifconfig import Interface - >>> Interface('eth0').set_proxy_arp_pvlan(1) - """ - self.set_interface('proxy_arp_pvlan', enable) - - def get_addr(self): - """ - Retrieve assigned IPv4 and IPv6 addresses from given interface. - This is done using the netifaces and ipaddress python modules. - - Example: - >>> from vyos.ifconfig import Interface - >>> Interface('eth0').get_addrs() - ['172.16.33.30/24', 'fe80::20c:29ff:fe11:a174/64'] - """ - - ipv4 = [] - ipv6 = [] - - if AF_INET in ifaddresses(self.config['ifname']).keys(): - for v4_addr in ifaddresses(self.config['ifname'])[AF_INET]: - # we need to manually assemble a list of IPv4 address/prefix - prefix = '/' + \ - str(IPv4Network('0.0.0.0/' + v4_addr['netmask']).prefixlen) - ipv4.append(v4_addr['addr'] + prefix) - - if AF_INET6 in ifaddresses(self.config['ifname']).keys(): - for v6_addr in ifaddresses(self.config['ifname'])[AF_INET6]: - # Note that currently expanded netmasks are not supported. That means - # 2001:db00::0/24 is a valid argument while 2001:db00::0/ffff:ff00:: not. - # see https://docs.python.org/3/library/ipaddress.html - bits = bin( - int(v6_addr['netmask'].replace(':', ''), 16)).count('1') - prefix = '/' + str(bits) - - # we alsoneed to remove the interface suffix on link local - # addresses - v6_addr['addr'] = v6_addr['addr'].split('%')[0] - ipv6.append(v6_addr['addr'] + prefix) - - return ipv4 + ipv6 - - def add_addr(self, addr): - """ - Add IP(v6) address to interface. Address is only added if it is not - already assigned to that interface. - - addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6! - IPv4: add IPv4 address to interface - IPv6: add IPv6 address to interface - dhcp: start dhclient (IPv4) on interface - dhcpv6: start dhclient (IPv6) on interface - - Example: - >>> from vyos.ifconfig import Interface - >>> j = Interface('eth0') - >>> j.add_addr('192.0.2.1/24') - >>> j.add_addr('2001:db8::ffff/64') - >>> j.get_addr() - ['192.0.2.1/24', '2001:db8::ffff/64'] - """ - - # cache new IP address which is assigned to interface - self._addr.append(addr) - - # we can not have both DHCP and static IPv4 addresses assigned to an interface - if 'dhcp' in self._addr: - for addr in self._addr: - # do not change below 'if' ordering esle you will get an exception as: - # ValueError: 'dhcp' does not appear to be an IPv4 or IPv6 address - if addr != 'dhcp' and is_ipv4(addr): - raise ConfigError("Can't configure both static IPv4 and DHCP address on the same interface") - - if addr == 'dhcp': - self._set_dhcp() - elif addr == 'dhcpv6': - self._set_dhcpv6() - else: - if not is_intf_addr_assigned(self.config['ifname'], addr): - cmd = 'ip addr add "{}" dev "{}"'.format(addr, self.config['ifname']) - return self._cmd(cmd) - - def del_addr(self, addr): - """ - Delete IP(v6) address to interface. Address is only added if it is - assigned to that interface. - - addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6! - IPv4: delete IPv4 address from interface - IPv6: delete IPv6 address from interface - dhcp: stop dhclient (IPv4) on interface - dhcpv6: stop dhclient (IPv6) on interface - - Example: - >>> from vyos.ifconfig import Interface - >>> j = Interface('eth0') - >>> j.add_addr('2001:db8::ffff/64') - >>> j.add_addr('192.0.2.1/24') - >>> j.get_addr() - ['192.0.2.1/24', '2001:db8::ffff/64'] - >>> j.del_addr('192.0.2.1/24') - >>> j.get_addr() - ['2001:db8::ffff/64'] - """ - if addr == 'dhcp': - self._del_dhcp() - elif addr == 'dhcpv6': - self._del_dhcpv6() - else: - if is_intf_addr_assigned(self.config['ifname'], addr): - cmd = 'ip addr del "{}" dev "{}"'.format(addr, self.config['ifname']) - return self._cmd(cmd) - - - def get_dhcp_options(self): - """ - Return dictionary with supported DHCP options. - - Dictionary should be altered and send back via set_dhcp_options() - so those options are applied when DHCP is run. - """ - return self._dhcp_options - - def set_dhcp_options(self, options): - """ - Store new DHCP options used by next run of DHCP client. - """ - self._dhcp_options = options - - def get_dhcpv6_options(self): - """ - Return dictionary with supported DHCPv6 options. - - Dictionary should be altered and send back via set_dhcp_options() - so those options are applied when DHCP is run. - """ - return self._dhcpv6_options - - def set_dhcpv6_options(self, options): - """ - Store new DHCP options used by next run of DHCP client. - """ - self._dhcpv6_options = options - - # replace dhcpv4/v6 with systemd.networkd? - def _set_dhcp(self): - """ - Configure interface as DHCP client. The dhclient binary is automatically - started in background! - - Example: - - >>> from vyos.ifconfig import Interface - >>> j = Interface('eth0') - >>> j.set_dhcp() - """ - - dhcp = self.get_dhcp_options() - if not dhcp['hostname']: - # read configured system hostname. - # maybe change to vyos hostd client ??? - with open('/etc/hostname', 'r') as f: - dhcp['hostname'] = f.read().rstrip('\n') - - # render DHCP configuration - tmpl = jinja2.Template(dhcp_cfg) - dhcp_text = tmpl.render(dhcp) - with open(self._dhcp_cfg_file, 'w') as f: - f.write(dhcp_text) - - cmd = 'start-stop-daemon --start --quiet --pidfile ' + \ - self._dhcp_pid_file - cmd += ' --exec /sbin/dhclient --' - # now pass arguments to dhclient binary - cmd += ' -4 -nw -cf {} -pf {} -lf {} {}'.format( - self._dhcp_cfg_file, self._dhcp_pid_file, self._dhcp_lease_file, self.config['ifname']) - return self._cmd(cmd) - - - def _del_dhcp(self): - """ - De-configure interface as DHCP clinet. All auto generated files like - pid, config and lease will be removed. - - Example: - - >>> from vyos.ifconfig import Interface - >>> j = Interface('eth0') - >>> j.del_dhcp() - """ - pid = 0 - if os.path.isfile(self._dhcp_pid_file): - with open(self._dhcp_pid_file, 'r') as f: - pid = int(f.read()) - else: - self._debug_msg('No DHCP client PID found') - return None - - # stop dhclient, we need to call dhclient and tell it should release the - # aquired IP address. tcpdump tells me: - # 172.16.35.103.68 > 172.16.35.254.67: [bad udp cksum 0xa0cb -> 0xb943!] BOOTP/DHCP, Request from 00:50:56:9d:11:df, length 300, xid 0x620e6946, Flags [none] (0x0000) - # Client-IP 172.16.35.103 - # Client-Ethernet-Address 00:50:56:9d:11:df - # Vendor-rfc1048 Extensions - # Magic Cookie 0x63825363 - # DHCP-Message Option 53, length 1: Release - # Server-ID Option 54, length 4: 172.16.35.254 - # Hostname Option 12, length 10: "vyos" - # - cmd = '/sbin/dhclient -cf {} -pf {} -lf {} -r {}'.format( - self._dhcp_cfg_file, self._dhcp_pid_file, self._dhcp_lease_file, self.config['ifname']) - self._cmd(cmd) - - # cleanup old config file - if os.path.isfile(self._dhcp_cfg_file): - os.remove(self._dhcp_cfg_file) - - # cleanup old pid file - if os.path.isfile(self._dhcp_pid_file): - os.remove(self._dhcp_pid_file) - - # cleanup old lease file - if os.path.isfile(self._dhcp_lease_file): - os.remove(self._dhcp_lease_file) - - - def _set_dhcpv6(self): - """ - Configure interface as DHCPv6 client. The dhclient binary is automatically - started in background! - - Example: - - >>> from vyos.ifconfig import Interface - >>> j = Interface('eth0') - >>> j.set_dhcpv6() - """ - dhcpv6 = self.get_dhcpv6_options() - - # better save then sorry .. should be checked in interface script - # but if you missed it we are safe! - if dhcpv6['dhcpv6_prm_only'] and dhcpv6['dhcpv6_temporary']: - raise Exception('DHCPv6 temporary and parameters-only options are mutually exclusive!') - - # render DHCP configuration - tmpl = jinja2.Template(dhcpv6_cfg) - dhcpv6_text = tmpl.render(dhcpv6) - with open(self._dhcpv6_cfg_file, 'w') as f: - f.write(dhcpv6_text) - - # https://bugs.launchpad.net/ubuntu/+source/ifupdown/+bug/1447715 - # - # wee need to wait for IPv6 DAD to finish once and interface is added - # this suxx :-( - sleep(5) - - # no longer accept router announcements on this interface - self._write_sysfs('/proc/sys/net/ipv6/conf/{}/accept_ra' - .format(self.config['ifname']), 0) - - # assemble command-line to start DHCPv6 client (dhclient) - cmd = 'start-stop-daemon --start --quiet --pidfile ' + \ - self._dhcpv6_pid_file - cmd += ' --exec /sbin/dhclient --' - # now pass arguments to dhclient binary - cmd += ' -6 -nw -cf {} -pf {} -lf {}'.format( - self._dhcpv6_cfg_file, self._dhcpv6_pid_file, self._dhcpv6_lease_file) - - # add optional arguments - if dhcpv6['dhcpv6_prm_only']: - cmd += ' -S' - if dhcpv6['dhcpv6_temporary']: - cmd += ' -T' - - cmd += ' {}'.format(self.config['ifname']) - return self._cmd(cmd) - - - def _del_dhcpv6(self): - """ - De-configure interface as DHCPv6 clinet. All auto generated files like - pid, config and lease will be removed. - - Example: - - >>> from vyos.ifconfig import Interface - >>> j = Interface('eth0') - >>> j.del_dhcpv6() - """ - pid = 0 - if os.path.isfile(self._dhcpv6_pid_file): - with open(self._dhcpv6_pid_file, 'r') as f: - pid = int(f.read()) - else: - self._debug_msg('No DHCPv6 client PID found') - return None - - # stop dhclient - cmd = 'start-stop-daemon --stop --quiet --pidfile {}'.format(self._dhcpv6_pid_file) - self._cmd(cmd) - - # accept router announcements on this interface - self._write_sysfs('/proc/sys/net/ipv6/conf/{}/accept_ra' - .format(self.config['ifname']), 1) - - # cleanup old config file - if os.path.isfile(self._dhcpv6_cfg_file): - os.remove(self._dhcpv6_cfg_file) - - # cleanup old pid file - if os.path.isfile(self._dhcpv6_pid_file): - os.remove(self._dhcpv6_pid_file) - - # cleanup old lease file - if os.path.isfile(self._dhcpv6_lease_file): - os.remove(self._dhcpv6_lease_file) - - def op_show_interface_stats(self): - stats = self.get_interface_stats() - rx = [['bytes','packets','errors','dropped','overrun','mcast'],[stats['rx_bytes'],stats['rx_packets'],stats['rx_errors'],stats['rx_dropped'],stats['rx_over_errors'],stats['multicast']]] - tx = [['bytes','packets','errors','dropped','carrier','collisions'],[stats['tx_bytes'],stats['tx_packets'],stats['tx_errors'],stats['tx_dropped'],stats['tx_carrier_errors'],stats['collisions']]] - output = "RX: \n" - output += tabulate(rx,headers="firstrow",numalign="right",tablefmt="plain") - output += "\n\nTX: \n" - output += tabulate(tx,headers="firstrow",numalign="right",tablefmt="plain") - print(' '.join(('\n'+output.lstrip()).splitlines(True))) - - def get_interface_stats(self): - interface_stats = dict() - devices = [f for f in glob.glob("/sys/class/net/**/statistics")] - for dev_path in devices: - metrics = [f for f in glob.glob(dev_path +"/**")] - dev = re.findall(r"/sys/class/net/(.*)/statistics",dev_path)[0] - dev_dict = dict() - for metric_path in metrics: - metric = metric_path.replace(dev_path+"/","") - if isfile(metric_path): - data = open(metric_path, 'r').read()[:-1] - dev_dict[metric] = int(data) - interface_stats[dev] = dev_dict - - return interface_stats[self.config['ifname']] - -class LoopbackIf(Interface): - """ - The loopback device is a special, virtual network interface that your router - uses to communicate with itself. - """ - - default = { - 'type': 'loopback', - } - - def __init__(self, ifname, **kargs): - super().__init__(ifname, **kargs) - - def remove(self): - """ - Loopback interface can not be deleted from operating system. We can - only remove all assigned IP addresses. - - Example: - >>> from vyos.ifconfig import Interface - >>> i = LoopbackIf('lo').remove() - """ - # remove all assigned IP addresses from interface - for addr in self.get_addr(): - if addr in ["127.0.0.1/8", "::1/128"]: - # Do not allow deletion of the default loopback addresses as - # this will cause weird system behavior like snmp/ssh no longer - # operating as expected, see https://phabricator.vyos.net/T2034. - continue - - self.del_addr(addr) - -class DummyIf(Interface): - """ - A dummy interface is entirely virtual like, for example, the loopback - interface. The purpose of a dummy interface is to provide a device to route - packets through without actually transmitting them. - """ - - default = { - 'type': 'dummy', - } - - def __init__(self, ifname, **kargs): - super().__init__(ifname, **kargs) - - -class STPIf(Interface): - """ - A spanning-tree capable interface. This applies only to bridge port member - interfaces! - """ - _sysfs_set = {**Interface._sysfs_set, **{ - 'path_cost': { - # XXX: we should set a maximum - 'validate': assert_positive, - 'location': '/sys/class/net/{ifname}/brport/path_cost', - 'errormsg': '{ifname} is not a bridge port member' - }, - 'path_priority': { - # XXX: we should set a maximum - 'validate': assert_positive, - 'location': '/sys/class/net/{ifname}/brport/priority', - 'errormsg': '{ifname} is not a bridge port member' - }, - }} - - default = { - 'type': 'stp', - } - - def __init__(self, ifname, **kargs): - super().__init__(ifname, **kargs) - - def set_path_cost(self, cost): - """ - Set interface path cost, only relevant for STP enabled interfaces - - Example: - - >>> from vyos.ifconfig import Interface - >>> Interface('eth0').set_path_cost(4) - """ - self.set_interface('path_cost', cost) - - def set_path_priority(self, priority): - """ - Set interface path priority, only relevant for STP enabled interfaces - - Example: - - >>> from vyos.ifconfig import Interface - >>> Interface('eth0').set_path_priority(4) - """ - self.set_interface('path_priority', priority) - -class BridgeIf(Interface): - """ - A bridge is a way to connect two Ethernet segments together in a protocol - independent way. Packets are forwarded based on Ethernet address, rather - than IP address (like a router). Since forwarding is done at Layer 2, all - protocols can go transparently through a bridge. - - The Linux bridge code implements a subset of the ANSI/IEEE 802.1d standard. - """ - - _sysfs_set = {**Interface._sysfs_set, **{ - 'ageing_time': { - 'validate': assert_positive, - 'convert': lambda time: int(time) * 100, - 'location': '/sys/class/net/{ifname}/bridge/ageing_time', - }, - 'forward_delay': { - 'validate': assert_positive, - 'convert': lambda time: int(time) * 100, - 'location': '/sys/class/net/{ifname}/bridge/forward_delay', - }, - 'hello_time': { - 'validate': assert_positive, - 'convert': lambda time: int(time) * 100, - 'location': '/sys/class/net/{ifname}/bridge/hello_time', - }, - 'max_age': { - 'validate': assert_positive, - 'convert': lambda time: int(time) * 100, - 'location': '/sys/class/net/{ifname}/bridge/max_age', - }, - 'priority': { - 'validate': assert_positive, - 'convert': lambda time: int(time) * 100, - 'location': '/sys/class/net/{ifname}/bridge/priority', - }, - 'stp': { - 'validate': assert_boolean, - 'location': '/sys/class/net/{ifconfig}/bridge/stp_state', - }, - 'multicast_querier': { - 'validate': assert_boolean, - 'location': '/sys/class/net/{ifname}/bridge/multicast_querier', - }, - }} - - _command_set = {**Interface._command_set, **{ - 'add_port': { - 'validate': assert_boolean, - 'shellcmd': 'ip link set dev {value} master {ifname}', - }, - 'del_port': { - 'validate': assert_boolean, - 'shellcmd': 'ip link set dev {value} nomaster', - }, - }} - - default = { - 'type': 'bridge', - } - - def __init__(self, ifname, **kargs): - super().__init__(ifname, **kargs) - - def set_ageing_time(self, time): - """ - Set bridge interface MAC address aging time in seconds. Internal kernel - representation is in centiseconds. Kernel default is 300 seconds. - - Example: - >>> from vyos.ifconfig import BridgeIf - >>> BridgeIf('br0').ageing_time(2) - """ - self.set_interface('ageing_time', time) - - def set_forward_delay(self, time): - """ - Set bridge forwarding delay in seconds. Internal Kernel representation - is in centiseconds. - - Example: - >>> from vyos.ifconfig import BridgeIf - >>> BridgeIf('br0').forward_delay(15) - """ - self.set_interface('forward_delay', time) - - - def set_hello_time(self, time): - """ - Set bridge hello time in seconds. Internal Kernel representation - is in centiseconds. - - Example: - >>> from vyos.ifconfig import BridgeIf - >>> BridgeIf('br0').set_hello_time(2) - """ - self.set_interface('hello_time', time) - - def set_max_age(self, time): - """ - Set bridge max message age in seconds. Internal Kernel representation - is in centiseconds. - - Example: - >>> from vyos.ifconfig import Interface - >>> BridgeIf('br0').set_max_age(30) - """ - self.set_interface('max_age', time) - - def set_priority(self, priority): - """ - Set bridge max aging time in seconds. - - Example: - >>> from vyos.ifconfig import BridgeIf - >>> BridgeIf('br0').set_priority(8192) - """ - self.set_interface('priority', time) - - def set_stp(self, state): - """ - Set bridge STP (Spanning Tree) state. 0 -> STP disabled, 1 -> STP enabled - - Example: - >>> from vyos.ifconfig import BridgeIf - >>> BridgeIf('br0').set_stp(1) - """ - self.set_interface('stp', state) - - - def set_multicast_querier(self, enable): - """ - Sets whether the bridge actively runs a multicast querier or not. When a - bridge receives a 'multicast host membership' query from another network - host, that host is tracked based on the time that the query was received - plus the multicast query interval time. - - Use enable=1 to enable or enable=0 to disable - - Example: - >>> from vyos.ifconfig import Interface - >>> BridgeIf('br0').set_multicast_querier(1) - """ - self.set_interface('multicast_querier', enable) - - - def add_port(self, interface): - """ - Add physical interface to bridge (member port) - - Example: - >>> from vyos.ifconfig import Interface - >>> BridgeIf('br0').add_port('eth0') - >>> BridgeIf('br0').add_port('eth1') - """ - return self.set_interface('add_port', interface) - - def del_port(self, interface): - """ - Remove member port from bridge instance. - - Example: - >>> from vyos.ifconfig import Interface - >>> BridgeIf('br0').del_port('eth1') - """ - return self.set_interface('del_port', interface) - -class VLANIf(Interface): - """ - This class handels the creation and removal of a VLAN interface. It serves - as base class for BondIf and EthernetIf. - """ - - default = { - 'type': 'vlan', - } - - def __init__(self, ifname, **kargs): - super().__init__(ifname, **kargs) - - def remove(self): - """ - Remove interface from operating system. Removing the interface - deconfigures all assigned IP addresses and clear possible DHCP(v6) - client processes. - - Example: - >>> from vyos.ifconfig import Interface - >>> i = Interface('eth0') - >>> i.remove() - """ - # Do we have sub interfaces (VLANs)? We apply a regex matching - # subinterfaces (indicated by a .) of a parent interface. - # - # As interfaces need to be deleted "in order" starting from Q-in-Q - # we delete them first. - vlan_ifs = [f for f in os.listdir(r'/sys/class/net') \ - if re.match(self.config['ifname'] + r'(?:\.\d+)(?:\.\d+)', f)] - - for vlan in vlan_ifs: - Interface(vlan).remove() - - # After deleting all Q-in-Q interfaces delete other VLAN interfaces - # which probably acted as parent to Q-in-Q or have been regular 802.1q - # interface. - vlan_ifs = [f for f in os.listdir(r'/sys/class/net') \ - if re.match(self.config['ifname'] + r'(?:\.\d+)', f)] - - for vlan in vlan_ifs: - Interface(vlan).remove() - - # All subinterfaces are now removed, continue on the physical interface - super().remove() - - - def add_vlan(self, vlan_id, ethertype='', ingress_qos='', egress_qos=''): - """ - A virtual LAN (VLAN) is any broadcast domain that is partitioned and - isolated in a computer network at the data link layer (OSI layer 2). - Use this function to create a new VLAN interface on a given physical - interface. - - This function creates both 802.1q and 802.1ad (Q-in-Q) interfaces. Proto - parameter is used to indicate VLAN type. - - A new object of type VLANIf is returned once the interface has been - created. - - @param ethertype: If specified, create 802.1ad or 802.1q Q-in-Q VLAN - interface - @param ingress_qos: Defines a mapping of VLAN header prio field to the - Linux internal packet priority on incoming frames. - @param ingress_qos: Defines a mapping of Linux internal packet priority - to VLAN header prio field but for outgoing frames. - - Example: - >>> from vyos.ifconfig import VLANIf - >>> i = VLANIf('eth0') - >>> i.add_vlan(10) - """ - vlan_ifname = self.config['ifname'] + '.' + str(vlan_id) - if not os.path.exists('/sys/class/net/{}'.format(vlan_ifname)): - self._vlan_id = int(vlan_id) - - if ethertype: - self._ethertype = ethertype - ethertype = 'proto {}'.format(ethertype) - - # Optional ingress QOS mapping - opt_i = '' - if ingress_qos: - opt_i = 'ingress-qos-map ' + ingress_qos - # Optional egress QOS mapping - opt_e = '' - if egress_qos: - opt_e = 'egress-qos-map ' + egress_qos - - # create interface in the system - cmd = 'ip link add link {intf} name {intf}.{vlan} type vlan {proto} id {vlan} {opt_e} {opt_i}' \ - .format(intf=self.config['ifname'], vlan=self._vlan_id, proto=ethertype, opt_e=opt_e, opt_i=opt_i) - self._cmd(cmd) - - # return new object mapping to the newly created interface - # we can now work on this object for e.g. IP address setting - # or interface description and so on - return VLANIf(vlan_ifname) - - - def del_vlan(self, vlan_id): - """ - Remove VLAN interface from operating system. Removing the interface - deconfigures all assigned IP addresses and clear possible DHCP(v6) - client processes. - - Example: - >>> from vyos.ifconfig import VLANIf - >>> i = VLANIf('eth0.10') - >>> i.del_vlan() - """ - vlan_ifname = self.config['ifname'] + '.' + str(vlan_id) - VLANIf(vlan_ifname).remove() - - -class EthernetIf(VLANIf): - """ - Abstraction of a Linux Ethernet Interface - """ - - _command_set = {**Interface._command_set, **{ - 'gro': { - 'validate': lambda v: assert_list(v,['on','off']), - 'shellcmd': '/sbin/ethtool -K {ifname} gro {value}', - }, - 'gso': { - 'validate': lambda v: assert_list(v,['on','off']), - 'shellcmd': '/sbin/ethtool -K {ifname} gso {value}', - }, - 'sg': { - 'validate': lambda v: assert_list(v,['on','off']), - 'shellcmd': '/sbin/ethtool -K {ifname} sg {value}', - }, - 'tso': { - 'validate': lambda v: assert_list(v,['on','off']), - 'shellcmd': '/sbin/ethtool -K {ifname} tso {value}', - }, - 'ufo': { - 'validate': lambda v: assert_list(v,['on','off']), - 'shellcmd': '/sbin/ethtool -K {ifname} ufo {value}', - }, - }} - - default = { - 'type': 'ethernet', - } - - def __init__(self, ifname, **kargs): - super().__init__(ifname, **kargs) - - def _delete (self): - # Ethernet interfaces can not be removed - pass - - def get_driver_name(self): - """ - Return the driver name used by NIC. Some NICs don't support all - features e.g. changing link-speed, duplex - - Example: - >>> from vyos.ifconfig import EthernetIf - >>> i = EthernetIf('eth0') - >>> i.get_driver_name() - 'vmxnet3' - """ - sysfs_file = '/sys/class/net/{}/device/driver/module'.format(self.config['ifname']) - if os.path.exists(sysfs_file): - link = os.readlink(sysfs_file) - return os.path.basename(link) - else: - return None - - def set_flow_control(self, enable): - """ - Changes the pause parameters of the specified Ethernet device. - - @param enable: true -> enable pause frames, false -> disable pause frames - - Example: - >>> from vyos.ifconfig import EthernetIf - >>> i = EthernetIf('eth0') - >>> i.set_flow_control(True) - """ - if enable not in ['on', 'off']: - raise ValueError("Value out of range") - - if self.get_driver_name() in ['vmxnet3', 'virtio_net']: - self._debug_msg('{} driver does not support changing flow control settings!' - .format(self.get_driver_name())) - return - - # Get current flow control settings: - cmd = '/sbin/ethtool --show-pause {0}'.format(self.config['ifname']) - tmp = self._cmd(cmd) - - # The above command returns - with tabs: - # - # Pause parameters for eth0: - # Autonegotiate: on - # RX: off - # TX: off - if re.search("Autonegotiate:\ton", tmp): - if enable == "on": - # flowcontrol is already enabled - no need to re-enable it again - # this will prevent the interface from flapping as applying the - # flow-control settings will take the interface down and bring - # it back up every time. - return - - # Assemble command executed on system. Unfortunately there is no way - # to change this setting via sysfs - cmd = '/sbin/ethtool --pause {0} autoneg {1} tx {1} rx {1}'.format( - self.config['ifname'], enable) - try: - # An exception will be thrown if the settings are not changed - return self._cmd(cmd) - except CalledProcessError: - pass - - - def set_speed_duplex(self, speed, duplex): - """ - Set link speed in Mbit/s and duplex. - - @speed can be any link speed in MBit/s, e.g. 10, 100, 1000 auto - @duplex can be half, full, auto - - Example: - >>> from vyos.ifconfig import EthernetIf - >>> i = EthernetIf('eth0') - >>> i.set_speed_duplex('auto', 'auto') - """ - - if speed not in ['auto', '10', '100', '1000', '2500', '5000', '10000', '25000', '40000', '50000', '100000', '400000']: - raise ValueError("Value out of range (speed)") - - if duplex not in ['auto', 'full', 'half']: - raise ValueError("Value out of range (duplex)") - - if self.get_driver_name() in ['vmxnet3', 'virtio_net']: - self._debug_msg('{} driver does not support changing speed/duplex settings!' - .format(self.get_driver_name())) - return - - # Get current speed and duplex settings: - cmd = '/sbin/ethtool {0}'.format(self.config['ifname']) - tmp = self._cmd(cmd) - - if re.search("\tAuto-negotiation: on", tmp): - if speed == 'auto' and duplex == 'auto': - # bail out early as nothing is to change - return - else: - # read in current speed and duplex settings - cur_speed = 0 - cur_duplex = '' - for line in tmp.splitlines(): - if line.lstrip().startswith("Speed:"): - non_decimal = re.compile(r'[^\d.]+') - cur_speed = non_decimal.sub('', line) - continue - - if line.lstrip().startswith("Duplex:"): - cur_duplex = line.split()[-1].lower() - break - - if (cur_speed == speed) and (cur_duplex == duplex): - # bail out early as nothing is to change - return - - cmd = '/sbin/ethtool -s {}'.format(self.config['ifname']) - if speed == 'auto' or duplex == 'auto': - cmd += ' autoneg on' - else: - cmd += ' speed {} duplex {} autoneg off'.format(speed, duplex) - - return self._cmd(cmd) - - - def set_gro(self, state): - """ - Example: - >>> from vyos.ifconfig import EthernetIf - >>> i = EthernetIf('eth0') - >>> i.set_gro('on') - """ - return self.set_interface('gro', state) - - - def set_gso(self, state): - """ - Example: - >>> from vyos.ifconfig import EthernetIf - >>> i = EthernetIf('eth0') - >>> i.set_gso('on') - """ - return self.set_interface('gso', state) - - - def set_sg(self, state): - """ - Example: - >>> from vyos.ifconfig import EthernetIf - >>> i = EthernetIf('eth0') - >>> i.set_sg('on') - """ - return self.set_interface('sg', state) - - - def set_tso(self, state): - """ - Example: - >>> from vyos.ifconfig import EthernetIf - >>> i = EthernetIf('eth0') - >>> i.set_tso('on') - """ - return self.set_interface('tso', state) - - - def set_ufo(self, state): - """ - Example: - >>> from vyos.ifconfig import EthernetIf - >>> i = EthernetIf('eth0') - >>> i.set_udp_offload('on') - """ - if state not in ['on', 'off']: - raise ValueError('state must be "on" or "off"') - - cmd = '/sbin/ethtool -K {} ufo {}'.format(self.config['ifname'], state) - return self._cmd(cmd) - -class MACVLANIf(VLANIf): - """ - Abstraction of a Linux MACvlan interface - """ - - options = VLANIf.options + ['link', 'mode'] - default = { - 'type': 'macvlan', - } - - def __init__(self, ifname, **kargs): - super().__init__(ifname, **kargs) - - def _create(self): - cmd = 'ip link add {intf} link {link} type macvlan mode {mode}'.format(**self.config) - self._cmd(cmd) - - @staticmethod - def get_config(): - """ - VXLAN interfaces require a configuration when they are added using - iproute2. This static method will provide the configuration dictionary - used by this class. - - Example: - >> dict = MACVLANIf().get_config() - """ - config = { - 'address': '', - 'link': 0, - 'mode': '' - } - return config - - def set_mode(self, mode): - """ - """ - - cmd = 'ip link set dev {} type macvlan mode {}'.format(self.config['ifname'], mode) - return self._cmd(cmd) - - -class BondIf(VLANIf): - """ - The Linux bonding driver provides a method for aggregating multiple network - interfaces into a single logical "bonded" interface. The behavior of the - bonded interfaces depends upon the mode; generally speaking, modes provide - either hot standby or load balancing services. Additionally, link integrity - monitoring may be performed. - """ - - _sysfs_set = {**Interface._sysfs_set, **{ - 'bond_hash_policy': { - 'validate': lambda v: assert_list(v,['layer2', 'layer2+3', 'layer3+4', 'encap2+3', 'encap3+4']), - 'location': '/sys/class/net/{ifname}/bonding/xmit_hash_policy', - }, - 'bond_miimon': { - 'validate': assert_positive, - 'location': '/sys/class/net/{ifname}/bonding/miimon' - }, - 'bond_arp_interval': { - 'validate': assert_positive, - 'location': '/sys/class/net/{ifname}/bonding/arp_interval' - }, - 'bond_arp_ip_target': { - # XXX: no validation of the IP - 'location': '/sys/class/net/{ifname}/bonding/arp_ip_target', - }, - 'bond_add_port': { - 'location': '/sys/class/net/{ifname}+{value}/bonding/slaves', - }, - 'bond_del_port': { - 'location': '/sys/class/net/{ifname}-{value}/bonding/slaves', - }, - 'bond_primary': { - 'convert': lambda name: name if name else '\0', - 'location': '/sys/class/net/{ifname}/bonding/primary', - }, - 'bond_mode': { - 'validate': lambda v: assert_list(v,['balance-rr', 'active-backup', 'balance-xor', 'broadcast', '802.3ad', 'balance-tlb', 'balance-alb']), - 'location': '/sys/class/net/{ifname}/bonding/mode', - }, - }} - - _sysfs_get = {**Interface._sysfs_get, **{ - 'bond_arp_ip_target': { - 'location': '/sys/class/net/{ifname}/bonding/arp_ip_target', - } - }} - - default = { - 'type': 'bond', - } - - def __init__(self, ifname, **kargs): - super().__init__(ifname, **kargs) - - def remove(self): - """ - Remove interface from operating system. Removing the interface - deconfigures all assigned IP addresses and clear possible DHCP(v6) - client processes. - Example: - >>> from vyos.ifconfig import Interface - >>> i = Interface('eth0') - >>> i.remove() - """ - # when a bond member gets deleted, all members are placed in A/D state - # even when they are enabled inside CLI. This will make the config - # and system look async. - slave_list = [] - for s in self.get_slaves(): - slave = { - 'ifname' : s, - 'state': Interface(s).get_state() - } - slave_list.append(slave) - - # remove bond master which places members in disabled state - super().remove() - - # replicate previous interface state before bond destruction back to - # physical interface - for slave in slave_list: - i = Interface(slave['ifname']) - i.set_state(slave['state']) - - - def set_hash_policy(self, mode): - """ - Selects the transmit hash policy to use for slave selection in - balance-xor, 802.3ad, and tlb modes. Possible values are: layer2, - layer2+3, layer3+4, encap2+3, encap3+4. - - The default value is layer2 - - Example: - >>> from vyos.ifconfig import BondIf - >>> BondIf('bond0').set_hash_policy('layer2+3') - """ - self.set_interface('bond_hash_policy', mode) - - def set_arp_interval(self, interval): - """ - Specifies the ARP link monitoring frequency in milliseconds. - - The ARP monitor works by periodically checking the slave devices - to determine whether they have sent or received traffic recently - (the precise criteria depends upon the bonding mode, and the - state of the slave). Regular traffic is generated via ARP probes - issued for the addresses specified by the arp_ip_target option. - - If ARP monitoring is used in an etherchannel compatible mode - (modes 0 and 2), the switch should be configured in a mode that - evenly distributes packets across all links. If the switch is - configured to distribute the packets in an XOR fashion, all - replies from the ARP targets will be received on the same link - which could cause the other team members to fail. - - value of 0 disables ARP monitoring. The default value is 0. - - Example: - >>> from vyos.ifconfig import BondIf - >>> BondIf('bond0').set_arp_interval('100') - """ - if int(interval) == 0: - """ - Specifies the MII link monitoring frequency in milliseconds. - This determines how often the link state of each slave is - inspected for link failures. A value of zero disables MII - link monitoring. A value of 100 is a good starting point. - """ - return self.set_interface('bond_miimon', interval) - else: - return self.set_interface('bond_arp_interval', interval) - - def get_arp_ip_target(self): - """ - Specifies the IP addresses to use as ARP monitoring peers when - arp_interval is > 0. These are the targets of the ARP request sent to - determine the health of the link to the targets. Specify these values - in ddd.ddd.ddd.ddd format. Multiple IP addresses must be separated by - a comma. At least one IP address must be given for ARP monitoring to - function. The maximum number of targets that can be specified is 16. - - The default value is no IP addresses. - - Example: - >>> from vyos.ifconfig import BondIf - >>> BondIf('bond0').get_arp_ip_target() - '192.0.2.1' - """ - return self.get_interface('bond_arp_ip_target') - - def set_arp_ip_target(self, target): - """ - Specifies the IP addresses to use as ARP monitoring peers when - arp_interval is > 0. These are the targets of the ARP request sent to - determine the health of the link to the targets. Specify these values - in ddd.ddd.ddd.ddd format. Multiple IP addresses must be separated by - a comma. At least one IP address must be given for ARP monitoring to - function. The maximum number of targets that can be specified is 16. - - The default value is no IP addresses. - - Example: - >>> from vyos.ifconfig import BondIf - >>> BondIf('bond0').set_arp_ip_target('192.0.2.1') - >>> BondIf('bond0').get_arp_ip_target() - '192.0.2.1' - """ - return self.set_interface('bond_arp_ip_target', target) - - def add_port(self, interface): - """ - Enslave physical interface to bond. - - Example: - >>> from vyos.ifconfig import BondIf - >>> BondIf('bond0').add_port('eth0') - >>> BondIf('bond0').add_port('eth1') - """ - # An interface can only be added to a bond if it is in 'down' state. If - # interface is in 'up' state, the following Kernel error will be thrown: - # bond0: eth1 is up - this may be due to an out of date ifenslave. - Interface(interface).set_state('down') - return self.set_interface('bond_add_port', interface) - - def del_port(self, interface): - """ - Remove physical port from bond - - Example: - >>> from vyos.ifconfig import BondIf - >>> BondIf('bond0').del_port('eth1') - """ - return self.set_interface('bond_del_port', interface) - - def get_slaves(self): - """ - Return a list with all configured slave interfaces on this bond. - - Example: - >>> from vyos.ifconfig import BondIf - >>> BondIf('bond0').get_slaves() - ['eth1', 'eth2'] - """ - enslaved_ifs = [] - # retrieve real enslaved interfaces from OS kernel - sysfs_bond = '/sys/class/net/{}'.format(self.config['ifname']) - if os.path.isdir(sysfs_bond): - for directory in os.listdir(sysfs_bond): - if 'lower_' in directory: - enslaved_ifs.append(directory.replace('lower_','')) - - return enslaved_ifs - - - def set_primary(self, interface): - """ - A string (eth0, eth2, etc) specifying which slave is the primary - device. The specified device will always be the active slave while it - is available. Only when the primary is off-line will alternate devices - be used. This is useful when one slave is preferred over another, e.g., - when one slave has higher throughput than another. - - The primary option is only valid for active-backup, balance-tlb and - balance-alb mode. - - Example: - >>> from vyos.ifconfig import BondIf - >>> BondIf('bond0').set_primary('eth2') - """ - return self.set_interface('bond_primary', interface) - - def set_mode(self, mode): - """ - Specifies one of the bonding policies. The default is balance-rr - (round robin). - - Possible values are: balance-rr, active-backup, balance-xor, - broadcast, 802.3ad, balance-tlb, balance-alb - - NOTE: the bonding mode can not be changed when the bond itself has - slaves - - Example: - >>> from vyos.ifconfig import BondIf - >>> BondIf('bond0').set_mode('802.3ad') - """ - return self.set_interface('bond_mode', mode) - -class WireGuardIf(Interface): - options = ['port', 'private-key', 'pubkey', 'psk', 'allowed-ips', 'fwmark', 'endpoint', 'keepalive'] - - default = { - 'type': 'wireguard', - 'port': 0, - 'private-key': None, - 'pubkey': None, - 'psk': '/dev/null', - 'allowed-ips': [], - 'fwmark': 0x00, - 'endpoint': None, - 'keepalive': 0 - } - - """ - Wireguard interface class, contains a comnfig dictionary since - wireguard VPN is being comnfigured via the wg command rather than - writing the config into a file. Otherwise if a pre-shared key is used - (symetric enryption key), it would we exposed within multiple files. - Currently it's only within the config.boot if the config was saved. - - Example: - >>> from vyos.ifconfig import WireGuardIf as wg_if - >>> wg_intfc = wg_if("wg01") - >>> print (wg_intfc.wg_config) - {'private-key': None, 'keepalive': 0, 'endpoint': None, 'port': 0, - 'allowed-ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'} - >>> wg_intfc.wg_config['keepalive'] = 100 - >>> print (wg_intfc.wg_config) - {'private-key': None, 'keepalive': 100, 'endpoint': None, 'port': 0, - 'allowed-ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'} - """ - - def __init__(self, ifname, **kargs): - super().__init__(ifname, **kargs) - - def update(self): - if not self.config['private-key']: - raise ValueError("private key required") - else: - # fmask permission check? - pass - - cmd = "wg set {} ".format(self.config['ifname']) - cmd += "listen-port {} ".format(self.config['port']) - cmd += "fwmark {} ".format(str(self.config['fwmark'])) - cmd += "private-key {} ".format(self.config['private-key']) - cmd += "peer {} ".format(self.config['pubkey']) - cmd += " preshared-key {} ".format(self.config['psk']) - cmd += " allowed-ips " - for aip in self.config['allowed-ips']: - if aip != self.config['allowed-ips'][-1]: - cmd += aip + "," - else: - cmd += aip - if self.config['endpoint']: - cmd += " endpoint {}".format(self.config['endpoint']) - cmd += " persistent-keepalive {}".format(self.config['keepalive']) - - self._cmd(cmd) - - # remove psk since it isn't required anymore and is saved in the cli - # config only !! - if self.config['psk'] != '/dev/null': - if os.path.exists(self.config['psk']): - os.remove(self.config['psk']) - - - def remove_peer(self, peerkey): - """ - Remove a peer of an interface, peers are identified by their public key. - Giving it a readable name is a vyos feature, to remove a peer the pubkey - and the interface is needed, to remove the entry. - """ - cmd = "wg set {0} peer {1} remove".format( - self.config['ifname'], str(peerkey)) - return self._cmd(cmd) - - def op_show_interface(self): - wgdump = vyos.interfaces.wireguard_dump().get(self.config['ifname'],None) - - c = Config() - c.set_level(["interfaces","wireguard",self.config['ifname']]) - description = c.return_effective_value(["description"]) - ips = c.return_effective_values(["address"]) - - print ("interface: {}".format(self.config['ifname'])) - if (description): - print (" description: {}".format(description)) - - if (ips): - print (" address: {}".format(", ".join(ips))) - print (" public key: {}".format(wgdump['public_key'])) - print (" private key: (hidden)") - print (" listening port: {}".format(wgdump['listen_port'])) - print () - - for peer in c.list_effective_nodes(["peer"]): - if wgdump['peers']: - pubkey = c.return_effective_value(["peer",peer,"pubkey"]) - if pubkey in wgdump['peers']: - wgpeer = wgdump['peers'][pubkey] - - print (" peer: {}".format(peer)) - print (" public key: {}".format(pubkey)) - - """ figure out if the tunnel is recently active or not """ - status = "inactive" - if (wgpeer['latest_handshake'] is None): - """ no handshake ever """ - status = "inactive" - else: - if int(wgpeer['latest_handshake']) > 0: - delta = timedelta(seconds=int(time.time() - wgpeer['latest_handshake'])) - print (" latest handshake: {}".format(delta)) - if (time.time() - int(wgpeer['latest_handshake']) < (60*5)): - """ Five minutes and the tunnel is still active """ - status = "active" - else: - """ it's been longer than 5 minutes """ - status = "inactive" - elif int(wgpeer['latest_handshake']) == 0: - """ no handshake ever """ - status = "inactive" - print (" status: {}".format(status)) - - if wgpeer['endpoint'] is not None: - print (" endpoint: {}".format(wgpeer['endpoint'])) - - if wgpeer['allowed_ips'] is not None: - print (" allowed ips: {}".format(",".join(wgpeer['allowed_ips']).replace(",",", "))) - - if wgpeer['transfer_rx'] > 0 or wgpeer['transfer_tx'] > 0: - rx_size =size(wgpeer['transfer_rx'],system=alternative) - tx_size =size(wgpeer['transfer_tx'],system=alternative) - print (" transfer: {} received, {} sent".format(rx_size,tx_size)) - - if wgpeer['persistent_keepalive'] is not None: - print (" persistent keepalive: every {} seconds".format(wgpeer['persistent_keepalive'])) - print() - super().op_show_interface_stats() - - -class VXLANIf(Interface): - """ - The VXLAN protocol is a tunnelling protocol designed to solve the - problem of limited VLAN IDs (4096) in IEEE 802.1q. With VXLAN the - size of the identifier is expanded to 24 bits (16777216). - - VXLAN is described by IETF RFC 7348, and has been implemented by a - number of vendors. The protocol runs over UDP using a single - destination port. This document describes the Linux kernel tunnel - device, there is also a separate implementation of VXLAN for - Openvswitch. - - Unlike most tunnels, a VXLAN is a 1 to N network, not just point to - point. A VXLAN device can learn the IP address of the other endpoint - either dynamically in a manner similar to a learning bridge, or make - use of statically-configured forwarding entries. - - For more information please refer to: - https://www.kernel.org/doc/Documentation/networking/vxlan.txt - """ - - default = { - 'type': 'vxlan', - 'vni': 0, - 'dev': '', - 'group': '', - 'remote': '', - 'port': 8472, # The Linux implementation of VXLAN pre-dates - # the IANA's selection of a standard destination port - } - - def __init__(self, ifname, **kargs): - super().__init__(ifname, **kargs) - - def _create(self): - # we assume that by default a multicast interface is created - group = 'group {}'.format(self.config['group']) - - # if remote host is specified we ignore the multicast address - if config['remote']: - group = 'remote {}'.format(self.config['remote']) - - # an underlay device is not always specified - dev = '' - if self.config['dev']: - dev = 'dev {}'.format(self.config['dev']) - - cmd = 'ip link add {intf} type vxlan id {vni} {grp_rem} {dev} dstport {port}' \ - .format(intf=self.config['ifname'], vni=self.config['vni'], grp_rem=group, dev=dev, port=self.config['port']) - self._cmd(cmd) - - @staticmethod - def get_config(): - """ - VXLAN interfaces require a configuration when they are added using - iproute2. This static method will provide the configuration dictionary - used by this class. - - Example: - >> dict = VXLANIf().get_config() - """ - config = { - 'vni': 0, - 'dev': '', - 'group': '', - 'port': 8472, # The Linux implementation of VXLAN pre-dates - # the IANA's selection of a standard destination port - 'remote': '' - } - return config - -class GeneveIf(Interface): - """ - Geneve: Generic Network Virtualization Encapsulation - - For more information please refer to: - https://tools.ietf.org/html/draft-gross-geneve-00 - https://www.redhat.com/en/blog/what-geneve - https://developers.redhat.com/blog/2019/05/17/an-introduction-to-linux-virtual-interfaces-tunnels/#geneve - https://lwn.net/Articles/644938/ - """ - - default = { - 'type': 'geneve', - } - - def __init__(self, ifname, **kargs): - super().__init__(ifname, **kargs) - - def _create (self): - cmd = 'ip link add name {} type geneve id {} remote {}' \ - .format(self.config['ifname'], config['vni'], config['remote']) - self._cmd(cmd) - - # interface is always A/D down. It needs to be enabled explicitly - self.set_state('down') - - @staticmethod - def get_config(): - """ - GENEVE interfaces require a configuration when they are added using - iproute2. This static method will provide the configuration dictionary - used by this class. - - Example: - >> dict = GeneveIf().get_config() - """ - config = { - 'vni': 0, - 'remote': '' - } - return config - -class L2TPv3If(Interface): - """ - The Linux bonding driver provides a method for aggregating multiple network - interfaces into a single logical "bonded" interface. The behavior of the - bonded interfaces depends upon the mode; generally speaking, modes provide - either hot standby or load balancing services. Additionally, link integrity - monitoring may be performed. - """ - - options = Interface.options + \ - ['tunnel_id','peer_tunnel_id','local_port','remote_port','encapsulation','local_address','remote_address'] - default = { - 'type': 'l2tp', - } - - def __init__(self, ifname, **kargs): - super().__init__(ifname, **kargs) - - def _create (self): - # create tunnel interface - cmd = 'ip l2tp add tunnel tunnel_id {} '.format(config['tunnel_id']) - cmd += 'peer_tunnel_id {} '.format(config['peer_tunnel_id']) - cmd += 'udp_sport {} '.format(config['local_port']) - cmd += 'udp_dport {} '.format(config['remote_port']) - cmd += 'encap {} '.format(config['encapsulation']) - cmd += 'local {} '.format(config['local_address']) - cmd += 'remote {} '.format(config['remote_address']) - self._cmd(cmd) - - # setup session - cmd = 'ip l2tp add session name {} '.format(self.config['ifname']) - cmd += 'tunnel_id {} '.format(config['tunnel_id']) - cmd += 'session_id {} '.format(config['session_id']) - cmd += 'peer_session_id {} '.format(config['peer_session_id']) - self._cmd(cmd) - - # interface is always A/D down. It needs to be enabled explicitly - self.set_state('down') - - def remove(self): - """ - Remove interface from operating system. Removing the interface - deconfigures all assigned IP addresses. - Example: - >>> from vyos.ifconfig import L2TPv3If - >>> i = L2TPv3If('l2tpeth0') - >>> i.remove() - """ - - if os.path.exists('/sys/class/net/{}'.format(self.config['ifname'])): - # interface is always A/D down. It needs to be enabled explicitly - self.set_state('down') - - if self._config['tunnel_id'] and self._config['session_id']: - cmd = 'ip l2tp del session tunnel_id {} '.format(self._config['tunnel_id']) - cmd += 'session_id {} '.format(self._config['session_id']) - self._cmd(cmd) - - if self._config['tunnel_id']: - cmd = 'ip l2tp del tunnel tunnel_id {} '.format(self._config['tunnel_id']) - self._cmd(cmd) - - @staticmethod - def get_config(): - """ - L2TPv3 interfaces require a configuration when they are added using - iproute2. This static method will provide the configuration dictionary - used by this class. - - Example: - >> dict = L2TPv3If().get_config() - """ - config = { - 'peer_tunnel_id': '', - 'local_port': 0, - 'remote_port': 0, - 'encapsulation': 'udp', - 'local_address': '', - 'remote_address': '', - 'session_id': '', - 'tunnel_id': '', - 'peer_session_id': '' - } - return config diff --git a/python/vyos/ifconfig/__init__.py b/python/vyos/ifconfig/__init__.py new file mode 100644 index 000000000..c77b2f9db --- /dev/null +++ b/python/vyos/ifconfig/__init__.py @@ -0,0 +1,29 @@ +# Copyright 2019 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/>. + + +from vyos.ifconfig.interface import Interface + +from vyos.ifconfig.bond import BondIf +from vyos.ifconfig.bridge import BridgeIf +from vyos.ifconfig.dummy import DummyIf +from vyos.ifconfig.ethernet import EthernetIf +from vyos.ifconfig.geneve import GeneveIf +from vyos.ifconfig.loopback import LoopbackIf +from vyos.ifconfig.macvlan import MACVLANIf +from vyos.ifconfig.stp import STPIf +from vyos.ifconfig.vlan import VLANIf +from vyos.ifconfig.vxlan import VXLANIf +from vyos.ifconfig.wireguard import WireGuardIf diff --git a/python/vyos/ifconfig/bond.py b/python/vyos/ifconfig/bond.py new file mode 100644 index 000000000..211790459 --- /dev/null +++ b/python/vyos/ifconfig/bond.py @@ -0,0 +1,270 @@ +# Copyright 2019 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/>. + +import os + +from vyos.ifconfig.interface import Interface +from vyos.ifconfig.vlan import VLANIf + +from vyos.validate import * + + +class BondIf(VLANIf): + """ + The Linux bonding driver provides a method for aggregating multiple network + interfaces into a single logical "bonded" interface. The behavior of the + bonded interfaces depends upon the mode; generally speaking, modes provide + either hot standby or load balancing services. Additionally, link integrity + monitoring may be performed. + """ + + _sysfs_set = {**VLANIf._sysfs_set, **{ + 'bond_hash_policy': { + 'validate': lambda v: assert_list(v, ['layer2', 'layer2+3', 'layer3+4', 'encap2+3', 'encap3+4']), + 'location': '/sys/class/net/{ifname}/bonding/xmit_hash_policy', + }, + 'bond_miimon': { + 'validate': assert_positive, + 'location': '/sys/class/net/{ifname}/bonding/miimon' + }, + 'bond_arp_interval': { + 'validate': assert_positive, + 'location': '/sys/class/net/{ifname}/bonding/arp_interval' + }, + 'bond_arp_ip_target': { + # XXX: no validation of the IP + 'location': '/sys/class/net/{ifname}/bonding/arp_ip_target', + }, + 'bond_add_port': { + 'location': '/sys/class/net/{ifname}/bonding/slaves', + }, + 'bond_del_port': { + 'location': '/sys/class/net/{ifname}/bonding/slaves', + }, + 'bond_primary': { + 'convert': lambda name: name if name else '\0', + 'location': '/sys/class/net/{ifname}/bonding/primary', + }, + 'bond_mode': { + 'validate': lambda v: assert_list(v, ['balance-rr', 'active-backup', 'balance-xor', 'broadcast', '802.3ad', 'balance-tlb', 'balance-alb']), + 'location': '/sys/class/net/{ifname}/bonding/mode', + }, + }} + + _sysfs_get = {**VLANIf._sysfs_get, **{ + 'bond_arp_ip_target': { + 'location': '/sys/class/net/{ifname}/bonding/arp_ip_target', + } + }} + + default = { + 'type': 'bond', + } + + def __init__(self, ifname, **kargs): + super().__init__(ifname, **kargs) + + def remove(self): + """ + Remove interface from operating system. Removing the interface + deconfigures all assigned IP addresses and clear possible DHCP(v6) + client processes. + Example: + >>> from vyos.ifconfig import Interface + >>> i = Interface('eth0') + >>> i.remove() + """ + # when a bond member gets deleted, all members are placed in A/D state + # even when they are enabled inside CLI. This will make the config + # and system look async. + slave_list = [] + for s in self.get_slaves(): + slave = { + 'ifname': s, + 'state': Interface(s).get_state() + } + slave_list.append(slave) + + # remove bond master which places members in disabled state + super().remove() + + # replicate previous interface state before bond destruction back to + # physical interface + for slave in slave_list: + i = Interface(slave['ifname']) + i.set_state(slave['state']) + + def set_hash_policy(self, mode): + """ + Selects the transmit hash policy to use for slave selection in + balance-xor, 802.3ad, and tlb modes. Possible values are: layer2, + layer2+3, layer3+4, encap2+3, encap3+4. + + The default value is layer2 + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').set_hash_policy('layer2+3') + """ + self.set_interface('bond_hash_policy', mode) + + def set_arp_interval(self, interval): + """ + Specifies the ARP link monitoring frequency in milliseconds. + + The ARP monitor works by periodically checking the slave devices + to determine whether they have sent or received traffic recently + (the precise criteria depends upon the bonding mode, and the + state of the slave). Regular traffic is generated via ARP probes + issued for the addresses specified by the arp_ip_target option. + + If ARP monitoring is used in an etherchannel compatible mode + (modes 0 and 2), the switch should be configured in a mode that + evenly distributes packets across all links. If the switch is + configured to distribute the packets in an XOR fashion, all + replies from the ARP targets will be received on the same link + which could cause the other team members to fail. + + value of 0 disables ARP monitoring. The default value is 0. + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').set_arp_interval('100') + """ + if int(interval) == 0: + """ + Specifies the MII link monitoring frequency in milliseconds. + This determines how often the link state of each slave is + inspected for link failures. A value of zero disables MII + link monitoring. A value of 100 is a good starting point. + """ + return self.set_interface('bond_miimon', interval) + else: + return self.set_interface('bond_arp_interval', interval) + + def get_arp_ip_target(self): + """ + Specifies the IP addresses to use as ARP monitoring peers when + arp_interval is > 0. These are the targets of the ARP request sent to + determine the health of the link to the targets. Specify these values + in ddd.ddd.ddd.ddd format. Multiple IP addresses must be separated by + a comma. At least one IP address must be given for ARP monitoring to + function. The maximum number of targets that can be specified is 16. + + The default value is no IP addresses. + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').get_arp_ip_target() + '192.0.2.1' + """ + return self.get_interface('bond_arp_ip_target') + + def set_arp_ip_target(self, target): + """ + Specifies the IP addresses to use as ARP monitoring peers when + arp_interval is > 0. These are the targets of the ARP request sent to + determine the health of the link to the targets. Specify these values + in ddd.ddd.ddd.ddd format. Multiple IP addresses must be separated by + a comma. At least one IP address must be given for ARP monitoring to + function. The maximum number of targets that can be specified is 16. + + The default value is no IP addresses. + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').set_arp_ip_target('192.0.2.1') + >>> BondIf('bond0').get_arp_ip_target() + '192.0.2.1' + """ + return self.set_interface('bond_arp_ip_target', target) + + def add_port(self, interface): + """ + Enslave physical interface to bond. + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').add_port('eth0') + >>> BondIf('bond0').add_port('eth1') + """ + # An interface can only be added to a bond if it is in 'down' state. If + # interface is in 'up' state, the following Kernel error will be thrown: + # bond0: eth1 is up - this may be due to an out of date ifenslave. + Interface(interface).set_state('down') + return self.set_interface('bond_add_port', f'+{interface}') + + def del_port(self, interface): + """ + Remove physical port from bond + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').del_port('eth1') + """ + return self.set_interface('bond_del_port', f'-{interface}') + + def get_slaves(self): + """ + Return a list with all configured slave interfaces on this bond. + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').get_slaves() + ['eth1', 'eth2'] + """ + enslaved_ifs = [] + # retrieve real enslaved interfaces from OS kernel + sysfs_bond = '/sys/class/net/{}'.format(self.config['ifname']) + if os.path.isdir(sysfs_bond): + for directory in os.listdir(sysfs_bond): + if 'lower_' in directory: + enslaved_ifs.append(directory.replace('lower_', '')) + + return enslaved_ifs + + def set_primary(self, interface): + """ + A string (eth0, eth2, etc) specifying which slave is the primary + device. The specified device will always be the active slave while it + is available. Only when the primary is off-line will alternate devices + be used. This is useful when one slave is preferred over another, e.g., + when one slave has higher throughput than another. + + The primary option is only valid for active-backup, balance-tlb and + balance-alb mode. + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').set_primary('eth2') + """ + return self.set_interface('bond_primary', interface) + + def set_mode(self, mode): + """ + Specifies one of the bonding policies. The default is balance-rr + (round robin). + + Possible values are: balance-rr, active-backup, balance-xor, + broadcast, 802.3ad, balance-tlb, balance-alb + + NOTE: the bonding mode can not be changed when the bond itself has + slaves + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').set_mode('802.3ad') + """ + return self.set_interface('bond_mode', mode) diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py new file mode 100644 index 000000000..392718d9f --- /dev/null +++ b/python/vyos/ifconfig/bridge.py @@ -0,0 +1,180 @@ +# Copyright 2019 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/>. + + +from vyos.ifconfig.interface import Interface + +from vyos.validate import * + +class BridgeIf(Interface): + """ + A bridge is a way to connect two Ethernet segments together in a protocol + independent way. Packets are forwarded based on Ethernet address, rather + than IP address (like a router). Since forwarding is done at Layer 2, all + protocols can go transparently through a bridge. + + The Linux bridge code implements a subset of the ANSI/IEEE 802.1d standard. + """ + + _sysfs_set = {**Interface._sysfs_set, **{ + 'ageing_time': { + 'validate': assert_positive, + 'convert': lambda t: int(t) * 100, + 'location': '/sys/class/net/{ifname}/bridge/ageing_time', + }, + 'forward_delay': { + 'validate': assert_positive, + 'convert': lambda t: int(t) * 100, + 'location': '/sys/class/net/{ifname}/bridge/forward_delay', + }, + 'hello_time': { + 'validate': assert_positive, + 'convert': lambda t: int(t) * 100, + 'location': '/sys/class/net/{ifname}/bridge/hello_time', + }, + 'max_age': { + 'validate': assert_positive, + 'convert': lambda t: int(t) * 100, + 'location': '/sys/class/net/{ifname}/bridge/max_age', + }, + 'priority': { + 'validate': assert_positive, + 'location': '/sys/class/net/{ifname}/bridge/priority', + }, + 'stp': { + 'validate': assert_boolean, + 'location': '/sys/class/net/{ifname}/bridge/stp_state', + }, + 'multicast_querier': { + 'validate': assert_boolean, + 'location': '/sys/class/net/{ifname}/bridge/multicast_querier', + }, + }} + + _command_set = {**Interface._command_set, **{ + 'add_port': { + 'shellcmd': 'ip link set dev {value} master {ifname}', + }, + 'del_port': { + 'shellcmd': 'ip link set dev {value} nomaster', + }, + }} + + default = { + 'type': 'bridge', + } + + def __init__(self, ifname, **kargs): + super().__init__(ifname, **kargs) + + def set_ageing_time(self, time): + """ + Set bridge interface MAC address aging time in seconds. Internal kernel + representation is in centiseconds. Kernel default is 300 seconds. + + Example: + >>> from vyos.ifconfig import BridgeIf + >>> BridgeIf('br0').ageing_time(2) + """ + self.set_interface('ageing_time', time) + + def set_forward_delay(self, time): + """ + Set bridge forwarding delay in seconds. Internal Kernel representation + is in centiseconds. + + Example: + >>> from vyos.ifconfig import BridgeIf + >>> BridgeIf('br0').forward_delay(15) + """ + self.set_interface('forward_delay', time) + + def set_hello_time(self, time): + """ + Set bridge hello time in seconds. Internal Kernel representation + is in centiseconds. + + Example: + >>> from vyos.ifconfig import BridgeIf + >>> BridgeIf('br0').set_hello_time(2) + """ + self.set_interface('hello_time', time) + + def set_max_age(self, time): + """ + Set bridge max message age in seconds. Internal Kernel representation + is in centiseconds. + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').set_max_age(30) + """ + self.set_interface('max_age', time) + + def set_priority(self, priority): + """ + Set bridge max aging time in seconds. + + Example: + >>> from vyos.ifconfig import BridgeIf + >>> BridgeIf('br0').set_priority(8192) + """ + self.set_interface('priority', priority) + + def set_stp(self, state): + """ + Set bridge STP (Spanning Tree) state. 0 -> STP disabled, 1 -> STP enabled + + Example: + >>> from vyos.ifconfig import BridgeIf + >>> BridgeIf('br0').set_stp(1) + """ + self.set_interface('stp', state) + + def set_multicast_querier(self, enable): + """ + Sets whether the bridge actively runs a multicast querier or not. When a + bridge receives a 'multicast host membership' query from another network + host, that host is tracked based on the time that the query was received + plus the multicast query interval time. + + Use enable=1 to enable or enable=0 to disable + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').set_multicast_querier(1) + """ + self.set_interface('multicast_querier', enable) + + def add_port(self, interface): + """ + Add physical interface to bridge (member port) + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').add_port('eth0') + >>> BridgeIf('br0').add_port('eth1') + """ + return self.set_interface('add_port', interface) + + def del_port(self, interface): + """ + Remove member port from bridge instance. + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').del_port('eth1') + """ + return self.set_interface('del_port', interface) diff --git a/python/vyos/ifconfig/control.py b/python/vyos/ifconfig/control.py new file mode 100644 index 000000000..508b4e279 --- /dev/null +++ b/python/vyos/ifconfig/control.py @@ -0,0 +1,143 @@ +# Copyright 2019 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/>. + + +import os +from subprocess import Popen, PIPE, STDOUT + + +class Control: + _command_get = {} + _command_set = {} + + def _debug_msg(self, msg): + if os.path.isfile('/tmp/vyos.ifconfig.debug'): + print('DEBUG/{:<6} {}'.format(self.config['ifname'], msg)) + + def _cmd(self, command): + p = Popen(command, stdout=PIPE, stderr=STDOUT, shell=True) + tmp = p.communicate()[0].strip() + self._debug_msg("cmd '{}'".format(command)) + if tmp.decode(): + self._debug_msg("returned:\n{}".format(tmp.decode())) + + # do we need some error checking code here? + return tmp.decode() + + def _get_command(self, config, name): + """ + Using the defined names, set data write to sysfs. + """ + cmd = self._command_get[name]['shellcmd'].format(**config) + return self._cmd(cmd) + + def _set_command(self, config, name, value): + """ + Using the defined names, set data write to sysfs. + """ + if not value: + return None + + # the code can pass int as int + value = str(value) + + validate = self._command_set[name].get('validate', None) + if validate: + validate(value) + + config = {**config, **{'value': value}} + + convert = self._command_set[name].get('convert', None) + if convert: + value = convert(value) + + cmd = self._command_set[name]['shellcmd'].format(**config) + return self._cmd(cmd) + + _sysfs_get = {} + _sysfs_set = {} + + def _read_sysfs(self, filename): + """ + Provide a single primitive w/ error checking for reading from sysfs. + """ + value = None + with open(filename, 'r') as f: + value = f.read().rstrip('\n') + + self._debug_msg("read '{}' < '{}'".format(value, filename)) + return value + + def _write_sysfs(self, filename, value): + """ + Provide a single primitive w/ error checking for writing to sysfs. + """ + self._debug_msg("write '{}' > '{}'".format(value, filename)) + if os.path.isfile(filename): + with open(filename, 'w') as f: + f.write(str(value)) + return True + return False + + def _get_sysfs(self, config, name): + """ + Using the defined names, get data write from sysfs. + """ + filename = self._sysfs_get[name]['location'].format(**config) + if not filename: + return None + return self._read_sysfs(filename) + + def _set_sysfs(self, config, name, value): + """ + Using the defined names, set data write to sysfs. + """ + if not value: + return None + + # the code can pass int as int + value = str(value) + + validate = self._sysfs_set[name].get('validate', None) + if validate: + validate(value) + + config = {**config, **{'value': value}} + + convert = self._sysfs_set[name].get('convert', None) + if convert: + value = convert(value) + + commited = self._write_sysfs( + self._sysfs_set[name]['location'].format(**config), value) + if not commited: + errmsg = self._sysfs_set.get('errormsg', '') + if errmsg: + raise TypeError(errmsg.format(**config)) + return commited + + def get_interface(self, name): + if name in self._sysfs_get: + return self._get_sysfs(self.config, name) + if name in self._command_get: + return self._get_command(self.config, name) + raise KeyError(f'{name} is not a attribute of the interface we can get') + + def set_interface(self, name, value): + if name in self._sysfs_set: + return self._set_sysfs(self.config, name, value) + if name in self._command_set: + return self._set_command(self.config, name, value) + raise KeyError(f'{name} is not a attribute of the interface we can set') diff --git a/python/vyos/ifconfig/dummy.py b/python/vyos/ifconfig/dummy.py new file mode 100644 index 000000000..55935a16e --- /dev/null +++ b/python/vyos/ifconfig/dummy.py @@ -0,0 +1,32 @@ +# Copyright 2019 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/>. + + +from vyos.ifconfig.interface import Interface + + +class DummyIf(Interface): + """ + A dummy interface is entirely virtual like, for example, the loopback + interface. The purpose of a dummy interface is to provide a device to route + packets through without actually transmitting them. + """ + + default = { + 'type': 'dummy', + } + + def __init__(self, ifname, **kargs): + super().__init__(ifname, **kargs) diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py new file mode 100644 index 000000000..9863ca826 --- /dev/null +++ b/python/vyos/ifconfig/ethernet.py @@ -0,0 +1,234 @@ +# Copyright 2019 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/>. + +import os +import re + +from vyos.ifconfig.vlan import VLANIf + +from vyos.validate import * + + +class EthernetIf(VLANIf): + """ + Abstraction of a Linux Ethernet Interface + """ + + _command_set = {**VLANIf._command_set, **{ + 'gro': { + 'validate': lambda v: assert_list(v, ['on', 'off']), + 'shellcmd': '/sbin/ethtool -K {ifname} gro {value}', + }, + 'gso': { + 'validate': lambda v: assert_list(v, ['on', 'off']), + 'shellcmd': '/sbin/ethtool -K {ifname} gso {value}', + }, + 'sg': { + 'validate': lambda v: assert_list(v, ['on', 'off']), + 'shellcmd': '/sbin/ethtool -K {ifname} sg {value}', + }, + 'tso': { + 'validate': lambda v: assert_list(v, ['on', 'off']), + 'shellcmd': '/sbin/ethtool -K {ifname} tso {value}', + }, + 'ufo': { + 'validate': lambda v: assert_list(v, ['on', 'off']), + 'shellcmd': '/sbin/ethtool -K {ifname} ufo {value}', + }, + }} + + default = { + 'type': 'ethernet', + } + + def __init__(self, ifname, **kargs): + super().__init__(ifname, **kargs) + + def _delete(self): + # Ethernet interfaces can not be removed + pass + + def get_driver_name(self): + """ + Return the driver name used by NIC. Some NICs don't support all + features e.g. changing link-speed, duplex + + Example: + >>> from vyos.ifconfig import EthernetIf + >>> i = EthernetIf('eth0') + >>> i.get_driver_name() + 'vmxnet3' + """ + sysfs_file = '/sys/class/net/{}/device/driver/module'.format( + self.config['ifname']) + if os.path.exists(sysfs_file): + link = os.readlink(sysfs_file) + return os.path.basename(link) + else: + return None + + def set_flow_control(self, enable): + """ + Changes the pause parameters of the specified Ethernet device. + + @param enable: true -> enable pause frames, false -> disable pause frames + + Example: + >>> from vyos.ifconfig import EthernetIf + >>> i = EthernetIf('eth0') + >>> i.set_flow_control(True) + """ + if enable not in ['on', 'off']: + raise ValueError("Value out of range") + + if self.get_driver_name() in ['vmxnet3', 'virtio_net']: + self._debug_msg('{} driver does not support changing flow control settings!' + .format(self.get_driver_name())) + return + + # Get current flow control settings: + cmd = '/sbin/ethtool --show-pause {0}'.format(self.config['ifname']) + tmp = self._cmd(cmd) + + # The above command returns - with tabs: + # + # Pause parameters for eth0: + # Autonegotiate: on + # RX: off + # TX: off + if re.search("Autonegotiate:\ton", tmp): + if enable == "on": + # flowcontrol is already enabled - no need to re-enable it again + # this will prevent the interface from flapping as applying the + # flow-control settings will take the interface down and bring + # it back up every time. + return + + # Assemble command executed on system. Unfortunately there is no way + # to change this setting via sysfs + cmd = '/sbin/ethtool --pause {0} autoneg {1} tx {1} rx {1}'.format( + self.config['ifname'], enable) + try: + # An exception will be thrown if the settings are not changed + return self._cmd(cmd) + except CalledProcessError: + pass + + def set_speed_duplex(self, speed, duplex): + """ + Set link speed in Mbit/s and duplex. + + @speed can be any link speed in MBit/s, e.g. 10, 100, 1000 auto + @duplex can be half, full, auto + + Example: + >>> from vyos.ifconfig import EthernetIf + >>> i = EthernetIf('eth0') + >>> i.set_speed_duplex('auto', 'auto') + """ + + if speed not in ['auto', '10', '100', '1000', '2500', '5000', '10000', '25000', '40000', '50000', '100000', '400000']: + raise ValueError("Value out of range (speed)") + + if duplex not in ['auto', 'full', 'half']: + raise ValueError("Value out of range (duplex)") + + if self.get_driver_name() in ['vmxnet3', 'virtio_net']: + self._debug_msg('{} driver does not support changing speed/duplex settings!' + .format(self.get_driver_name())) + return + + # Get current speed and duplex settings: + cmd = '/sbin/ethtool {0}'.format(self.config['ifname']) + tmp = self._cmd(cmd) + + if re.search("\tAuto-negotiation: on", tmp): + if speed == 'auto' and duplex == 'auto': + # bail out early as nothing is to change + return + else: + # read in current speed and duplex settings + cur_speed = 0 + cur_duplex = '' + for line in tmp.splitlines(): + if line.lstrip().startswith("Speed:"): + non_decimal = re.compile(r'[^\d.]+') + cur_speed = non_decimal.sub('', line) + continue + + if line.lstrip().startswith("Duplex:"): + cur_duplex = line.split()[-1].lower() + break + + if (cur_speed == speed) and (cur_duplex == duplex): + # bail out early as nothing is to change + return + + cmd = '/sbin/ethtool -s {}'.format(self.config['ifname']) + if speed == 'auto' or duplex == 'auto': + cmd += ' autoneg on' + else: + cmd += ' speed {} duplex {} autoneg off'.format(speed, duplex) + + return self._cmd(cmd) + + def set_gro(self, state): + """ + Example: + >>> from vyos.ifconfig import EthernetIf + >>> i = EthernetIf('eth0') + >>> i.set_gro('on') + """ + return self.set_interface('gro', state) + + def set_gso(self, state): + """ + Example: + >>> from vyos.ifconfig import EthernetIf + >>> i = EthernetIf('eth0') + >>> i.set_gso('on') + """ + return self.set_interface('gso', state) + + def set_sg(self, state): + """ + Example: + >>> from vyos.ifconfig import EthernetIf + >>> i = EthernetIf('eth0') + >>> i.set_sg('on') + """ + return self.set_interface('sg', state) + + def set_tso(self, state): + """ + Example: + >>> from vyos.ifconfig import EthernetIf + >>> i = EthernetIf('eth0') + >>> i.set_tso('on') + """ + return self.set_interface('tso', state) + + def set_ufo(self, state): + """ + Example: + >>> from vyos.ifconfig import EthernetIf + >>> i = EthernetIf('eth0') + >>> i.set_udp_offload('on') + """ + if state not in ['on', 'off']: + raise ValueError('state must be "on" or "off"') + + cmd = '/sbin/ethtool -K {} ufo {}'.format(self.config['ifname'], state) + return self._cmd(cmd) diff --git a/python/vyos/ifconfig/geneve.py b/python/vyos/ifconfig/geneve.py new file mode 100644 index 000000000..46782a685 --- /dev/null +++ b/python/vyos/ifconfig/geneve.py @@ -0,0 +1,60 @@ +# Copyright 2019 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/>. + + +from vyos.ifconfig.interface import Interface + + +class GeneveIf(Interface): + """ + Geneve: Generic Network Virtualization Encapsulation + + For more information please refer to: + https://tools.ietf.org/html/draft-gross-geneve-00 + https://www.redhat.com/en/blog/what-geneve + https://developers.redhat.com/blog/2019/05/17/an-introduction-to-linux-virtual-interfaces-tunnels/#geneve + https://lwn.net/Articles/644938/ + """ + + default = { + 'type': 'geneve', + } + + def __init__(self, ifname, **kargs): + super().__init__(ifname, **kargs) + + def _create(self): + cmd = 'ip link add name {} type geneve id {} remote {}' \ + .format(self.config['ifname'], config['vni'], config['remote']) + self._cmd(cmd) + + # interface is always A/D down. It needs to be enabled explicitly + self.set_state('down') + + @staticmethod + def get_config(): + """ + GENEVE interfaces require a configuration when they are added using + iproute2. This static method will provide the configuration dictionary + used by this class. + + Example: + >> dict = GeneveIf().get_config() + """ + config = { + 'vni': 0, + 'remote': '' + } + return config diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py new file mode 100644 index 000000000..a750bda3f --- /dev/null +++ b/python/vyos/ifconfig/interface.py @@ -0,0 +1,836 @@ +# Copyright 2019 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/>. + +import os +import re +import jinja2 +import json +import glob +import time +from copy import deepcopy + +import vyos.interfaces + +from vyos.validate import * # should not * include +from vyos.config import Config # not used anymore +from vyos import ConfigError + +from ipaddress import IPv4Network, IPv6Address +from netifaces import ifaddresses, AF_INET, AF_INET6 +from time import sleep +from os.path import isfile +from tabulate import tabulate +from hurry.filesize import size,alternative +from datetime import timedelta + +from vyos.ifconfig.control import Control + +dhclient_base = r'/var/lib/dhcp/dhclient_' +dhcp_cfg = """ +# generated by ifconfig.py +option rfc3442-classless-static-routes code 121 = array of unsigned integer 8; +timeout 60; +retry 300; + +interface "{{ intf }}" { + send host-name "{{ hostname }}"; + {% if client_id -%} + send dhcp-client-identifier "{{ client_id }}"; + {% endif -%} + {% if vendor_class_id -%} + send vendor-class-identifier "{{ vendor_class_id }}"; + {% endif -%} + request subnet-mask, broadcast-address, routers, domain-name-servers, + rfc3442-classless-static-routes, domain-name, interface-mtu; + require subnet-mask; +} + +""" + +dhcpv6_cfg = """ +# generated by ifconfig.py +interface "{{ intf }}" { + request routers, domain-name-servers, domain-name; +} + +""" + + + +class Interface(Control): + options = [] + required = [] + default = { + 'type': '', + } + + _command_set = { + 'mac': { + 'validate': assert_mac, + 'shellcmd': 'ip link set dev {ifname} address {value}', + }, + 'add_vrf': { + 'shellcmd': 'ip link set dev {ifname} master {value}', + }, + 'del_vrf': { + 'shellcmd': 'ip link set dev {ifname} nomaster {value}', + }, + } + + _sysfs_get = { + 'mtu': { + 'location': '/sys/class/net/{ifname}/mtu', + }, + } + + _sysfs_set = { + 'alias': { + 'convert': lambda name: name if name else '\0', + 'location': '/sys/class/net/{ifname}/ifalias', + }, + 'mtu': { + 'validate': assert_mtu, + 'location': '/sys/class/net/{ifname}/mtu', + }, + 'arp_cache_tmo': { + 'convert': lambda tmo: (int(tmo) * 1000), + 'location': '/proc/sys/net/ipv4/neigh/{ifname}/base_reachable_time_ms', + }, + 'arp_filter': { + 'validate': assert_boolean, + 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_filter', + }, + 'arp_accept': { + 'validate': lambda arp: assert_range(arp,0,2), + 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_accept', + }, + 'arp_announce': { + 'validate': assert_boolean, + 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_announce', + }, + 'arp_ignore': { + 'validate': assert_boolean, + 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_ignore', + }, + 'proxy_arp': { + 'validate': assert_boolean, + 'location': '/proc/sys/net/ipv4/conf/{ifname}/proxy_arp', + }, + 'proxy_arp_pvlan': { + 'validate': assert_boolean, + 'location': '/proc/sys/net/ipv4/conf/{ifname}/proxy_arp_pvlan', + }, + # link_detect vs link_filter name weirdness + 'link_detect': { + 'validate': lambda link: assert_range(link,0,3), + 'location': '/proc/sys/net/ipv4/conf/{ifname}/link_filter', + }, + } + + def __init__(self, ifname, **kargs): + """ + This is the base interface class which supports basic IP/MAC address + operations as well as DHCP(v6). Other interface which represent e.g. + and ethernet bridge are implemented as derived classes adding all + additional functionality. + + For creation you will need to provide the interface type, otherwise + the existing interface is used + + DEBUG: + This class has embedded debugging (print) which can be enabled by + creating the following file: + vyos@vyos# touch /tmp/vyos.ifconfig.debug + + Example: + >>> from vyos.ifconfig import Interface + >>> i = Interface('eth0') + """ + + self.config = deepcopy(self.default) + self.config['ifname'] = ifname + + for k in self.options: + if k in kargs: + self.config[k] = kargs[k] + + for k in self.required: + if k not in kargs: + raise ConfigError('missing required option {} for {}'.format(k,self.__class__)) + + if not os.path.exists('/sys/class/net/{}'.format(self.config['ifname'])): + if not self.config['type']: + raise Exception('interface "{}" not found'.format(self.config['ifname'])) + self._create() + + # per interface DHCP config files + self._dhcp_cfg_file = dhclient_base + self.config['ifname'] + '.conf' + self._dhcp_pid_file = dhclient_base + self.config['ifname'] + '.pid' + self._dhcp_lease_file = dhclient_base + self.config['ifname'] + '.leases' + + # per interface DHCPv6 config files + self._dhcpv6_cfg_file = dhclient_base + self.config['ifname'] + '.v6conf' + self._dhcpv6_pid_file = dhclient_base + self.config['ifname'] + '.v6pid' + self._dhcpv6_lease_file = dhclient_base + self.config['ifname'] + '.v6leases' + + # DHCP options + self._dhcp_options = { + 'intf' : self.config['ifname'], + 'hostname' : '', + 'client_id' : '', + 'vendor_class_id' : '' + } + + # DHCPv6 options + self._dhcpv6_options = { + 'intf' : self.config['ifname'], + 'dhcpv6_prm_only' : False, + 'dhcpv6_temporary' : False + } + + # list of assigned IP addresses + self._addr = [] + + def _create(self): + cmd = 'ip link add dev {ifname} type {type}'.format(**self.config) + self._cmd(cmd) + + def remove(self): + """ + Remove interface from operating system. Removing the interface + deconfigures all assigned IP addresses and clear possible DHCP(v6) + client processes. + + Example: + >>> from vyos.ifconfig import Interface + >>> i = Interface('eth0') + >>> i.remove() + """ + # stop DHCP(v6) if running + self._del_dhcp() + self._del_dhcpv6() + + # remove all assigned IP addresses from interface - this is a bit redundant + # as the kernel will remove all addresses on interface deletion, but we + # can not delete ALL interfaces, see below + for addr in self.get_addr(): + self.del_addr(addr) + + # --------------------------------------------------------------------- + # A code refactoring is required as this type check is present as + # Interface implement behaviour for one of it's sub-class. + + # It is required as the current pattern for vlan is: + # Interface('name').remove() to delete an interface + # The code should be modified to have a class method called connect and + # have Interface.connect('name').remove() + + # each subclass should register within Interface the pattern for that + # interface ie: (ethX, etc.) and use this to create an instance of + # the right class (EthernetIf, ...) + + # Ethernet interfaces can not be removed + + # Commented out as nowhere in the code do we call Interface() + # This would also cause an import loop + # if self.__class__ == EthernetIf: + # return + + # --------------------------------------------------------------------- + + self._delete() + + def _delete(self): + # NOTE (Improvement): + # after interface removal no other commands should be allowed + # to be called and instead should raise an Exception: + cmd = 'ip link del dev {}'.format(self.config['ifname']) + return self._cmd(cmd) + + def get_mtu(self): + """ + Get/set interface mtu in bytes. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').get_mtu() + '1500' + """ + return self.get_interface('mtu') + + def set_mtu(self, mtu): + """ + Get/set interface mtu in bytes. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_mtu(1400) + >>> Interface('eth0').get_mtu() + '1400' + """ + return self.set_interface('mtu', mtu) + + def set_mac(self, mac): + """ + Set interface MAC (Media Access Contrl) address to given value. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_mac('00:50:ab:cd:ef:01') + """ + self.set_interface('mac', mac) + + def add_vrf(self, vrf): + """ + Add interface to given VRF instance. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').add_vrf('foo') + """ + self.set_interface('add_vrf', vrf) + + def del_vrf(self, vrf): + """ + Remove interface from given VRF instance. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').del_vrf('foo') + """ + self.set_interface('del_vrf', vrf) + + def set_arp_cache_tmo(self, tmo): + """ + Set ARP cache timeout value in seconds. Internal Kernel representation + is in milliseconds. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_arp_cache_tmo(40) + """ + return self.set_interface('arp_cache_tmo', tmo) + + def set_arp_filter(self, arp_filter): + """ + Filter ARP requests + + 1 - Allows you to have multiple network interfaces on the same + subnet, and have the ARPs for each interface be answered + based on whether or not the kernel would route a packet from + the ARP'd IP out that interface (therefore you must use source + based routing for this to work). In other words it allows control + of which cards (usually 1) will respond to an arp request. + + 0 - (default) The kernel can respond to arp requests with addresses + from other interfaces. This may seem wrong but it usually makes + sense, because it increases the chance of successful communication. + IP addresses are owned by the complete host on Linux, not by + particular interfaces. Only for more complex setups like load- + balancing, does this behaviour cause problems. + """ + return self.set_interface('arp_filter', arp_filter) + + def set_arp_accept(self, arp_accept): + """ + Define behavior for gratuitous ARP frames who's IP is not + already present in the ARP table: + 0 - don't create new entries in the ARP table + 1 - create new entries in the ARP table + + Both replies and requests type gratuitous arp will trigger the + ARP table to be updated, if this setting is on. + + If the ARP table already contains the IP address of the + gratuitous arp frame, the arp table will be updated regardless + if this setting is on or off. + """ + return self.set_interface('arp_accept', arp_accept) + + def set_arp_announce(self, arp_announce): + """ + Define different restriction levels for announcing the local + source IP address from IP packets in ARP requests sent on + interface: + 0 - (default) Use any local address, configured on any interface + 1 - Try to avoid local addresses that are not in the target's + subnet for this interface. This mode is useful when target + hosts reachable via this interface require the source IP + address in ARP requests to be part of their logical network + configured on the receiving interface. When we generate the + request we will check all our subnets that include the + target IP and will preserve the source address if it is from + such subnet. + + Increasing the restriction level gives more chance for + receiving answer from the resolved target while decreasing + the level announces more valid sender's information. + """ + return self.set_interface('arp_announce', arp_announce) + + def set_arp_ignore(self, arp_ignore): + """ + Define different modes for sending replies in response to received ARP + requests that resolve local target IP addresses: + + 0 - (default): reply for any local target IP address, configured + on any interface + 1 - reply only if the target IP address is local address + configured on the incoming interface + """ + return self.set_interface('arp_ignore', arp_ignore) + + def set_link_detect(self, link_filter): + """ + Configure kernel response in packets received on interfaces that are 'down' + + 0 - Allow packets to be received for the address on this interface + even if interface is disabled or no carrier. + + 1 - Ignore packets received if interface associated with the incoming + address is down. + + 2 - Ignore packets received if interface associated with the incoming + address is down or has no carrier. + + Default value is 0. Note that some distributions enable it in startup + scripts. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_link_detect(1) + """ + return self.set_interface('link_detect', link_filter) + + def set_alias(self, ifalias=''): + """ + Set interface alias name used by e.g. SNMP + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_alias('VyOS upstream interface') + + to clear alias e.g. delete it use: + + >>> Interface('eth0').set_ifalias('') + """ + self.set_interface('alias', ifalias) + + def get_state(self): + """ + Enable (up) / Disable (down) an interface + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').get_state() + 'up' + """ + cmd = 'ip -json link show dev {}'.format(self.config['ifname']) + tmp = self._cmd(cmd) + out = json.loads(tmp) + return out[0]['operstate'].lower() + + def set_state(self, state): + """ + Enable (up) / Disable (down) an interface + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_state('down') + >>> Interface('eth0').get_state() + 'down' + """ + if state not in ['up', 'down']: + raise ValueError('state must be "up" or "down"') + + # Assemble command executed on system. Unfortunately there is no way + # to up/down an interface via sysfs + cmd = 'ip link set dev {} {}'.format(self.config['ifname'], state) + return self._cmd(cmd) + + def set_proxy_arp(self, enable): + """ + Set per interface proxy ARP configuration + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_proxy_arp(1) + """ + self.set_interface('proxy_arp', enable) + + def set_proxy_arp_pvlan(self, enable): + """ + Private VLAN proxy arp. + Basically allow proxy arp replies back to the same interface + (from which the ARP request/solicitation was received). + + This is done to support (ethernet) switch features, like RFC + 3069, where the individual ports are NOT allowed to + communicate with each other, but they are allowed to talk to + the upstream router. As described in RFC 3069, it is possible + to allow these hosts to communicate through the upstream + router by proxy_arp'ing. Don't need to be used together with + proxy_arp. + + This technology is known by different names: + In RFC 3069 it is called VLAN Aggregation. + Cisco and Allied Telesyn call it Private VLAN. + Hewlett-Packard call it Source-Port filtering or port-isolation. + Ericsson call it MAC-Forced Forwarding (RFC Draft). + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_proxy_arp_pvlan(1) + """ + self.set_interface('proxy_arp_pvlan', enable) + + def get_addr(self): + """ + Retrieve assigned IPv4 and IPv6 addresses from given interface. + This is done using the netifaces and ipaddress python modules. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').get_addrs() + ['172.16.33.30/24', 'fe80::20c:29ff:fe11:a174/64'] + """ + + ipv4 = [] + ipv6 = [] + + if AF_INET in ifaddresses(self.config['ifname']).keys(): + for v4_addr in ifaddresses(self.config['ifname'])[AF_INET]: + # we need to manually assemble a list of IPv4 address/prefix + prefix = '/' + \ + str(IPv4Network('0.0.0.0/' + v4_addr['netmask']).prefixlen) + ipv4.append(v4_addr['addr'] + prefix) + + if AF_INET6 in ifaddresses(self.config['ifname']).keys(): + for v6_addr in ifaddresses(self.config['ifname'])[AF_INET6]: + # Note that currently expanded netmasks are not supported. That means + # 2001:db00::0/24 is a valid argument while 2001:db00::0/ffff:ff00:: not. + # see https://docs.python.org/3/library/ipaddress.html + bits = bin( + int(v6_addr['netmask'].replace(':', ''), 16)).count('1') + prefix = '/' + str(bits) + + # we alsoneed to remove the interface suffix on link local + # addresses + v6_addr['addr'] = v6_addr['addr'].split('%')[0] + ipv6.append(v6_addr['addr'] + prefix) + + return ipv4 + ipv6 + + def add_addr(self, addr): + """ + Add IP(v6) address to interface. Address is only added if it is not + already assigned to that interface. + + addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6! + IPv4: add IPv4 address to interface + IPv6: add IPv6 address to interface + dhcp: start dhclient (IPv4) on interface + dhcpv6: start dhclient (IPv6) on interface + + Example: + >>> from vyos.ifconfig import Interface + >>> j = Interface('eth0') + >>> j.add_addr('192.0.2.1/24') + >>> j.add_addr('2001:db8::ffff/64') + >>> j.get_addr() + ['192.0.2.1/24', '2001:db8::ffff/64'] + """ + + # cache new IP address which is assigned to interface + self._addr.append(addr) + + # we can not have both DHCP and static IPv4 addresses assigned to an interface + if 'dhcp' in self._addr: + for addr in self._addr: + # do not change below 'if' ordering esle you will get an exception as: + # ValueError: 'dhcp' does not appear to be an IPv4 or IPv6 address + if addr != 'dhcp' and is_ipv4(addr): + raise ConfigError("Can't configure both static IPv4 and DHCP address on the same interface") + + if addr == 'dhcp': + self._set_dhcp() + elif addr == 'dhcpv6': + self._set_dhcpv6() + else: + if not is_intf_addr_assigned(self.config['ifname'], addr): + cmd = 'ip addr add "{}" dev "{}"'.format(addr, self.config['ifname']) + return self._cmd(cmd) + + def del_addr(self, addr): + """ + Delete IP(v6) address to interface. Address is only added if it is + assigned to that interface. + + addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6! + IPv4: delete IPv4 address from interface + IPv6: delete IPv6 address from interface + dhcp: stop dhclient (IPv4) on interface + dhcpv6: stop dhclient (IPv6) on interface + + Example: + >>> from vyos.ifconfig import Interface + >>> j = Interface('eth0') + >>> j.add_addr('2001:db8::ffff/64') + >>> j.add_addr('192.0.2.1/24') + >>> j.get_addr() + ['192.0.2.1/24', '2001:db8::ffff/64'] + >>> j.del_addr('192.0.2.1/24') + >>> j.get_addr() + ['2001:db8::ffff/64'] + """ + if addr == 'dhcp': + self._del_dhcp() + elif addr == 'dhcpv6': + self._del_dhcpv6() + else: + if is_intf_addr_assigned(self.config['ifname'], addr): + cmd = 'ip addr del "{}" dev "{}"'.format(addr, self.config['ifname']) + return self._cmd(cmd) + + + def get_dhcp_options(self): + """ + Return dictionary with supported DHCP options. + + Dictionary should be altered and send back via set_dhcp_options() + so those options are applied when DHCP is run. + """ + return self._dhcp_options + + def set_dhcp_options(self, options): + """ + Store new DHCP options used by next run of DHCP client. + """ + self._dhcp_options = options + + def get_dhcpv6_options(self): + """ + Return dictionary with supported DHCPv6 options. + + Dictionary should be altered and send back via set_dhcp_options() + so those options are applied when DHCP is run. + """ + return self._dhcpv6_options + + def set_dhcpv6_options(self, options): + """ + Store new DHCP options used by next run of DHCP client. + """ + self._dhcpv6_options = options + + # replace dhcpv4/v6 with systemd.networkd? + def _set_dhcp(self): + """ + Configure interface as DHCP client. The dhclient binary is automatically + started in background! + + Example: + + >>> from vyos.ifconfig import Interface + >>> j = Interface('eth0') + >>> j.set_dhcp() + """ + + dhcp = self.get_dhcp_options() + if not dhcp['hostname']: + # read configured system hostname. + # maybe change to vyos hostd client ??? + with open('/etc/hostname', 'r') as f: + dhcp['hostname'] = f.read().rstrip('\n') + + # render DHCP configuration + tmpl = jinja2.Template(dhcp_cfg) + dhcp_text = tmpl.render(dhcp) + with open(self._dhcp_cfg_file, 'w') as f: + f.write(dhcp_text) + + cmd = 'start-stop-daemon --start --quiet --pidfile ' + \ + self._dhcp_pid_file + cmd += ' --exec /sbin/dhclient --' + # now pass arguments to dhclient binary + cmd += ' -4 -nw -cf {} -pf {} -lf {} {}'.format( + self._dhcp_cfg_file, self._dhcp_pid_file, self._dhcp_lease_file, self.config['ifname']) + return self._cmd(cmd) + + + def _del_dhcp(self): + """ + De-configure interface as DHCP clinet. All auto generated files like + pid, config and lease will be removed. + + Example: + + >>> from vyos.ifconfig import Interface + >>> j = Interface('eth0') + >>> j.del_dhcp() + """ + pid = 0 + if os.path.isfile(self._dhcp_pid_file): + with open(self._dhcp_pid_file, 'r') as f: + pid = int(f.read()) + else: + self._debug_msg('No DHCP client PID found') + return None + + # stop dhclient, we need to call dhclient and tell it should release the + # aquired IP address. tcpdump tells me: + # 172.16.35.103.68 > 172.16.35.254.67: [bad udp cksum 0xa0cb -> 0xb943!] BOOTP/DHCP, Request from 00:50:56:9d:11:df, length 300, xid 0x620e6946, Flags [none] (0x0000) + # Client-IP 172.16.35.103 + # Client-Ethernet-Address 00:50:56:9d:11:df + # Vendor-rfc1048 Extensions + # Magic Cookie 0x63825363 + # DHCP-Message Option 53, length 1: Release + # Server-ID Option 54, length 4: 172.16.35.254 + # Hostname Option 12, length 10: "vyos" + # + cmd = '/sbin/dhclient -cf {} -pf {} -lf {} -r {}'.format( + self._dhcp_cfg_file, self._dhcp_pid_file, self._dhcp_lease_file, self.config['ifname']) + self._cmd(cmd) + + # cleanup old config file + if os.path.isfile(self._dhcp_cfg_file): + os.remove(self._dhcp_cfg_file) + + # cleanup old pid file + if os.path.isfile(self._dhcp_pid_file): + os.remove(self._dhcp_pid_file) + + # cleanup old lease file + if os.path.isfile(self._dhcp_lease_file): + os.remove(self._dhcp_lease_file) + + + def _set_dhcpv6(self): + """ + Configure interface as DHCPv6 client. The dhclient binary is automatically + started in background! + + Example: + + >>> from vyos.ifconfig import Interface + >>> j = Interface('eth0') + >>> j.set_dhcpv6() + """ + dhcpv6 = self.get_dhcpv6_options() + + # better save then sorry .. should be checked in interface script + # but if you missed it we are safe! + if dhcpv6['dhcpv6_prm_only'] and dhcpv6['dhcpv6_temporary']: + raise Exception('DHCPv6 temporary and parameters-only options are mutually exclusive!') + + # render DHCP configuration + tmpl = jinja2.Template(dhcpv6_cfg) + dhcpv6_text = tmpl.render(dhcpv6) + with open(self._dhcpv6_cfg_file, 'w') as f: + f.write(dhcpv6_text) + + # https://bugs.launchpad.net/ubuntu/+source/ifupdown/+bug/1447715 + # + # wee need to wait for IPv6 DAD to finish once and interface is added + # this suxx :-( + sleep(5) + + # no longer accept router announcements on this interface + self._write_sysfs('/proc/sys/net/ipv6/conf/{}/accept_ra' + .format(self.config['ifname']), 0) + + # assemble command-line to start DHCPv6 client (dhclient) + cmd = 'start-stop-daemon --start --quiet --pidfile ' + \ + self._dhcpv6_pid_file + cmd += ' --exec /sbin/dhclient --' + # now pass arguments to dhclient binary + cmd += ' -6 -nw -cf {} -pf {} -lf {}'.format( + self._dhcpv6_cfg_file, self._dhcpv6_pid_file, self._dhcpv6_lease_file) + + # add optional arguments + if dhcpv6['dhcpv6_prm_only']: + cmd += ' -S' + if dhcpv6['dhcpv6_temporary']: + cmd += ' -T' + + cmd += ' {}'.format(self.config['ifname']) + return self._cmd(cmd) + + + def _del_dhcpv6(self): + """ + De-configure interface as DHCPv6 clinet. All auto generated files like + pid, config and lease will be removed. + + Example: + + >>> from vyos.ifconfig import Interface + >>> j = Interface('eth0') + >>> j.del_dhcpv6() + """ + pid = 0 + if os.path.isfile(self._dhcpv6_pid_file): + with open(self._dhcpv6_pid_file, 'r') as f: + pid = int(f.read()) + else: + self._debug_msg('No DHCPv6 client PID found') + return None + + # stop dhclient + cmd = 'start-stop-daemon --stop --quiet --pidfile {}'.format(self._dhcpv6_pid_file) + self._cmd(cmd) + + # accept router announcements on this interface + self._write_sysfs('/proc/sys/net/ipv6/conf/{}/accept_ra' + .format(self.config['ifname']), 1) + + # cleanup old config file + if os.path.isfile(self._dhcpv6_cfg_file): + os.remove(self._dhcpv6_cfg_file) + + # cleanup old pid file + if os.path.isfile(self._dhcpv6_pid_file): + os.remove(self._dhcpv6_pid_file) + + # cleanup old lease file + if os.path.isfile(self._dhcpv6_lease_file): + os.remove(self._dhcpv6_lease_file) + + def op_show_interface_stats(self): + stats = self.get_interface_stats() + rx = [['bytes','packets','errors','dropped','overrun','mcast'],[stats['rx_bytes'],stats['rx_packets'],stats['rx_errors'],stats['rx_dropped'],stats['rx_over_errors'],stats['multicast']]] + tx = [['bytes','packets','errors','dropped','carrier','collisions'],[stats['tx_bytes'],stats['tx_packets'],stats['tx_errors'],stats['tx_dropped'],stats['tx_carrier_errors'],stats['collisions']]] + output = "RX: \n" + output += tabulate(rx,headers="firstrow",numalign="right",tablefmt="plain") + output += "\n\nTX: \n" + output += tabulate(tx,headers="firstrow",numalign="right",tablefmt="plain") + print(' '.join(('\n'+output.lstrip()).splitlines(True))) + + def get_interface_stats(self): + interface_stats = dict() + devices = [f for f in glob.glob("/sys/class/net/**/statistics")] + for dev_path in devices: + metrics = [f for f in glob.glob(dev_path +"/**")] + dev = re.findall(r"/sys/class/net/(.*)/statistics",dev_path)[0] + dev_dict = dict() + for metric_path in metrics: + metric = metric_path.replace(dev_path+"/","") + if isfile(metric_path): + data = open(metric_path, 'r').read()[:-1] + dev_dict[metric] = int(data) + interface_stats[dev] = dev_dict + + return interface_stats[self.config['ifname']] + diff --git a/python/vyos/ifconfig/l2tpv3.py b/python/vyos/ifconfig/l2tpv3.py new file mode 100644 index 000000000..491fd24a7 --- /dev/null +++ b/python/vyos/ifconfig/l2tpv3.py @@ -0,0 +1,108 @@ +# Copyright 2019 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/>. + + +import os + +from vyos.ifconfig.interface import Interface + + +class L2TPv3If(Interface): + """ + The Linux bonding driver provides a method for aggregating multiple network + interfaces into a single logical "bonded" interface. The behavior of the + bonded interfaces depends upon the mode; generally speaking, modes provide + either hot standby or load balancing services. Additionally, link integrity + monitoring may be performed. + """ + + options = Interface.options + \ + ['tunnel_id', 'peer_tunnel_id', 'local_port', 'remote_port', + 'encapsulation', 'local_address', 'remote_address'] + default = { + 'type': 'l2tp', + } + + def __init__(self, ifname, **kargs): + super().__init__(ifname, **kargs) + + def _create(self): + # create tunnel interface + cmd = 'ip l2tp add tunnel tunnel_id {} '.format(config['tunnel_id']) + cmd += 'peer_tunnel_id {} '.format(config['peer_tunnel_id']) + cmd += 'udp_sport {} '.format(config['local_port']) + cmd += 'udp_dport {} '.format(config['remote_port']) + cmd += 'encap {} '.format(config['encapsulation']) + cmd += 'local {} '.format(config['local_address']) + cmd += 'remote {} '.format(config['remote_address']) + self._cmd(cmd) + + # setup session + cmd = 'ip l2tp add session name {} '.format(self.config['ifname']) + cmd += 'tunnel_id {} '.format(config['tunnel_id']) + cmd += 'session_id {} '.format(config['session_id']) + cmd += 'peer_session_id {} '.format(config['peer_session_id']) + self._cmd(cmd) + + # interface is always A/D down. It needs to be enabled explicitly + self.set_state('down') + + def remove(self): + """ + Remove interface from operating system. Removing the interface + deconfigures all assigned IP addresses. + Example: + >>> from vyos.ifconfig import L2TPv3If + >>> i = L2TPv3If('l2tpeth0') + >>> i.remove() + """ + + if os.path.exists('/sys/class/net/{}'.format(self.config['ifname'])): + # interface is always A/D down. It needs to be enabled explicitly + self.set_state('down') + + if self._config['tunnel_id'] and self._config['session_id']: + cmd = 'ip l2tp del session tunnel_id {} '.format( + self._config['tunnel_id']) + cmd += 'session_id {} '.format(self._config['session_id']) + self._cmd(cmd) + + if self._config['tunnel_id']: + cmd = 'ip l2tp del tunnel tunnel_id {} '.format( + self._config['tunnel_id']) + self._cmd(cmd) + + @staticmethod + def get_config(): + """ + L2TPv3 interfaces require a configuration when they are added using + iproute2. This static method will provide the configuration dictionary + used by this class. + + Example: + >> dict = L2TPv3If().get_config() + """ + config = { + 'peer_tunnel_id': '', + 'local_port': 0, + 'remote_port': 0, + 'encapsulation': 'udp', + 'local_address': '', + 'remote_address': '', + 'session_id': '', + 'tunnel_id': '', + 'peer_session_id': '' + } + return config diff --git a/python/vyos/ifconfig/loopback.py b/python/vyos/ifconfig/loopback.py new file mode 100644 index 000000000..410a19dcf --- /dev/null +++ b/python/vyos/ifconfig/loopback.py @@ -0,0 +1,50 @@ +# Copyright 2019 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/>. + + +from vyos.ifconfig.interface import Interface + + +class LoopbackIf(Interface): + """ + The loopback device is a special, virtual network interface that your router + uses to communicate with itself. + """ + + default = { + 'type': 'loopback', + } + + def __init__(self, ifname, **kargs): + super().__init__(ifname, **kargs) + + def remove(self): + """ + Loopback interface can not be deleted from operating system. We can + only remove all assigned IP addresses. + + Example: + >>> from vyos.ifconfig import Interface + >>> i = LoopbackIf('lo').remove() + """ + # remove all assigned IP addresses from interface + for addr in self.get_addr(): + if addr in ["127.0.0.1/8", "::1/128"]: + # Do not allow deletion of the default loopback addresses as + # this will cause weird system behavior like snmp/ssh no longer + # operating as expected, see https://phabricator.vyos.net/T2034. + continue + + self.del_addr(addr) diff --git a/python/vyos/ifconfig/macvlan.py b/python/vyos/ifconfig/macvlan.py new file mode 100644 index 000000000..a86f84f3e --- /dev/null +++ b/python/vyos/ifconfig/macvlan.py @@ -0,0 +1,61 @@ +# Copyright 2019 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/>. + + +from vyos.ifconfig.vlan import VLANIf + + +class MACVLANIf(VLANIf): + """ + Abstraction of a Linux MACvlan interface + """ + + options = VLANIf.options + ['link', 'mode'] + default = { + 'type': 'macvlan', + } + + def __init__(self, ifname, **kargs): + super().__init__(ifname, **kargs) + + def _create(self): + cmd = 'ip link add {ifname} link {link} type macvlan mode {mode}'.format( + **self.config) + self._cmd(cmd) + + @staticmethod + def get_config(): + """ + VXLAN interfaces require a configuration when they are added using + iproute2. This static method will provide the configuration dictionary + used by this class. + + Example: + >> dict = MACVLANIf().get_config() + """ + config = { + 'address': '', + 'link': 0, + 'mode': '' + } + return config + + def set_mode(self, mode): + """ + """ + + cmd = 'ip link set dev {} type macvlan mode {}'.format( + self.config['ifname'], mode) + return self._cmd(cmd) diff --git a/python/vyos/ifconfig/stp.py b/python/vyos/ifconfig/stp.py new file mode 100644 index 000000000..741322d0d --- /dev/null +++ b/python/vyos/ifconfig/stp.py @@ -0,0 +1,69 @@ +# Copyright 2019 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/>. + + +from vyos.ifconfig.interface import Interface + +from vyos.validate import * + + +class STPIf(Interface): + """ + A spanning-tree capable interface. This applies only to bridge port member + interfaces! + """ + _sysfs_set = {**Interface._sysfs_set, **{ + 'path_cost': { + # XXX: we should set a maximum + 'validate': assert_positive, + 'location': '/sys/class/net/{ifname}/brport/path_cost', + 'errormsg': '{ifname} is not a bridge port member' + }, + 'path_priority': { + # XXX: we should set a maximum + 'validate': assert_positive, + 'location': '/sys/class/net/{ifname}/brport/priority', + 'errormsg': '{ifname} is not a bridge port member' + }, + }} + + default = { + 'type': 'stp', + } + + def __init__(self, ifname, **kargs): + super().__init__(ifname, **kargs) + + def set_path_cost(self, cost): + """ + Set interface path cost, only relevant for STP enabled interfaces + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_path_cost(4) + """ + self.set_interface('path_cost', cost) + + def set_path_priority(self, priority): + """ + Set interface path priority, only relevant for STP enabled interfaces + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_path_priority(4) + """ + self.set_interface('path_priority', priority) diff --git a/python/vyos/ifconfig/vlan.py b/python/vyos/ifconfig/vlan.py new file mode 100644 index 000000000..ad5d066c4 --- /dev/null +++ b/python/vyos/ifconfig/vlan.py @@ -0,0 +1,134 @@ +# Copyright 2019 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/>. + + +import os +import re + +from vyos.ifconfig.interface import Interface + + +class VLANIf(Interface): + """ + This class handels the creation and removal of a VLAN interface. It serves + as base class for BondIf and EthernetIf. + """ + + default = { + 'type': 'vlan', + } + + def __init__(self, ifname, **kargs): + super().__init__(ifname, **kargs) + + def remove(self): + """ + Remove interface from operating system. Removing the interface + deconfigures all assigned IP addresses and clear possible DHCP(v6) + client processes. + + Example: + >>> from vyos.ifconfig import Interface + >>> i = Interface('eth0') + >>> i.remove() + """ + # Do we have sub interfaces (VLANs)? We apply a regex matching + # subinterfaces (indicated by a .) of a parent interface. + # + # As interfaces need to be deleted "in order" starting from Q-in-Q + # we delete them first. + vlan_ifs = [f for f in os.listdir(r'/sys/class/net') + if re.match(self.config['ifname'] + r'(?:\.\d+)(?:\.\d+)', f)] + + for vlan in vlan_ifs: + Interface(vlan).remove() + + # After deleting all Q-in-Q interfaces delete other VLAN interfaces + # which probably acted as parent to Q-in-Q or have been regular 802.1q + # interface. + vlan_ifs = [f for f in os.listdir(r'/sys/class/net') + if re.match(self.config['ifname'] + r'(?:\.\d+)', f)] + + for vlan in vlan_ifs: + Interface(vlan).remove() + + # All subinterfaces are now removed, continue on the physical interface + super().remove() + + def add_vlan(self, vlan_id, ethertype='', ingress_qos='', egress_qos=''): + """ + A virtual LAN (VLAN) is any broadcast domain that is partitioned and + isolated in a computer network at the data link layer (OSI layer 2). + Use this function to create a new VLAN interface on a given physical + interface. + + This function creates both 802.1q and 802.1ad (Q-in-Q) interfaces. Proto + parameter is used to indicate VLAN type. + + A new object of type VLANIf is returned once the interface has been + created. + + @param ethertype: If specified, create 802.1ad or 802.1q Q-in-Q VLAN + interface + @param ingress_qos: Defines a mapping of VLAN header prio field to the + Linux internal packet priority on incoming frames. + @param ingress_qos: Defines a mapping of Linux internal packet priority + to VLAN header prio field but for outgoing frames. + + Example: + >>> from vyos.ifconfig import VLANIf + >>> i = VLANIf('eth0') + >>> i.add_vlan(10) + """ + vlan_ifname = self.config['ifname'] + '.' + str(vlan_id) + if not os.path.exists('/sys/class/net/{}'.format(vlan_ifname)): + self._vlan_id = int(vlan_id) + + if ethertype: + self._ethertype = ethertype + ethertype = 'proto {}'.format(ethertype) + + # Optional ingress QOS mapping + opt_i = '' + if ingress_qos: + opt_i = 'ingress-qos-map ' + ingress_qos + # Optional egress QOS mapping + opt_e = '' + if egress_qos: + opt_e = 'egress-qos-map ' + egress_qos + + # create interface in the system + cmd = 'ip link add link {ifname} name {ifname}.{vlan} type vlan {proto} id {vlan} {opt_e} {opt_i}' \ + .format(ifname=self.config['ifname'], vlan=self._vlan_id, proto=ethertype, opt_e=opt_e, opt_i=opt_i) + self._cmd(cmd) + + # return new object mapping to the newly created interface + # we can now work on this object for e.g. IP address setting + # or interface description and so on + return VLANIf(vlan_ifname) + + def del_vlan(self, vlan_id): + """ + Remove VLAN interface from operating system. Removing the interface + deconfigures all assigned IP addresses and clear possible DHCP(v6) + client processes. + + Example: + >>> from vyos.ifconfig import VLANIf + >>> i = VLANIf('eth0.10') + >>> i.del_vlan() + """ + vlan_ifname = self.config['ifname'] + '.' + str(vlan_id) + VLANIf(vlan_ifname).remove() diff --git a/python/vyos/ifconfig/vxlan.py b/python/vyos/ifconfig/vxlan.py new file mode 100644 index 000000000..bc2ec508b --- /dev/null +++ b/python/vyos/ifconfig/vxlan.py @@ -0,0 +1,91 @@ +# Copyright 2019 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/>. + + +from vyos.ifconfig.interface import Interface + + +class VXLANIf(Interface): + """ + The VXLAN protocol is a tunnelling protocol designed to solve the + problem of limited VLAN IDs (4096) in IEEE 802.1q. With VXLAN the + size of the identifier is expanded to 24 bits (16777216). + + VXLAN is described by IETF RFC 7348, and has been implemented by a + number of vendors. The protocol runs over UDP using a single + destination port. This document describes the Linux kernel tunnel + device, there is also a separate implementation of VXLAN for + Openvswitch. + + Unlike most tunnels, a VXLAN is a 1 to N network, not just point to + point. A VXLAN device can learn the IP address of the other endpoint + either dynamically in a manner similar to a learning bridge, or make + use of statically-configured forwarding entries. + + For more information please refer to: + https://www.kernel.org/doc/Documentation/networking/vxlan.txt + """ + + options = ['group', 'remote', 'dev', 'port', 'vni'] + + default = { + 'type': 'vxlan', + 'vni': 0, + 'dev': '', + 'group': '', + 'remote': '', + 'port': 8472, # The Linux implementation of VXLAN pre-dates + # the IANA's selection of a standard destination port + } + + def __init__(self, ifname, **kargs): + super().__init__(ifname, **kargs) + + def _create(self): + # we assume that by default a multicast interface is created + group = 'group {}'.format(self.config['group']) + + # if remote host is specified we ignore the multicast address + if self.config['remote']: + group = 'remote {}'.format(self.config['remote']) + + # an underlay device is not always specified + dev = '' + if self.config['dev']: + dev = 'dev {}'.format(self.config['dev']) + + cmd = 'ip link add {ifname} type vxlan id {vni} {group} {dev} dstport {port}'.format( + **config) + self._cmd(cmd) + + @staticmethod + def get_config(): + """ + VXLAN interfaces require a configuration when they are added using + iproute2. This static method will provide the configuration dictionary + used by this class. + + Example: + >> dict = VXLANIf().get_config() + """ + config = { + 'vni': 0, + 'dev': '', + 'group': '', + 'port': 8472, # The Linux implementation of VXLAN pre-dates + # the IANA's selection of a standard destination port + 'remote': '' + } + return config diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py new file mode 100644 index 000000000..2926e72e1 --- /dev/null +++ b/python/vyos/ifconfig/wireguard.py @@ -0,0 +1,169 @@ +# Copyright 2019 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/>. + + +import os + +from vyos.ifconfig.interface import Interface + +class WireGuardIf(Interface): + options = ['port', 'private-key', 'pubkey', 'psk', + 'allowed-ips', 'fwmark', 'endpoint', 'keepalive'] + + default = { + 'type': 'wireguard', + 'port': 0, + 'private-key': None, + 'pubkey': None, + 'psk': '/dev/null', + 'allowed-ips': [], + 'fwmark': 0x00, + 'endpoint': None, + 'keepalive': 0 + } + + """ + Wireguard interface class, contains a comnfig dictionary since + wireguard VPN is being comnfigured via the wg command rather than + writing the config into a file. Otherwise if a pre-shared key is used + (symetric enryption key), it would we exposed within multiple files. + Currently it's only within the config.boot if the config was saved. + + Example: + >>> from vyos.ifconfig import WireGuardIf as wg_if + >>> wg_intfc = wg_if("wg01") + >>> print (wg_intfc.wg_config) + {'private-key': None, 'keepalive': 0, 'endpoint': None, 'port': 0, + 'allowed-ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'} + >>> wg_intfc.wg_config['keepalive'] = 100 + >>> print (wg_intfc.wg_config) + {'private-key': None, 'keepalive': 100, 'endpoint': None, 'port': 0, + 'allowed-ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'} + """ + + def __init__(self, ifname, **kargs): + super().__init__(ifname, **kargs) + + def update(self): + if not self.config['private-key']: + raise ValueError("private key required") + else: + # fmask permission check? + pass + + cmd = "wg set {} ".format(self.config['ifname']) + cmd += "listen-port {} ".format(self.config['port']) + cmd += "fwmark {} ".format(str(self.config['fwmark'])) + cmd += "private-key {} ".format(self.config['private-key']) + cmd += "peer {} ".format(self.config['pubkey']) + cmd += " preshared-key {} ".format(self.config['psk']) + cmd += " allowed-ips " + for aip in self.config['allowed-ips']: + if aip != self.config['allowed-ips'][-1]: + cmd += aip + "," + else: + cmd += aip + if self.config['endpoint']: + cmd += " endpoint {}".format(self.config['endpoint']) + cmd += " persistent-keepalive {}".format(self.config['keepalive']) + + self._cmd(cmd) + + # remove psk since it isn't required anymore and is saved in the cli + # config only !! + if self.config['psk'] != '/dev/null': + if os.path.exists(self.config['psk']): + os.remove(self.config['psk']) + + def remove_peer(self, peerkey): + """ + Remove a peer of an interface, peers are identified by their public key. + Giving it a readable name is a vyos feature, to remove a peer the pubkey + and the interface is needed, to remove the entry. + """ + cmd = "wg set {0} peer {1} remove".format( + self.config['ifname'], str(peerkey)) + return self._cmd(cmd) + + def op_show_interface(self): + wgdump = vyos.interfaces.wireguard_dump().get( + self.config['ifname'], None) + + c = Config() + c.set_level(["interfaces", "wireguard", self.config['ifname']]) + description = c.return_effective_value(["description"]) + ips = c.return_effective_values(["address"]) + + print ("interface: {}".format(self.config['ifname'])) + if (description): + print (" description: {}".format(description)) + + if (ips): + print (" address: {}".format(", ".join(ips))) + print (" public key: {}".format(wgdump['public_key'])) + print (" private key: (hidden)") + print (" listening port: {}".format(wgdump['listen_port'])) + print () + + for peer in c.list_effective_nodes(["peer"]): + if wgdump['peers']: + pubkey = c.return_effective_value(["peer", peer, "pubkey"]) + if pubkey in wgdump['peers']: + wgpeer = wgdump['peers'][pubkey] + + print (" peer: {}".format(peer)) + print (" public key: {}".format(pubkey)) + + """ figure out if the tunnel is recently active or not """ + status = "inactive" + if (wgpeer['latest_handshake'] is None): + """ no handshake ever """ + status = "inactive" + else: + if int(wgpeer['latest_handshake']) > 0: + delta = timedelta(seconds=int( + time.time() - wgpeer['latest_handshake'])) + print (" latest handshake: {}".format(delta)) + if (time.time() - int(wgpeer['latest_handshake']) < (60*5)): + """ Five minutes and the tunnel is still active """ + status = "active" + else: + """ it's been longer than 5 minutes """ + status = "inactive" + elif int(wgpeer['latest_handshake']) == 0: + """ no handshake ever """ + status = "inactive" + print (" status: {}".format(status)) + + if wgpeer['endpoint'] is not None: + print (" endpoint: {}".format(wgpeer['endpoint'])) + + if wgpeer['allowed_ips'] is not None: + print (" allowed ips: {}".format( + ",".join(wgpeer['allowed_ips']).replace(",", ", "))) + + if wgpeer['transfer_rx'] > 0 or wgpeer['transfer_tx'] > 0: + rx_size = size( + wgpeer['transfer_rx'], system=alternative) + tx_size = size( + wgpeer['transfer_tx'], system=alternative) + print (" transfer: {} received, {} sent".format( + rx_size, tx_size)) + + if wgpeer['persistent_keepalive'] is not None: + print (" persistent keepalive: every {} seconds".format( + wgpeer['persistent_keepalive'])) + print() + super().op_show_interface_stats() diff --git a/python/vyos/ifconfig_vlan.py b/python/vyos/ifconfig_vlan.py index 576bb244a..1d57283ac 100644 --- a/python/vyos/ifconfig_vlan.py +++ b/python/vyos/ifconfig_vlan.py @@ -13,7 +13,9 @@ # 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/>. +from netifaces import interfaces from vyos.ifconfig import VLANIf +from vyos import ConfigError def apply_vlan_config(vlan, config): """ @@ -65,6 +67,13 @@ def apply_vlan_config(vlan, config): vlan.set_arp_ignore(config['ip_enable_arp_ignore']) # Maximum Transmission Unit (MTU) vlan.set_mtu(config['mtu']) + + # assign to VRF + if config['vrf']: + vlan.add_vrf(config['vrf']) + else: + vlan.del_vrf(config['vrf']) + # Change VLAN interface MAC address if config['mac']: vlan.set_mac(config['mac']) @@ -83,3 +92,46 @@ def apply_vlan_config(vlan, config): for addr in config['address']: vlan.add_addr(addr) +def verify_vlan_config(config): + """ + Generic function to verify VLAN config consistency. Instead of re- + implementing this function in multiple places use single source \o/ + """ + + for vif in config['vif']: + # DHCPv6 parameters-only and temporary address are mutually exclusive + if vif['dhcpv6_prm_only'] and vif['dhcpv6_temporary']: + raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') + + vrf_name = vif['vrf'] + if vrf_name and vrf_name not in interfaces(): + raise ConfigError(f'VRF "{vrf_name}" does not exist') + + # e.g. wireless interface has no vif_s support + # thus we bail out eraly. + if 'vif_s' not in config.keys(): + return + + for vif_s in config['vif_s']: + for vif in config['vif']: + if vif['id'] == vif_s['id']: + raise ConfigError('Can not use identical ID on vif and vif-s interface') + + # DHCPv6 parameters-only and temporary address are mutually exclusive + if vif_s['dhcpv6_prm_only'] and vif_s['dhcpv6_temporary']: + raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') + + vrf_name = vif_s['vrf'] + if vrf_name and vrf_name not in interfaces(): + raise ConfigError(f'VRF "{vrf_name}" does not exist') + + for vif_c in vif_s['vif_c']: + # DHCPv6 parameters-only and temporary address are mutually exclusive + if vif_c['dhcpv6_prm_only'] and vif_c['dhcpv6_temporary']: + raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') + + vrf_name = vif_c['vrf'] + if vrf_name and vrf_name not in interfaces(): + raise ConfigError(f'VRF "{vrf_name}" does not exist') + + diff --git a/python/vyos/validate.py b/python/vyos/validate.py index 4dca82dd8..48a18642b 100644 --- a/python/vyos/validate.py +++ b/python/vyos/validate.py @@ -187,7 +187,7 @@ def assert_list(s, l): def assert_number(n): - if not n.isnumeric(): + if not str(n).isnumeric(): raise ValueError(f'{n} must be a number') diff --git a/scripts/build-command-op-templates b/scripts/build-command-op-templates index 0383c8982..689d19ece 100755 --- a/scripts/build-command-op-templates +++ b/scripts/build-command-op-templates @@ -111,7 +111,7 @@ def get_properties(p): for i in lists: comp_exprs.append("echo \"{0}\"".format(i.text)) for i in paths: - comp_exprs.append("/bin/cli-shell-api listNodes {0}".format(i.text)) + comp_exprs.append("/bin/cli-shell-api listActiveNodes {0} | sed -e \"s/'//g\"".format(i.text)) for i in scripts: comp_exprs.append("{0}".format(i.text)) comp_help = " && ".join(comp_exprs) diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py index dcb0b59ed..a75beabd1 100755 --- a/src/conf_mode/interfaces-bonding.py +++ b/src/conf_mode/interfaces-bonding.py @@ -21,7 +21,7 @@ from sys import exit from netifaces import interfaces from vyos.ifconfig import BondIf -from vyos.ifconfig_vlan import apply_vlan_config +from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config from vyos.configdict import list_diff, vlan_to_dict from vyos.config import Config from vyos import ConfigError @@ -58,7 +58,8 @@ default_config_data = { 'vif_s': [], 'vif_s_remove': [], 'vif': [], - 'vif_remove': [] + 'vif_remove': [], + 'vrf': '' } @@ -221,8 +222,10 @@ def get_config(): if conf.exists('primary'): bond['primary'] = conf.return_value('primary') - # re-set configuration level to parse new nodes - conf.set_level(cfg_base) + # retrieve VRF instance + if conf.exists('vrf'): + bond['vrf'] = conf.return_value('vrf') + # get vif-s interfaces (currently effective) - to determine which vif-s # interface is no longer present and needs to be removed eff_intf = conf.list_effective_nodes('vif-s') @@ -265,26 +268,12 @@ def verify(bond): raise ConfigError('Interface "{}" is not part of the bond' \ .format(bond['primary'])) + vrf_name = bond['vrf'] + if vrf_name and vrf_name not in interfaces(): + raise ConfigError(f'VRF "{vrf_name}" does not exist') - # DHCPv6 parameters-only and temporary address are mutually exclusive - for vif_s in bond['vif_s']: - if vif_s['dhcpv6_prm_only'] and vif_s['dhcpv6_temporary']: - raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - - for vif_c in vif_s['vif_c']: - if vif_c['dhcpv6_prm_only'] and vif_c['dhcpv6_temporary']: - raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - - for vif in bond['vif']: - if vif['dhcpv6_prm_only'] and vif['dhcpv6_temporary']: - raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - - - for vif_s in bond['vif_s']: - for vif in bond['vif']: - if vif['id'] == vif_s['id']: - raise ConfigError('Can not use identical ID on vif and vif-s interface') - + # use common function to verify VLAN configuration + verify_vlan_config(bond) conf = Config() for intf in bond['member']: @@ -472,6 +461,12 @@ def apply(bond): for addr in bond['address']: b.add_addr(addr) + # assign to VRF + if bond['vrf']: + b.add_vrf(bond['vrf']) + else: + b.del_vrf(bond['vrf']) + # remove no longer required service VLAN interfaces (vif-s) for vif_s in bond['vif_s_remove']: b.del_vlan(vif_s) diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py index 0810d63d6..f189ce36d 100755 --- a/src/conf_mode/interfaces-bridge.py +++ b/src/conf_mode/interfaces-bridge.py @@ -52,7 +52,8 @@ default_config_data = { 'member': [], 'member_remove': [], 'priority': 32768, - 'stp': 0 + 'stp': 0, + 'vrf': '' } def get_config(): @@ -191,12 +192,20 @@ def get_config(): if conf.exists('stp'): bridge['stp'] = 1 + # retrieve VRF instance + if conf.exists('vrf'): + bridge['vrf'] = conf.return_value('vrf') + return bridge def verify(bridge): if bridge['dhcpv6_prm_only'] and bridge['dhcpv6_temporary']: raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') + vrf_name = bridge['vrf'] + if vrf_name and vrf_name not in interfaces(): + raise ConfigError(f'VRF "{vrf_name}" does not exist') + conf = Config() for br in conf.list_nodes('interfaces bridge'): # it makes no sense to verify ourself in this case @@ -286,6 +295,12 @@ def apply(bridge): # store DHCPv6 config dictionary - used later on when addresses are aquired br.set_dhcpv6_options(opt) + # assign to VRF + if bridge['vrf']: + br.add_vrf(bridge['vrf']) + else: + br.del_vrf(bridge['vrf']) + # Change interface MAC address if bridge['mac']: br.set_mac(bridge['mac']) diff --git a/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py index e79e6222d..10cae5d7d 100755 --- a/src/conf_mode/interfaces-dummy.py +++ b/src/conf_mode/interfaces-dummy.py @@ -18,6 +18,7 @@ import os from copy import deepcopy from sys import exit +from netifaces import interfaces from vyos.ifconfig import DummyIf from vyos.configdict import list_diff @@ -30,7 +31,8 @@ default_config_data = { 'deleted': False, 'description': '', 'disable': False, - 'intf': '' + 'intf': '', + 'vrf': '' } def get_config(): @@ -69,9 +71,17 @@ def get_config(): act_addr = conf.return_values('address') dummy['address_remove'] = list_diff(eff_addr, act_addr) + # retrieve VRF instance + if conf.exists('vrf'): + dummy['vrf'] = conf.return_value('vrf') + return dummy def verify(dummy): + vrf_name = dummy['vrf'] + if vrf_name and vrf_name not in interfaces(): + raise ConfigError(f'VRF "{vrf_name}" does not exist') + return None def generate(dummy): @@ -95,6 +105,12 @@ def apply(dummy): for addr in dummy['address']: d.add_addr(addr) + # assign to VRF + if dummy['vrf']: + d.add_vrf(dummy['vrf']) + else: + d.del_vrf(dummy['vrf']) + # disable interface on demand if dummy['disable']: d.set_state('down') diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py index 43cc22589..6d779c94c 100755 --- a/src/conf_mode/interfaces-ethernet.py +++ b/src/conf_mode/interfaces-ethernet.py @@ -16,11 +16,12 @@ import os -from copy import deepcopy from sys import exit +from copy import deepcopy +from netifaces import interfaces from vyos.ifconfig import EthernetIf -from vyos.ifconfig_vlan import apply_vlan_config +from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config from vyos.configdict import list_diff, vlan_to_dict from vyos.config import Config from vyos import ConfigError @@ -59,7 +60,8 @@ default_config_data = { 'vif_s': [], 'vif_s_remove': [], 'vif': [], - 'vif_remove': [] + 'vif_remove': [], + 'vrf': '' } def get_config(): @@ -197,6 +199,10 @@ def get_config(): if conf.exists('speed'): eth['speed'] = conf.return_value('speed') + # retrieve VRF instance + if conf.exists('vrf'): + eth['vrf'] = conf.return_value('vrf') + # re-set configuration level to parse new nodes conf.set_level(cfg_base) # get vif-s interfaces (currently effective) - to determine which vif-s @@ -243,6 +249,10 @@ def verify(eth): if eth['dhcpv6_prm_only'] and eth['dhcpv6_temporary']: raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') + vrf_name = eth['vrf'] + if vrf_name and vrf_name not in interfaces(): + raise ConfigError(f'VRF "{vrf_name}" does not exist') + conf = Config() # some options can not be changed when interface is enslaved to a bond for bond in conf.list_nodes('interfaces bonding'): @@ -250,21 +260,10 @@ def verify(eth): bond_member = conf.return_values('interfaces bonding ' + bond + ' member interface') if eth['intf'] in bond_member: if eth['address']: - raise ConfigError('Can not assign address to interface {} which is a member of {}').format(eth['intf'], bond) - - # DHCPv6 parameters-only and temporary address are mutually exclusive - for vif_s in eth['vif_s']: - if vif_s['dhcpv6_prm_only'] and vif_s['dhcpv6_temporary']: - raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - - for vif_c in vif_s['vif_c']: - if vif_c['dhcpv6_prm_only'] and vif_c['dhcpv6_temporary']: - raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - - for vif in eth['vif']: - if vif['dhcpv6_prm_only'] and vif['dhcpv6_temporary']: - raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') + raise ConfigError('Can not assign address to interface {} which is a member of {}'.format(eth['intf'], bond)) + # use common function to verify VLAN configuration + verify_vlan_config(eth) return None def generate(eth): @@ -367,6 +366,12 @@ def apply(eth): for addr in eth['address']: e.add_addr(addr) + # assign to VRF + if eth['vrf']: + e.add_vrf(eth['vrf']) + else: + e.del_vrf(eth['vrf']) + # remove no longer required service VLAN interfaces (vif-s) for vif_s in eth['vif_s_remove']: e.del_vlan(vif_s) diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py index 8ec78bab3..f948070ee 100755 --- a/src/conf_mode/interfaces-pppoe.py +++ b/src/conf_mode/interfaces-pppoe.py @@ -20,9 +20,9 @@ from sys import exit from copy import deepcopy from jinja2 import Template from subprocess import Popen, PIPE -from time import sleep from pwd import getpwnam from grp import getgrnam +from stat import S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IXGRP, S_IROTH, S_IXOTH from vyos.config import Config from vyos.ifconfig import Interface @@ -30,9 +30,7 @@ from vyos import ConfigError from netifaces import interfaces # Please be careful if you edit the template. -config_pppoe_tmpl = """ -### Autogenerated by interfaces-pppoe.py ### - +config_pppoe_tmpl = """### Autogenerated by interfaces-pppoe.py ### {% if description %} # {{ description }} {% endif %} @@ -92,14 +90,85 @@ usepeerdns {% endif %} {% if ipv6_enable -%} +ipv6 +ipv6cp-use-ipaddr {% endif %} {% if service_name -%} rp_pppoe_service "{{ service_name }}" {% endif %} +{% if on_demand %} +demand +{% endif %} """ -PPP_LOGFILE = '/var/log/vyatta/ppp_{}.log' +# Please be careful if you edit the template. +# There must be no blank line at the top pf the script file +config_pppoe_ipv6_up_tmpl = """#!/bin/sh + +# As PPPoE is an "on demand" interface we need to re-configure it when it +# becomes up + +if [ "$6" != "{{ intf }}" ]; then + exit +fi + +# add some info to syslog +DIALER_PID=$(cat /var/run/{{ intf }}.pid) +logger -t pppd[$DIALER_PID] "executing $0" +logger -t pppd[$DIALER_PID] "configuring dialer interface $6 via $2" + +echo "{{ description }}" > /sys/class/net/{{ intf }}/ifalias + +{% if ipv6_autoconf -%} + + +# Configure interface-specific Host/Router behaviour. +# Note: It is recommended to have the same setting on all interfaces; mixed +# router/host scenarios are rather uncommon. Possible values are: +# +# 0 Forwarding disabled +# 1 Forwarding enabled +# +echo 1 > /proc/sys/net/ipv6/conf/{{ intf }}/forwarding + +# Accept Router Advertisements; autoconfigure using them. +# +# It also determines whether or not to transmit Router +# Solicitations. If and only if the functional setting is to +# accept Router Advertisements, Router Solicitations will be +# transmitted. Possible values are: +# +# 0 Do not accept Router Advertisements. +# 1 Accept Router Advertisements if forwarding is disabled. +# 2 Overrule forwarding behaviour. Accept Router Advertisements +# even if forwarding is enabled. +# +echo 2 > /proc/sys/net/ipv6/conf/{{ intf }}/accept_ra + +# Autoconfigure addresses using Prefix Information in Router Advertisements. +echo 1 > /proc/sys/net/ipv6/conf/{{ intf }}/autoconfigure +{% endif %} +""" + +config_pppoe_ip_pre_up_tmpl = """#!/bin/sh + +# As PPPoE is an "on demand" interface we need to re-configure it when it +# becomes up + +if [ "$6" != "pppoe0" ]; then + exit +fi + +# add some info to syslog +DIALER_PID=$(cat /var/run/{{ intf }}.pid) +logger -t pppd[$DIALER_PID] "executing $0" + +{% if vrf -%} +logger -t pppd[$DIALER_PID] "configuring dialer interface $6 for VRF {{ vrf }}" +ip link set dev {{ intf }} master {{ vrf }} +{% endif %} + +""" default_config_data = { 'access_concentrator': '', @@ -108,7 +177,7 @@ default_config_data = { 'on_demand': False, 'default_route': 'auto', 'deleted': False, - 'description': '', + 'description': '\0', 'disable': False, 'intf': '', 'idle_timeout': '', @@ -120,7 +189,8 @@ default_config_data = { 'name_server': True, 'remote_address': '', 'service_name': '', - 'source_interface': '' + 'source_interface': '', + 'vrf': '' } def subprocess_cmd(command): @@ -137,7 +207,7 @@ def get_config(): raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') pppoe['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - pppoe['logfile'] = PPP_LOGFILE.format(pppoe['intf']) + pppoe['logfile'] = f"/var/log/vyatta/ppp_{pppoe['intf']}.log" # Check if interface has been removed if not conf.exists(base_path + [pppoe['intf']]): @@ -193,7 +263,7 @@ def get_config(): # Physical Interface used for this PPPoE session if conf.exists(['source-interface']): - pppoe['source_interface'] = conf.return_value('source-interface') + pppoe['source_interface'] = conf.return_value(['source-interface']) # Maximum Transmission Unit (MTU) if conf.exists(['mtu']): @@ -211,6 +281,10 @@ def get_config(): if conf.exists(['service-name']): pppoe['service_name'] = conf.return_value(['service-name']) + # retrieve VRF instance + if conf.exists('vrf'): + pppoe['vrf'] = conf.return_value(['vrf']) + return pppoe def verify(pppoe): @@ -219,18 +293,24 @@ def verify(pppoe): return None if not pppoe['source_interface']: - raise ConfigError('PPPoE source interface is missing') + raise ConfigError('PPPoE source interface missing') + + if not pppoe['source_interface'] in interfaces(): + raise ConfigError(f"PPPoE source interface {pppoe['source_interface']} does not exist") - if pppoe['source_interface'] not in interfaces(): - raise ConfigError('PPPoE source interface does not exist') + vrf_name = pppoe['vrf'] + if vrf_name and vrf_name not in interfaces(): + raise ConfigError(f'VRF {vrf_name} does not exist') return None def generate(pppoe): - config_file_pppoe = '/etc/ppp/peers/{}'.format(pppoe['intf']) + config_file_pppoe = f"/etc/ppp/peers/{pppoe['intf']}" + ip_pre_up_script_file = f"/etc/ppp/ip-pre-up.d/9999-vyos-vrf-{pppoe['intf']}" + ipv6_if_up_script_file = f"/etc/ppp/ipv6-up.d/50-vyos-{pppoe['intf']}-autoconf" # Always hang-up PPPoE connection prior generating new configuration file - cmd = 'systemctl stop ppp@{}.service'.format(pppoe['intf']) + cmd = f"systemctl stop ppp@{pppoe['intf']}.service" subprocess_cmd(cmd) if pppoe['deleted']: @@ -238,6 +318,12 @@ def generate(pppoe): if os.path.exists(config_file_pppoe): os.unlink(config_file_pppoe) + if os.path.exists(ipv6_if_up_script_file): + os.unlink(ipv6_if_up_script_file) + + if os.path.exists(ip_pre_up_script_file): + os.unlink(ip_pre_up_script_file) + else: # Create PPP configuration files tmpl = Template(config_pppoe_tmpl) @@ -245,6 +331,21 @@ def generate(pppoe): with open(config_file_pppoe, 'w') as f: f.write(config_text) + tmpl = Template(config_pppoe_ip_pre_up_tmpl) + config_text = tmpl.render(pppoe) + with open(ip_pre_up_script_file, 'w') as f: + f.write(config_text) + + tmpl = Template(config_pppoe_ipv6_up_tmpl) + config_text = tmpl.render(pppoe) + with open(ipv6_if_up_script_file, 'w') as f: + f.write(config_text) + + bitmask = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | \ + S_IROTH | S_IXOTH + os.chmod(ip_pre_up_script_file, bitmask) + os.chmod(ipv6_if_up_script_file, bitmask) + return None def apply(pppoe): @@ -254,7 +355,7 @@ def apply(pppoe): if not pppoe['disable']: # dial PPPoE connection - cmd = 'systemctl start ppp@{}.service'.format(pppoe['intf']) + cmd = f"systemctl start ppp@{pppoe['intf']}.service" subprocess_cmd(cmd) # make logfile owned by root / vyattacfg @@ -263,24 +364,6 @@ def apply(pppoe): gid = getgrnam('vyattacfg').gr_gid os.chown(pppoe['logfile'], uid, gid) - # better late then sorry ... but we can only set interface alias after - # pppd has been launched and created the interface - cnt = 0 - while pppoe['intf'] not in interfaces(): - cnt += 1 - if cnt == 50: - break - - # sleep 250ms - sleep(0.250) - - try: - # we need to catch the exception if the interface is not up due to - # reason stated above - Interface(pppoe['intf']).set_alias(pppoe['description']) - except: - pass - return None if __name__ == '__main__': diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py index 3d36da226..989b1432b 100755 --- a/src/conf_mode/interfaces-pseudo-ethernet.py +++ b/src/conf_mode/interfaces-pseudo-ethernet.py @@ -21,7 +21,8 @@ from sys import exit from netifaces import interfaces from vyos.ifconfig import MACVLANIf -from vyos.configdict import list_diff +from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config +from vyos.configdict import list_diff, vlan_to_dict from vyos.config import Config from vyos import ConfigError @@ -52,7 +53,8 @@ default_config_data = { 'vif_s': [], 'vif_s_remove': [], 'vif': [], - 'vif_remove': [] + 'vif_remove': [], + 'vrf': '' } def get_config(): @@ -158,6 +160,10 @@ def get_config(): if conf.exists(['mode']): peth['mode'] = conf.return_value(['mode']) + # retrieve VRF instance + if conf.exists('vrf'): + peth['vrf'] = conf.return_value('vrf') + # re-set configuration level to parse new nodes conf.set_level(cfg_base) # get vif-s interfaces (currently effective) - to determine which vif-s @@ -196,6 +202,15 @@ def verify(peth): if not peth['link']: raise ConfigError('Link device must be set for virtual ethernet {}'.format(peth['intf'])) + if not peth['link'] in interfaces(): + raise ConfigError('Pseudo-ethernet source interface does not exist') + + vrf_name = peth['vrf'] + if vrf_name and vrf_name not in interfaces(): + raise ConfigError(f'VRF "{vrf_name}" does not exist') + + # use common function to verify VLAN configuration + verify_vlan_config(peth) return None def generate(peth): @@ -282,6 +297,12 @@ def apply(peth): # Enable private VLAN proxy ARP on this interface p.set_proxy_arp_pvlan(peth['ip_proxy_arp_pvlan']) + # assign to VRF + if peth['vrf']: + p.add_vrf(peth['vrf']) + else: + p.del_vrf(peth['vrf']) + # Change interface MAC address if peth['mac']: p.set_mac(peth['mac']) @@ -303,6 +324,35 @@ def apply(peth): for addr in peth['address']: p.add_addr(addr) + # remove no longer required service VLAN interfaces (vif-s) + for vif_s in peth['vif_s_remove']: + p.del_vlan(vif_s) + + # create service VLAN interfaces (vif-s) + for vif_s in peth['vif_s']: + s_vlan = p.add_vlan(vif_s['id'], ethertype=vif_s['ethertype']) + apply_vlan_config(s_vlan, vif_s) + + # remove no longer required client VLAN interfaces (vif-c) + # on lower service VLAN interface + for vif_c in vif_s['vif_c_remove']: + s_vlan.del_vlan(vif_c) + + # create client VLAN interfaces (vif-c) + # on lower service VLAN interface + for vif_c in vif_s['vif_c']: + c_vlan = s_vlan.add_vlan(vif_c['id']) + apply_vlan_config(c_vlan, vif_c) + + # remove no longer required VLAN interfaces (vif) + for vif in peth['vif_remove']: + p.del_vlan(vif) + + # create VLAN interfaces (vif) + for vif in peth['vif']: + vlan = p.add_vlan(vif['id']) + apply_vlan_config(vlan, vif) + return None if __name__ == '__main__': diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py index dabfe4836..c9ef0fe9c 100755 --- a/src/conf_mode/interfaces-vxlan.py +++ b/src/conf_mode/interfaces-vxlan.py @@ -134,8 +134,11 @@ def verify(vxlan): if vxlan['mtu'] < 1500: print('WARNING: RFC7348 recommends VXLAN tunnels preserve a 1500 byte MTU') - if vxlan['group'] and not vxlan['link']: - raise ConfigError('Multicast VXLAN requires an underlaying interface ') + if vxlan['group']: + if not vxlan['link']: + raise ConfigError('Multicast VXLAN requires an underlaying interface ') + if not vxlan['link'] in interfaces(): + raise ConfigError('VXLAN source interface does not exist') if not (vxlan['group'] or vxlan['remote']): raise ConfigError('Group or remote must be configured') diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py index 5289208d9..19e1f01b8 100755 --- a/src/conf_mode/interfaces-wireless.py +++ b/src/conf_mode/interfaces-wireless.py @@ -25,9 +25,10 @@ from grp import getgrnam from subprocess import Popen, PIPE from psutil import pid_exists +from netifaces import interfaces from vyos.ifconfig import EthernetIf -from vyos.ifconfig_vlan import apply_vlan_config +from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config from vyos.configdict import list_diff, vlan_to_dict from vyos.config import Config from vyos import ConfigError @@ -640,9 +641,16 @@ wpa_key_mgmt=WPA-EAP # IP addresses, but this field can be used to force a specific address to be # used, e.g., when the device has multiple IP addresses. radius_client_addr={{ sec_wpa_radius_source }} + +# The own IP address of the access point (used as NAS-IP-Address) +own_ip_addr={{ sec_wpa_radius_source }} +{% else %} +# The own IP address of the access point (used as NAS-IP-Address) +own_ip_addr=127.0.0.1 {% endif %} {% for radius in sec_wpa_radius -%} +{%- if not radius.disabled -%} # RADIUS authentication server auth_server_addr={{ radius.server }} auth_server_port={{ radius.port }} @@ -653,6 +661,7 @@ acct_server_addr={{ radius.server }} acct_server_port={{ radius.acc_port }} acct_server_shared_secret={{ radius.key }} {% endif %} +{% endif %} {% endfor %} {% endif %} @@ -760,6 +769,8 @@ network={ ssid="{{ ssid }}" {%- if sec_wpa_passphrase %} psk="{{ sec_wpa_passphrase }}" +{% else %} + key_mgmt=NONE {% endif %} } @@ -836,7 +847,8 @@ default_config_data = { 'ssid' : '', 'type' : 'monitor', 'vif': [], - 'vif_remove': [] + 'vif_remove': [], + 'vrf': '' } def get_conf_file(conf_type, intf): @@ -1148,6 +1160,10 @@ def get_config(): if conf.exists('mode'): wifi['mode'] = conf.return_value('mode') + # retrieve VRF instance + if conf.exists('vrf'): + wifi['vrf'] = conf.return_value('vrf') + # Wireless physical device if conf.exists('phy'): wifi['phy'] = conf.return_value('phy') @@ -1204,6 +1220,7 @@ def get_config(): radius = { 'server' : server, 'acc_port' : '', + 'disabled': False, 'port' : 1812, 'key' : '' } @@ -1216,6 +1233,10 @@ def get_config(): if conf.exists('accounting'): radius['acc_port'] = radius['port'] + 1 + # Check if RADIUS server was temporary disabled + if conf.exists(['disable']): + radius['disabled'] = True + # RADIUS server shared-secret if conf.exists('key'): radius['key'] = conf.return_value('key') @@ -1248,6 +1269,9 @@ def get_config(): conf.set_level(cfg_base + ' vif ' + vif) wifi['vif'].append(vlan_to_dict(conf)) + # disable interface + if conf.exists('disable'): + wifi['disable'] = True # retrieve configured regulatory domain conf.set_level('system') @@ -1273,7 +1297,6 @@ def verify(wifi): if not wifi['channel']: raise ConfigError('Channel must be set for {}'.format(wifi['intf'])) - if len(wifi['sec_wep_key']) > 4: raise ConfigError('No more then 4 WEP keys configurable') @@ -1293,7 +1316,12 @@ def verify(wifi): if not radius['key']: raise ConfigError('Misssing RADIUS shared secret key for server: {}'.format(radius['server'])) + vrf_name = wifi['vrf'] + if vrf_name and vrf_name not in interfaces(): + raise ConfigError(f'VRF "{vrf_name}" does not exist') + # use common function to verify VLAN configuration + verify_vlan_config(wifi) return None @@ -1390,6 +1418,12 @@ def apply(wifi): # ignore link state changes w.set_link_detect(wifi['disable_link_detect']) + # assign to VRF + if wifi['vrf']: + w.add_vrf(wifi['vrf']) + else: + w.del_vrf(wifi['vrf']) + # Change interface MAC address - re-set to real hardware address (hw-id) # if custom mac is removed if wifi['mac']: @@ -1406,8 +1440,10 @@ def apply(wifi): # configure ARP ignore w.set_arp_ignore(wifi['ip_enable_arp_ignore']) - # enable interface - if not wifi['disable']: + # Enable/Disable interface + if wifi['disable']: + w.set_state('down') + else: w.set_state('up') # Configure interface address(es) diff --git a/src/conf_mode/system-login-banner.py b/src/conf_mode/system-login-banner.py index e66d409bb..20cc16f97 100755 --- a/src/conf_mode/system-login-banner.py +++ b/src/conf_mode/system-login-banner.py @@ -16,6 +16,7 @@ from sys import exit from vyos.config import Config +from vyos import ConfigError motd=""" The programs included with the Debian GNU/Linux system are free software; @@ -49,15 +50,25 @@ def get_config(): # Post-Login banner if conf.exists(['post-login']): tmp = conf.return_value(['post-login']) - tmp = tmp.replace('\\n','\n') - tmp = tmp.replace('\\t','\t') + # post-login banner can be empty as well + if tmp: + tmp = tmp.replace('\\n','\n') + tmp = tmp.replace('\\t','\t') + else: + tmp = '' + banner['motd'] = tmp # Pre-Login banner if conf.exists(['pre-login']): tmp = conf.return_value(['pre-login']) - tmp = tmp.replace('\\n','\n') - tmp = tmp.replace('\\t','\t') + # pre-login banner can be empty as well + if tmp: + tmp = tmp.replace('\\n','\n') + tmp = tmp.replace('\\t','\t') + else: + tmp = '' + banner['issue'] = banner['issue_net'] = tmp return banner diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py index a7fb8ee8f..959e86e5b 100755 --- a/src/conf_mode/system-login.py +++ b/src/conf_mode/system-login.py @@ -196,6 +196,14 @@ def verify(login): if cur_user in login['del_users']: raise ConfigError('Attempting to delete current user: {}'.format(cur_user)) + for user in login['add_users']: + for key in user['public_keys']: + if not key['type']: + raise ConfigError('SSH public key type missing for "{}"!'.format(key['name'])) + + if not key['key']: + raise ConfigError('SSH public key for id "{}" missing!'.format(key['name'])) + # At lease one RADIUS server must not be disabled if len(login['radius_server']) > 0: fail = True diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py new file mode 100755 index 000000000..991c5cb2c --- /dev/null +++ b/src/conf_mode/vrf.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 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 +import jinja2 + +from sys import exit +from copy import deepcopy +from json import loads +from subprocess import check_output, CalledProcessError + +from vyos.config import Config +from vyos.configdict import list_diff +from vyos.ifconfig import Interface +from vyos import ConfigError + +config_file = r'/etc/iproute2/rt_tables.d/vyos-vrf.conf' + +# Please be careful if you edit the template. +config_tmpl = """ +### Autogenerated by vrf.py ### +# +# Routing table ID to name mapping reference + +# id vrf name comment +{% for vrf in vrf_add -%} +{{ "%-10s" | format(vrf.table) }} {{ "%-16s" | format(vrf.name) }} # {{ vrf.description }} +{% endfor -%} + +""" + +default_config_data = { + 'bind_to_all': 0, + 'deleted': False, + 'vrf_add': [], + 'vrf_existing': [], + 'vrf_remove': [] +} + +def _cmd(command): + try: + check_output(command.split()) + except CalledProcessError as e: + raise ConfigError(f'Error changing VRF: {e}') + +def list_rules(): + command = 'ip -j -4 rule show' + answer = loads(check_output(command.split()).decode()) + return [_ for _ in answer if _] + +def vrf_interfaces(c, match): + matched = [] + old_level = c.get_level() + c.set_level(['interfaces']) + section = c.get_config_dict([]) + for type in section: + interfaces = section[type] + for name in interfaces: + interface = interfaces[name] + if 'vrf' in interface: + v = interface.get('vrf', '') + if v == match: + matched.append(name) + + c.set_level(old_level) + return matched + +def vrf_routing(c, match): + matched = [] + old_level = c.get_level() + c.set_level(['protocols', 'vrf']) + if match in c.list_nodes([]): + matched.append(match) + + c.set_level(old_level) + return matched + + +def get_config(): + conf = Config() + vrf_config = deepcopy(default_config_data) + + cfg_base = ['vrf'] + if not conf.exists(cfg_base): + # get all currently effetive VRFs and mark them for deletion + vrf_config['vrf_remove'] = conf.list_effective_nodes(cfg_base + ['name']) + else: + # set configuration level base + conf.set_level(cfg_base) + + # Should services be allowed to bind to all VRFs? + if conf.exists(['bind-to-all']): + vrf_config['bind_to_all'] = 1 + + # Determine vrf interfaces (currently effective) - to determine which + # vrf interface is no longer present and needs to be removed + eff_vrf = conf.list_effective_nodes(['name']) + act_vrf = conf.list_nodes(['name']) + vrf_config['vrf_remove'] = list_diff(eff_vrf, act_vrf) + + # read in individual VRF definition and build up + # configuration + for name in conf.list_nodes(['name']): + vrf_inst = { + 'description' : '', + 'members': [], + 'name' : name, + 'table' : '', + 'table_mod': False + } + conf.set_level(cfg_base + ['name', name]) + + if conf.exists(['table']): + # VRF table can't be changed on demand, thus we need to read in the + # current and the effective routing table number + act_table = conf.return_value(['table']) + eff_table = conf.return_effective_value(['table']) + vrf_inst['table'] = act_table + if eff_table and eff_table != act_table: + vrf_inst['table_mod'] = True + + if conf.exists(['description']): + vrf_inst['description'] = conf.return_value(['description']) + + # append individual VRF configuration to global configuration list + vrf_config['vrf_add'].append(vrf_inst) + + # set configuration level base + conf.set_level(cfg_base) + + # check VRFs which need to be removed as they are not allowed to have + # interfaces attached + tmp = [] + for name in vrf_config['vrf_remove']: + vrf_inst = { + 'interfaces': [], + 'name': name, + 'routes': [] + } + + # find member interfaces of this particulat VRF + vrf_inst['interfaces'] = vrf_interfaces(conf, name) + + # find routing protocols used by this VRF + vrf_inst['routes'] = vrf_routing(conf, name) + + # append individual VRF configuration to temporary configuration list + tmp.append(vrf_inst) + + # replace values in vrf_remove with list of dictionaries + # as we need it in verify() - we can't delete a VRF with members attached + vrf_config['vrf_remove'] = tmp + return vrf_config + +def verify(vrf_config): + # ensure VRF is not assigned to any interface + for vrf in vrf_config['vrf_remove']: + if len(vrf['interfaces']) > 0: + raise ConfigError(f"VRF {vrf['name']} can not be deleted. It has active member interfaces!") + + if len(vrf['routes']) > 0: + raise ConfigError(f"VRF {vrf['name']} can not be deleted. It has active routing protocols!") + + table_ids = [] + for vrf in vrf_config['vrf_add']: + # table id is mandatory + if not vrf['table']: + raise ConfigError(f"VRF {vrf['name']} table id is mandatory!") + + # routing table id can't be changed - OS restriction + if vrf['table_mod']: + raise ConfigError(f"VRF {vrf['name']} table id modification is not possible!") + + # VRf routing table ID must be unique on the system + if vrf['table'] in table_ids: + raise ConfigError(f"VRF {vrf['name']} table id {vrf['table']} is not unique!") + + table_ids.append(vrf['table']) + + return None + +def generate(vrf_config): + tmpl = jinja2.Template(config_tmpl) + config_text = tmpl.render(vrf_config) + with open(config_file, 'w') as f: + f.write(config_text) + + return None + +def apply(vrf_config): + # Documentation + # + # - https://github.com/torvalds/linux/blob/master/Documentation/networking/vrf.txt + # - https://github.com/Mellanox/mlxsw/wiki/Virtual-Routing-and-Forwarding-(VRF) + # - https://netdevconf.info/1.1/proceedings/slides/ahern-vrf-tutorial.pdf + # - https://netdevconf.info/1.2/slides/oct6/02_ahern_what_is_l3mdev_slides.pdf + + # set the default VRF global behaviour + bind_all = vrf_config['bind_to_all'] + _cmd(f'sysctl -wq net.ipv4.tcp_l3mdev_accept={bind_all}') + _cmd(f'sysctl -wq net.ipv4.udp_l3mdev_accept={bind_all}') + + for vrf_name in vrf_config['vrf_remove']: + if os.path.isdir(f'/sys/class/net/{vrf_name}'): + _cmd(f'ip link delete dev {vrf_name}') + + for vrf in vrf_config['vrf_add']: + name = vrf['name'] + table = vrf['table'] + + if not os.path.isdir(f'/sys/class/net/{name}'): + # For each VRF apart from your default context create a VRF + # interface with a separate routing table + _cmd(f'ip link add {name} type vrf table {table}') + # Start VRf + _cmd(f'ip link set dev {name} up') + # The kernel Documentation/networking/vrf.txt also recommends + # adding unreachable routes to the VRF routing tables so that routes + # afterwards are taken. + _cmd(f'ip -4 route add vrf {name} unreachable default metric 4278198272') + _cmd(f'ip -6 route add vrf {name} unreachable default metric 4278198272') + + # set VRF description for e.g. SNMP monitoring + Interface(name).set_alias(vrf['description']) + + # Linux routing uses rules to find tables - routing targets are then + # looked up in those tables. If the lookup got a matching route, the + # process ends. + # + # TL;DR; first table with a matching entry wins! + # + # You can see your routing table lookup rules using "ip rule", sadly the + # local lookup is hit before any VRF lookup. Pinging an addresses from the + # VRF will usually find a hit in the local table, and never reach the VRF + # routing table - this is usually not what you want. Thus we will + # re-arrange the tables and move the local lookup furhter down once VRFs + # are enabled. + + # get current preference on local table + local_pref = [r.get('priority') for r in list_rules() if r.get('table') == 'local'][0] + + # change preference when VRFs are enabled and local lookup table is default + if not local_pref and vrf_config['vrf_add']: + for af in ['-4', '-6']: + _cmd(f'ip {af} rule add pref 32765 table local') + _cmd(f'ip {af} rule del pref 0') + + # return to default lookup preference when no VRF is configured + if not vrf_config['vrf_add']: + for af in ['-4', '-6']: + _cmd(f'ip {af} rule add pref 0 table local') + _cmd(f'ip {af} rule del pref 32765') + + # clean out l3mdev-table rule if present + if 1000 in [r.get('priority') for r in list_rules() if r.get('priority') == 1000]: + _cmd(f'ip {af} rule del pref 1000') + + 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/etc/ppp/ip-pre-up b/src/etc/ppp/ip-pre-up new file mode 100755 index 000000000..05840650b --- /dev/null +++ b/src/etc/ppp/ip-pre-up @@ -0,0 +1,51 @@ +#!/bin/sh +# +# This script is run by the pppd when the link is created. +# It uses run-parts to run scripts in /etc/ppp/ip-pre-up.d, to +# change name, setup firewall,etc you should create script(s) there. +# +# Be aware that other packages may include /etc/ppp/ip-pre-up.d scripts (named +# after that package), so choose local script names with that in mind. +# +# This script is called with the following arguments: +# Arg Name Example +# $1 Interface name ppp0 +# $2 The tty ttyS1 +# $3 The link speed 38400 +# $4 Local IP number 12.34.56.78 +# $5 Peer IP number 12.34.56.99 +# $6 Optional ``ipparam'' value foo + +# The environment is cleared before executing this script +# so the path must be reset +PATH=/usr/local/sbin:/usr/sbin:/sbin:/usr/local/bin:/usr/bin:/bin +export PATH + +# These variables are for the use of the scripts run by run-parts +PPP_IFACE="$1" +PPP_TTY="$2" +PPP_SPEED="$3" +PPP_LOCAL="$4" +PPP_REMOTE="$5" +PPP_IPPARAM="$6" +export PPP_IFACE PPP_TTY PPP_SPEED PPP_LOCAL PPP_REMOTE PPP_IPPARAM + +# as an additional convenience, $PPP_TTYNAME is set to the tty name, +# stripped of /dev/ (if present) for easier matching. +PPP_TTYNAME=`/usr/bin/basename "$2"` +export PPP_TTYNAME + +# If /var/log/ppp-ipupdown.log exists use it for logging. +if [ -e /var/log/ppp-ipupdown.log ]; then + exec > /var/log/ppp-ipupdown.log 2>&1 + echo $0 $* + echo +fi + +# This script can be used to override the .d files supplied by other packages. +if [ -x /etc/ppp/ip-pre-up.local ]; then + exec /etc/ppp/ip-pre-up.local "$*" +fi + +run-parts /etc/ppp/ip-pre-up.d \ + --arg="$1" --arg="$2" --arg="$3" --arg="$4" --arg="$5" --arg="$6" diff --git a/src/migration-scripts/quagga/3-to-4 b/src/migration-scripts/quagga/3-to-4 index f8c87ce8c..be3528391 100755 --- a/src/migration-scripts/quagga/3-to-4 +++ b/src/migration-scripts/quagga/3-to-4 @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019 VyOS maintainers and contributors +# Copyright (C) 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 @@ -16,6 +16,13 @@ # # +# Between 1.2.3 and 1.2.4, FRR added per-neighbor enforce-first-as option. +# Unfortunately they also removed the global enforce-first-as option, +# which broke all old configs that used to have it. +# +# To emulate the effect of the original option, we insert it in every neighbor +# if the config used to have the original global option + import sys from vyos.configtree import ConfigTree @@ -45,11 +52,16 @@ else: # There's actually no BGP, just its empty shell sys.exit(0) - # Check if BGP scan-time parameter exist - scan_time_param = ['protocols', 'bgp', asn, 'parameters', 'scan-time'] - if config.exists(scan_time_param): - # Delete BGP scan-time parameter - config.delete(scan_time_param) + # Check if BGP enforce-first-as option is set + enforce_first_as_path = ['protocols', 'bgp', asn, 'parameters', 'enforce-first-as'] + if config.exists(enforce_first_as_path): + # Delete the obsolete option + config.delete(enforce_first_as_path) + + # Now insert it in every peer + peers = config.list_nodes(['protocols', 'bgp', asn, 'neighbor']) + for p in peers: + config.set(['protocols', 'bgp', asn, 'neighbor', p, 'enforce-first-as']) else: # Do nothing sys.exit(0) @@ -61,3 +73,4 @@ else: except OSError as e: print("Failed to save the modified config: {}".format(e)) sys.exit(1) + diff --git a/src/migration-scripts/quagga/4-to-5 b/src/migration-scripts/quagga/4-to-5 new file mode 100755 index 000000000..f8c87ce8c --- /dev/null +++ b/src/migration-scripts/quagga/4-to-5 @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 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 sys + +from vyos.configtree import ConfigTree + + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +if not config.exists(['protocols', 'bgp']): + # Nothing to do + sys.exit(0) +else: + # Check if BGP is actually configured and obtain the ASN + asn_list = config.list_nodes(['protocols', 'bgp']) + if asn_list: + # There's always just one BGP node, if any + asn = asn_list[0] + else: + # There's actually no BGP, just its empty shell + sys.exit(0) + + # Check if BGP scan-time parameter exist + scan_time_param = ['protocols', 'bgp', asn, 'parameters', 'scan-time'] + if config.exists(scan_time_param): + # Delete BGP scan-time parameter + config.delete(scan_time_param) + else: + # Do nothing + sys.exit(0) + + # Save a new configuration file + 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)) + sys.exit(1) diff --git a/src/op_mode/reset_openvpn.py b/src/op_mode/reset_openvpn.py index 7043ac261..38eca53cc 100755 --- a/src/op_mode/reset_openvpn.py +++ b/src/op_mode/reset_openvpn.py @@ -64,6 +64,7 @@ if __name__ == '__main__': cmd += ' --exec /usr/sbin/openvpn' # now pass arguments to openvpn binary cmd += ' --' + cmd += ' --daemon openvpn-' + interface cmd += ' --config ' + get_config_name(interface) subprocess_cmd(cmd) diff --git a/src/op_mode/show_vrf.py b/src/op_mode/show_vrf.py new file mode 100755 index 000000000..66c33e607 --- /dev/null +++ b/src/op_mode/show_vrf.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 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 argparse +import jinja2 + +from subprocess import check_output +from json import loads + +vrf_out_tmpl = """ +VRF name state mac address flags interfaces +-------- ----- ----------- ----- ---------- +{%- for v in vrf %} +{{"%-16s"|format(v.ifname)}} {{ "%-8s"|format(v.operstate | lower())}} {{"%-17s"|format(v.address | lower())}} {{ v.flags|join(',')|lower()}} {{v.members|join(',')|lower()}} +{%- endfor %} + +""" + +def list_vrfs(): + command = 'ip -j -br link show type vrf' + answer = loads(check_output(command.split()).decode()) + return [_ for _ in answer if _] + +def list_vrf_members(vrf): + command = f'ip -j -br link show master {vrf}' + answer = loads(check_output(command.split()).decode()) + return [_ for _ in answer if _] + +parser = argparse.ArgumentParser() +group = parser.add_mutually_exclusive_group() +group.add_argument("-e", "--extensive", action="store_true", + help="provide detailed vrf informatio") +parser.add_argument('interface', metavar='I', type=str, nargs='?', + help='interface to display') + +args = parser.parse_args() + +if args.extensive: + data = { 'vrf': [] } + for vrf in list_vrfs(): + name = vrf['ifname'] + if args.interface and name != args.interface: + continue + + vrf['members'] = [] + for member in list_vrf_members(name): + vrf['members'].append(member['ifname']) + data['vrf'].append(vrf) + + tmpl = jinja2.Template(vrf_out_tmpl) + print(tmpl.render(data)) + +else: + print(" ".join([vrf['ifname'] for vrf in list_vrfs()])) |