diff options
Diffstat (limited to 'python/vyos/ifconfig')
26 files changed, 4489 insertions, 0 deletions
diff --git a/python/vyos/ifconfig/__init__.py b/python/vyos/ifconfig/__init__.py new file mode 100644 index 000000000..9cd8d44c1 --- /dev/null +++ b/python/vyos/ifconfig/__init__.py @@ -0,0 +1,44 @@ +# 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.section import Section +from vyos.ifconfig.control import Control +from vyos.ifconfig.interface import Interface +from vyos.ifconfig.operational import Operational +from vyos.ifconfig.vrrp import VRRP + +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.vxlan import VXLANIf +from vyos.ifconfig.wireguard import WireGuardIf +from vyos.ifconfig.vtun import VTunIf +from vyos.ifconfig.vti import VTIIf +from vyos.ifconfig.pppoe import PPPoEIf +from vyos.ifconfig.tunnel import GREIf +from vyos.ifconfig.tunnel import GRETapIf +from vyos.ifconfig.tunnel import IP6GREIf +from vyos.ifconfig.tunnel import IPIPIf +from vyos.ifconfig.tunnel import IPIP6If +from vyos.ifconfig.tunnel import IP6IP6If +from vyos.ifconfig.tunnel import SitIf +from vyos.ifconfig.tunnel import Sit6RDIf +from vyos.ifconfig.wireless import WiFiIf +from vyos.ifconfig.l2tpv3 import L2TPv3If +from vyos.ifconfig.macsec import MACsecIf diff --git a/python/vyos/ifconfig/afi.py b/python/vyos/ifconfig/afi.py new file mode 100644 index 000000000..fd263d220 --- /dev/null +++ b/python/vyos/ifconfig/afi.py @@ -0,0 +1,19 @@ +# 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/>. + +# https://www.iana.org/assignments/address-family-numbers/address-family-numbers.xhtml + +IP4 = 1 +IP6 = 2 diff --git a/python/vyos/ifconfig/bond.py b/python/vyos/ifconfig/bond.py new file mode 100644 index 000000000..64407401b --- /dev/null +++ b/python/vyos/ifconfig/bond.py @@ -0,0 +1,383 @@ +# 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 VLAN + +from vyos.util import cmd +from vyos.util import vyos_dict_search +from vyos.validate import assert_list +from vyos.validate import assert_positive + +@Interface.register +@VLAN.enable +class BondIf(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. + """ + + default = { + 'type': 'bond', + } + definition = { + **Interface.definition, + ** { + 'section': 'bonding', + 'prefixes': ['bond', ], + 'broadcast': True, + 'bridgeable': True, + }, + } + + _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}/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 = {**Interface._sysfs_get, **{ + 'bond_arp_ip_target': { + 'location': '/sys/class/net/{ifname}/bonding/arp_ip_target', + } + }} + + 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_admin_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_admin_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' + """ + # As this function might also be called from update() of a VLAN interface + # we must check if the bond_arp_ip_target retrieval worked or not - as this + # can not be set for a bond vif interface + try: + return self.get_interface('bond_arp_ip_target') + except FileNotFoundError: + return '' + + 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') + """ + + # From drivers/net/bonding/bond_main.c: + # ... + # bond_set_slave_link_state(new_slave, + # BOND_LINK_UP, + # BOND_SLAVE_NOTIFY_NOW); + # ... + # + # The kernel will ALWAYS place new bond members in "up" state regardless + # what the CLI will tell us! + + # Physical interface must be in admin down state before they can be + # enslaved. If this is not the case an error will be shown: + # bond0: eth0 is up - this may be due to an out of date ifenslave + slave = Interface(interface) + slave_state = slave.get_admin_state() + if slave_state == 'up': + slave.set_admin_state('down') + + ret = self.set_interface('bond_add_port', f'+{interface}') + # The kernel will ALWAYS place new bond members in "up" state regardless + # what the LI is configured for - thus we place the interface in its + # desired state + slave.set_admin_state(slave_state) + return ret + + 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) + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # use ref-counting function to place an interface into admin down state. + # set_admin_state_up() must be called the same amount of times else the + # interface won't come up. This can/should be used to prevent link flapping + # when changing interface parameters require the interface to be down. + # We will disable it once before reconfiguration and enable it afterwards. + if 'shutdown_required' in config: + self.set_admin_state('down') + + # call base class first + super().update(config) + + # ARP monitor targets need to be synchronized between sysfs and CLI. + # Unfortunately an address can't be send twice to sysfs as this will + # result in the following exception: OSError: [Errno 22] Invalid argument. + # + # We remove ALL addresses prior to adding new ones, this will remove + # addresses manually added by the user too - but as we are limited to 16 adresses + # from the kernel side this looks valid to me. We won't run into an error + # when a user added manual adresses which would result in having more + # then 16 adresses in total. + arp_tgt_addr = list(map(str, self.get_arp_ip_target().split())) + for addr in arp_tgt_addr: + self.set_arp_ip_target('-' + addr) + + # Add configured ARP target addresses + value = vyos_dict_search('arp_monitor.target', config) + if isinstance(value, str): + value = [value] + if value: + for addr in value: + self.set_arp_ip_target('+' + addr) + + # Bonding transmit hash policy + value = config.get('hash_policy') + if value: self.set_hash_policy(value) + + # Some interface options can only be changed if the interface is + # administratively down + if self.get_admin_state() == 'down': + # Delete bond member port(s) + for interface in self.get_slaves(): + self.del_port(interface) + + # Bonding policy/mode + value = config.get('mode') + if value: self.set_mode(value) + + # Add (enslave) interfaces to bond + value = vyos_dict_search('member.interface', config) + if value: + for interface in value: + # if we've come here we already verified the interface + # does not have an addresses configured so just flush + # any remaining ones + Interface(interface).flush_addrs() + self.add_port(interface) + + # Primary device interface - must be set after 'mode' + value = config.get('primary') + if value: self.set_primary(value) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py new file mode 100644 index 000000000..4c76fe996 --- /dev/null +++ b/python/vyos/ifconfig/bridge.py @@ -0,0 +1,263 @@ +# 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.stp import STP +from vyos.validate import assert_boolean +from vyos.validate import assert_positive +from vyos.util import cmd +from vyos.util import vyos_dict_search + +@Interface.register +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. + """ + + default = { + 'type': 'bridge', + } + definition = { + **Interface.definition, + **{ + 'section': 'bridge', + 'prefixes': ['br', ], + 'broadcast': True, + }, + } + + _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', + }, + }} + + + 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) + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # call base class first + super().update(config) + + # Set ageing time + value = config.get('aging') + self.set_ageing_time(value) + + # set bridge forward delay + value = config.get('forwarding_delay') + self.set_forward_delay(value) + + # set hello time + value = config.get('hello_time') + self.set_hello_time(value) + + # set max message age + value = config.get('max_age') + self.set_max_age(value) + + # set bridge priority + value = config.get('priority') + self.set_priority(value) + + # enable/disable spanning tree + value = '1' if 'stp' in config else '0' + self.set_stp(value) + + # enable or disable IGMP querier + tmp = vyos_dict_search('igmp.querier', config) + value = '1' if (tmp != None) else '0' + self.set_multicast_querier(value) + + # remove interface from bridge + tmp = vyos_dict_search('member.interface_remove', config) + if tmp: + for member in tmp: + self.del_port(member) + + STPBridgeIf = STP.enable(BridgeIf) + tmp = vyos_dict_search('member.interface', config) + if tmp: + for interface, interface_config in tmp.items(): + # if we've come here we already verified the interface + # does not have an addresses configured so just flush + # any remaining ones + Interface(interface).flush_addrs() + # enslave interface port to bridge + self.add_port(interface) + + tmp = STPBridgeIf(interface) + # set bridge port path cost + value = interface_config.get('cost') + tmp.set_path_cost(value) + + # set bridge port path priority + value = interface_config.get('priority') + tmp.set_path_priority(value) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/control.py b/python/vyos/ifconfig/control.py new file mode 100644 index 000000000..a6fc8ac6c --- /dev/null +++ b/python/vyos/ifconfig/control.py @@ -0,0 +1,185 @@ +# 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 inspect import signature +from inspect import _empty + +from vyos import debug +from vyos.util import popen +from vyos.util import cmd +from vyos.ifconfig.section import Section + + +class Control(Section): + _command_get = {} + _command_set = {} + _signature = {} + + def __init__(self, **kargs): + # some commands (such as operation comands - show interfaces, etc.) + # need to query the interface statistics. If the interface + # code is used and the debugging is enabled, the screen output + # will include both the command but also the debugging for that command + # to prevent this, debugging can be explicitely disabled + + # if debug is not explicitely disabled the the config, enable it + self.debug = '' + if kargs.get('debug', True) and debug.enabled('ifconfig'): + self.debug = 'ifconfig' + + def _debug_msg (self, message): + return debug.message(message, self.debug) + + def _popen(self, command): + return popen(command, self.debug) + + def _cmd(self, command): + return cmd(command, self.debug) + + 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._command_get[name].get('format', lambda _: _)(self._cmd(cmd)) + + def _values(self, name, validate, value): + """ + looks at the validation function "validate" + for the interface sysfs or command and + returns a dict with the right options to call it + """ + if name not in self._signature: + self._signature[name] = signature(validate) + + values = {} + + for k in self._signature[name].parameters: + default = self._signature[name].parameters[k].default + if default is not _empty: + continue + if k == 'self': + values[k] = self + elif k == 'ifname': + values[k] = self.ifname + else: + values[k] = value + + return values + + def _set_command(self, config, name, value): + """ + Using the defined names, set data write to sysfs. + """ + # the code can pass int as int + value = str(value) + + validate = self._command_set[name].get('validate', None) + if validate: + try: + validate(**self._values(name, validate, value)) + except Exception as e: + raise e.__class__(f'Could not set {name}. {e}') + + convert = self._command_set[name].get('convert', None) + if convert: + value = convert(value) + + possible = self._command_set[name].get('possible', None) + if possible and not possible(config['ifname'], value): + return False + + config = {**config, **{'value': value}} + + cmd = self._command_set[name]['shellcmd'].format(**config) + return self._command_set[name].get('format', lambda _: _)(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. + """ + # the code can pass int as int + value = str(value) + + validate = self._sysfs_set[name].get('validate', None) + if validate: + try: + validate(**self._values(name, validate, value)) + except Exception as e: + raise e.__class__(f'Could not set {name}. {e}') + + 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..43614cd1c --- /dev/null +++ b/python/vyos/ifconfig/dummy.py @@ -0,0 +1,56 @@ +# 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 + + +@Interface.register +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', + } + definition = { + **Interface.definition, + **{ + 'section': 'dummy', + 'prefixes': ['dum', ], + }, + } + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # call base class first + super().update(config) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py new file mode 100644 index 000000000..17c1bd64d --- /dev/null +++ b/python/vyos/ifconfig/ethernet.py @@ -0,0 +1,309 @@ +# 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 +from vyos.ifconfig.vlan import VLAN +from vyos.validate import assert_list +from vyos.util import run +from vyos.util import vyos_dict_search + +@Interface.register +@VLAN.enable +class EthernetIf(Interface): + """ + Abstraction of a Linux Ethernet Interface + """ + + default = { + 'type': 'ethernet', + } + definition = { + **Interface.definition, + **{ + 'section': 'ethernet', + 'prefixes': ['lan', 'eth', 'eno', 'ens', 'enp', 'enx'], + 'bondable': True, + 'broadcast': True, + 'bridgeable': True, + 'eternal': '(lan|eth|eno|ens|enp|enx)[0-9]+$', + } + } + + @staticmethod + def feature(ifname, option, value): + run(f'/sbin/ethtool -K {ifname} {option} {value}','ifconfig') + return False + + _command_set = {**Interface._command_set, **{ + 'gro': { + 'validate': lambda v: assert_list(v, ['on', 'off']), + 'possible': lambda i, v: EthernetIf.feature(i, 'gro', v), + # 'shellcmd': '/sbin/ethtool -K {ifname} gro {value}', + }, + 'gso': { + 'validate': lambda v: assert_list(v, ['on', 'off']), + 'possible': lambda i, v: EthernetIf.feature(i, 'gso', v), + # 'shellcmd': '/sbin/ethtool -K {ifname} gso {value}', + }, + 'sg': { + 'validate': lambda v: assert_list(v, ['on', 'off']), + 'possible': lambda i, v: EthernetIf.feature(i, 'sg', v), + # 'shellcmd': '/sbin/ethtool -K {ifname} sg {value}', + }, + 'tso': { + 'validate': lambda v: assert_list(v, ['on', 'off']), + 'possible': lambda i, v: EthernetIf.feature(i, 'tso', v), + # 'shellcmd': '/sbin/ethtool -K {ifname} tso {value}', + }, + 'ufo': { + 'validate': lambda v: assert_list(v, ['on', 'off']), + 'possible': lambda i, v: EthernetIf.feature(i, 'ufo', v), + # 'shellcmd': '/sbin/ethtool -K {ifname} ufo {value}', + }, + }} + + 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) + """ + ifname = self.config['ifname'] + + if enable not in ['on', 'off']: + raise ValueError("Value out of range") + + if self.get_driver_name() in ['vmxnet3', 'virtio_net', 'xen_netfront']: + self._debug_msg('{} driver does not support changing flow control settings!' + .format(self.get_driver_name())) + return + + # Get current flow control settings: + cmd = f'/sbin/ethtool --show-pause {ifname}' + output, code = self._popen(cmd) + if code == 76: + # the interface does not support it + return '' + if code: + # never fail here as it prevent vyos to boot + print(f'unexpected return code {code} from {cmd}') + return '' + + # The above command returns - with tabs: + # + # Pause parameters for eth0: + # Autonegotiate: on + # RX: off + # TX: off + if re.search("Autonegotiate:\ton", output): + 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 = f'/sbin/ethtool --pause {ifname} autoneg {enable} tx {enable} rx {enable}' + output, code = self._popen(cmd) + if code: + print(f'could not set flowcontrol for {ifname}') + return output + + 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', 'xen_netfront']: + 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') + """ + return self.set_interface('ufo', state) + + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # call base class first + super().update(config) + + # disable ethernet flow control (pause frames) + value = 'off' if 'disable_flow_control' in config.keys() else 'on' + self.set_flow_control(value) + + # GRO (generic receive offload) + tmp = vyos_dict_search('offload_options.generic_receive', config) + value = tmp if (tmp != None) else 'off' + self.set_gro(value) + + # GSO (generic segmentation offload) + tmp = vyos_dict_search('offload_options.generic_segmentation', config) + value = tmp if (tmp != None) else 'off' + self.set_gso(value) + + # scatter-gather option + tmp = vyos_dict_search('offload_options.scatter_gather', config) + value = tmp if (tmp != None) else 'off' + self.set_sg(value) + + # TSO (TCP segmentation offloading) + tmp = vyos_dict_search('offload_options.udp_fragmentation', config) + value = tmp if (tmp != None) else 'off' + self.set_tso(value) + + # UDP fragmentation offloading + tmp = vyos_dict_search('offload_options.udp_fragmentation', config) + value = tmp if (tmp != None) else 'off' + self.set_ufo(value) + + # Set physical interface speed and duplex + if {'speed', 'duplex'} <= set(config): + speed = config.get('speed') + duplex = config.get('duplex') + self.set_speed_duplex(speed, duplex) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/geneve.py b/python/vyos/ifconfig/geneve.py new file mode 100644 index 000000000..dd0658668 --- /dev/null +++ b/python/vyos/ifconfig/geneve.py @@ -0,0 +1,85 @@ +# 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 copy import deepcopy + +from vyos.ifconfig.interface import Interface + + +@Interface.register +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', + 'vni': 0, + 'remote': '', + } + options = Interface.options + \ + ['vni', 'remote'] + definition = { + **Interface.definition, + **{ + 'section': 'geneve', + 'prefixes': ['gnv', ], + 'bridgeable': True, + } + } + + def _create(self): + cmd = 'ip link add name {ifname} type geneve id {vni} remote {remote}'.format(**self.config) + self._cmd(cmd) + + # interface is always A/D down. It needs to be enabled explicitly + self.set_admin_state('down') + + @classmethod + def get_config(cls): + """ + 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() + """ + return deepcopy(cls.default) + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # call base class first + super().update(config) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/input.py b/python/vyos/ifconfig/input.py new file mode 100644 index 000000000..bfab36335 --- /dev/null +++ b/python/vyos/ifconfig/input.py @@ -0,0 +1,31 @@ +# Copyright 2020 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 + + +@Interface.register +class InputIf(Interface): + default = { + 'type': '', + } + definition = { + **Interface.definition, + **{ + 'section': 'input', + 'prefixes': ['ifb', ], + }, + } diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py new file mode 100644 index 000000000..67ba973c4 --- /dev/null +++ b/python/vyos/ifconfig/interface.py @@ -0,0 +1,1067 @@ +# Copyright 2019-2020 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 json +import jmespath + +from copy import deepcopy +from glob import glob + +from ipaddress import IPv4Network +from ipaddress import IPv6Address +from ipaddress import IPv6Network +from netifaces import ifaddresses +# this is not the same as socket.AF_INET/INET6 +from netifaces import AF_INET +from netifaces import AF_INET6 + +from vyos import ConfigError +from vyos.configdict import list_diff +from vyos.configdict import dict_merge +from vyos.template import render +from vyos.util import mac2eui64 +from vyos.util import vyos_dict_search +from vyos.validate import is_ipv4 +from vyos.validate import is_ipv6 +from vyos.validate import is_intf_addr_assigned +from vyos.validate import assert_boolean +from vyos.validate import assert_list +from vyos.validate import assert_mac +from vyos.validate import assert_mtu +from vyos.validate import assert_positive +from vyos.validate import assert_range + +from vyos.ifconfig.control import Control +from vyos.ifconfig.vrrp import VRRP +from vyos.ifconfig.operational import Operational +from vyos.ifconfig import Section + +def get_ethertype(ethertype_val): + if ethertype_val == '0x88A8': + return '802.1ad' + elif ethertype_val == '0x8100': + return '802.1q' + else: + raise ConfigError('invalid ethertype "{}"'.format(ethertype_val)) + +class Interface(Control): + # This is the class which will be used to create + # self.operational, it allows subclasses, such as + # WireGuard to modify their display behaviour + OperationalClass = Operational + + options = ['debug', 'create'] + required = [] + default = { + 'type': '', + 'debug': True, + 'create': True, + } + definition = { + 'section': '', + 'prefixes': [], + 'vlan': False, + 'bondable': False, + 'broadcast': False, + 'bridgeable': False, + 'eternal': '', + } + + _command_get = { + 'admin_state': { + 'shellcmd': 'ip -json link show dev {ifname}', + 'format': lambda j: 'up' if 'UP' in jmespath.search('[*].flags | [0]', json.loads(j)) else 'down', + }, + 'vlan_protocol': { + 'shellcmd': 'ip -json -details link show dev {ifname}', + 'format': lambda j: jmespath.search('[*].linkinfo.info_data.protocol | [0]', json.loads(j)), + }, + } + + _command_set = { + 'admin_state': { + 'validate': lambda v: assert_list(v, ['up', 'down']), + 'shellcmd': 'ip link set dev {ifname} {value}', + }, + 'mac': { + 'validate': assert_mac, + 'shellcmd': 'ip link set dev {ifname} address {value}', + }, + 'vrf': { + 'convert': lambda v: f'master {v}' if v else 'nomaster', + 'shellcmd': 'ip link set dev {ifname} {value}', + }, + } + + _sysfs_get = { + 'alias': { + 'location': '/sys/class/net/{ifname}/ifalias', + }, + 'mac': { + 'location': '/sys/class/net/{ifname}/address', + }, + 'mtu': { + 'location': '/sys/class/net/{ifname}/mtu', + }, + 'oper_state':{ + 'location': '/sys/class/net/{ifname}/operstate', + }, + } + + _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', + }, + 'ipv6_accept_ra': { + 'validate': lambda ara: assert_range(ara,0,3), + 'location': '/proc/sys/net/ipv6/conf/{ifname}/accept_ra', + }, + 'ipv6_autoconf': { + 'validate': lambda aco: assert_range(aco,0,2), + 'location': '/proc/sys/net/ipv6/conf/{ifname}/autoconf', + }, + 'ipv6_forwarding': { + 'validate': lambda fwd: assert_range(fwd,0,2), + 'location': '/proc/sys/net/ipv6/conf/{ifname}/forwarding', + }, + 'ipv6_dad_transmits': { + 'validate': assert_positive, + 'location': '/proc/sys/net/ipv6/conf/{ifname}/dad_transmits', + }, + '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', + }, + } + + @classmethod + def exists(cls, ifname): + return os.path.exists(f'/sys/class/net/{ifname}') + + 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) + for k in self.options: + if k in kargs: + self.config[k] = kargs[k] + + # make sure the ifname is the first argument and not from the dict + self.config['ifname'] = ifname + self._admin_state_down_cnt = 0 + + # we must have updated config before initialising the Interface + super().__init__(**kargs) + self.ifname = ifname + + if not self.exists(ifname): + # Any instance of Interface, such as Interface('eth0') + # can be used safely to access the generic function in this class + # as 'type' is unset, the class can not be created + if not self.config['type']: + raise Exception(f'interface "{ifname}" not found') + + # Should an Instance of a child class (EthernetIf, DummyIf, ..) + # be required, then create should be set to False to not accidentally create it. + # In case a subclass does not define it, we use get to set the default to True + if self.config.get('create',True): + for k in self.required: + if k not in kargs: + name = self.default['type'] + raise ConfigError(f'missing required option {k} for {name} {ifname} creation') + + self._create() + # If we can not connect to the interface then let the caller know + # as the class could not be correctly initialised + else: + raise Exception('interface "{}" not found'.format(self.config['ifname'])) + + # temporary list of assigned IP addresses + self._addr = [] + + self.operational = self.OperationalClass(ifname) + self.vrrp = VRRP(ifname) + + 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() + """ + + # 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 + self.flush_addrs() + + # --------------------------------------------------------------------- + # Any class can define an eternal regex in its definition + # interface matching the regex will not be deleted + + eternal = self.definition['eternal'] + if not eternal: + self._delete() + elif not re.match(eternal, self.ifname): + 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 {ifname}'.format(**self.config) + 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 get_mac(self): + """ + Get current interface MAC (Media Access Contrl) address used. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').get_mac() + '00:50:ab:cd:ef:00' + """ + return self.get_interface('mac') + + 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') + """ + + # If MAC is unchanged, bail out early + if mac == self.get_mac(): + return None + + # MAC address can only be changed if interface is in 'down' state + prev_state = self.get_admin_state() + if prev_state == 'up': + self.set_admin_state('down') + + self.set_interface('mac', mac) + + # Turn an interface to the 'up' state if it was changed to 'down' by this fucntion + if prev_state == 'up': + self.set_admin_state('up') + + def set_vrf(self, vrf=''): + """ + Add/Remove interface from given VRF instance. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_vrf('foo') + >>> Interface('eth0').set_vrf() + """ + self.set_interface('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_ipv6_accept_ra(self, accept_ra): + """ + 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. + + 0 - Do not accept Router Advertisements. + 1 - (default) Accept Router Advertisements if forwarding is disabled. + 2 - Overrule forwarding behaviour. Accept Router Advertisements even if + forwarding is enabled. + """ + return self.set_interface('ipv6_accept_ra', accept_ra) + + def set_ipv6_autoconf(self, autoconf): + """ + Autoconfigure addresses using Prefix Information in Router + Advertisements. + """ + return self.set_interface('ipv6_autoconf', autoconf) + + def add_ipv6_eui64_address(self, prefix): + """ + Extended Unique Identifier (EUI), as per RFC2373, allows a host to + assign itself a unique IPv6 address based on a given IPv6 prefix. + + Calculate the EUI64 from the interface's MAC, then assign it + with the given prefix to the interface. + """ + + eui64 = mac2eui64(self.get_mac(), prefix) + prefixlen = prefix.split('/')[1] + self.add_addr(f'{eui64}/{prefixlen}') + + def del_ipv6_eui64_address(self, prefix): + """ + Delete the address based on the interface's MAC-based EUI64 + combined with the prefix address. + """ + eui64 = mac2eui64(self.get_mac(), prefix) + prefixlen = prefix.split('/')[1] + self.del_addr(f'{eui64}/{prefixlen}') + + + def set_ipv6_forwarding(self, forwarding): + """ + Configure IPv6 interface-specific Host/Router behaviour. + + False: + + By default, Host behaviour is assumed. This means: + + 1. IsRouter flag is not set in Neighbour Advertisements. + 2. If accept_ra is TRUE (default), transmit Router + Solicitations. + 3. If accept_ra is TRUE (default), accept Router + Advertisements (and do autoconfiguration). + 4. If accept_redirects is TRUE (default), accept Redirects. + + True: + + If local forwarding is enabled, Router behaviour is assumed. + This means exactly the reverse from the above: + + 1. IsRouter flag is set in Neighbour Advertisements. + 2. Router Solicitations are not sent unless accept_ra is 2. + 3. Router Advertisements are ignored unless accept_ra is 2. + 4. Redirects are ignored. + """ + return self.set_interface('ipv6_forwarding', forwarding) + + def set_ipv6_dad_messages(self, dad): + """ + The amount of Duplicate Address Detection probes to send. + Default: 1 + """ + return self.set_interface('ipv6_dad_transmits', dad) + + 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 get_alias(self): + """ + Get interface alias name used by e.g. SNMP + + Example: + >>> Interface('eth0').get_alias() + 'interface description as set by user' + """ + return self.get_interface('alias') + + 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_vlan_protocol(self): + """ + Retrieve VLAN protocol in use, this can be 802.1Q, 802.1ad or None + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0.10').get_vlan_protocol() + '802.1Q' + """ + return self.get_interface('vlan_protocol') + + def get_admin_state(self): + """ + Get interface administrative state. Function will return 'up' or 'down' + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').get_admin_state() + 'up' + """ + return self.get_interface('admin_state') + + def set_admin_state(self, state): + """ + Set interface administrative state to be 'up' or 'down' + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_admin_state('down') + >>> Interface('eth0').get_admin_state() + 'down' + """ + # A VLAN interface can only be placed in admin up state when + # the lower interface is up, too + if self.get_vlan_protocol(): + lower_interface = glob(f'/sys/class/net/{self.ifname}/lower*/flags')[0] + with open(lower_interface, 'r') as f: + flags = f.read() + # If parent is not up - bail out as we can not bring up the VLAN. + # Flags are defined in kernel source include/uapi/linux/if.h + if not int(flags, 16) & 1: + return None + + if state == 'up': + self._admin_state_down_cnt -= 1 + if self._admin_state_down_cnt < 1: + return self.set_interface('admin_state', state) + else: + self._admin_state_down_cnt += 1 + return self.set_interface('admin_state', state) + + 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. Address format must be validated + and compressed/normalized before calling this function. + + 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 WIDE DHCPv6 (IPv6) on interface + + Returns False if address is already assigned and wasn't re-added. + 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'] + """ + # XXX: normalize/compress with ipaddress if calling functions don't? + # is subnet mask always passed, and in the same way? + + # do not add same address twice + if addr in self._addr: + return False + + addr_is_v4 = is_ipv4(addr) + + # we can't have both DHCP and static IPv4 addresses assigned + for a in self._addr: + if ( ( addr == 'dhcp' and a != 'dhcpv6' and is_ipv4(a) ) or + ( a == 'dhcp' and addr != 'dhcpv6' and addr_is_v4 ) ): + raise ConfigError(( + "Can't configure both static IPv4 and DHCP address " + "on the same interface")) + + # add to interface + if addr == 'dhcp': + self.set_dhcp(True) + elif addr == 'dhcpv6': + self.set_dhcpv6(True) + elif not is_intf_addr_assigned(self.ifname, addr): + self._cmd(f'ip addr add "{addr}" ' + f'{"brd + " if addr_is_v4 else ""}dev "{self.ifname}"') + else: + return False + + # add to cache + self._addr.append(addr) + + return True + + def del_addr(self, addr): + """ + Delete IP(v6) address from interface. Address is only deleted if it is + assigned to that interface. Address format must be exactly the same as + was used when adding the address. + + 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 + + Returns False if address isn't already assigned and wasn't deleted. + 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'] + """ + + # remove from interface + if addr == 'dhcp': + self.set_dhcp(False) + elif addr == 'dhcpv6': + self.set_dhcpv6(False) + elif is_intf_addr_assigned(self.ifname, addr): + self._cmd(f'ip addr del "{addr}" dev "{self.ifname}"') + else: + return False + + # remove from cache + if addr in self._addr: + self._addr.remove(addr) + + return True + + def flush_addrs(self): + """ + Flush all addresses from an interface, including DHCP. + + Will raise an exception on error. + """ + # stop DHCP(v6) if running + self.set_dhcp(False) + self.set_dhcpv6(False) + + # flush all addresses + self._cmd(f'ip addr flush dev "{self.ifname}"') + + def add_to_bridge(self, br): + """ + Adds the interface to the bridge with the passed port config. + + Returns False if bridge doesn't exist. + """ + + # check if the bridge exists (on boot it doesn't) + if br not in Section.interfaces('bridge'): + return False + + self.flush_addrs() + # add interface to bridge - use Section.klass to get BridgeIf class + Section.klass(br)(br, create=False).add_port(self.ifname) + + # TODO: port config (STP) + + return True + + def set_dhcp(self, enable): + """ + Enable/Disable DHCP client on a given interface. + """ + if enable not in [True, False]: + raise ValueError() + + ifname = self.ifname + config_base = r'/var/lib/dhcp/dhclient' + config_file = f'{config_base}_{ifname}.conf' + options_file = f'{config_base}_{ifname}.options' + pid_file = f'{config_base}_{ifname}.pid' + lease_file = f'{config_base}_{ifname}.leases' + + if enable and 'disable' not in self._config: + if vyos_dict_search('dhcp_options.host_name', self._config) == None: + # read configured system hostname. + # maybe change to vyos hostd client ??? + hostname = 'vyos' + with open('/etc/hostname', 'r') as f: + hostname = f.read().rstrip('\n') + tmp = {'dhcp_options' : { 'host_name' : hostname}} + self._config = dict_merge(tmp, self._config) + + render(options_file, 'dhcp-client/daemon-options.tmpl', + self._config, trim_blocks=True) + render(config_file, 'dhcp-client/ipv4.tmpl', + self._config, trim_blocks=True) + + # 'up' check is mandatory b/c even if the interface is A/D, as soon as + # the DHCP client is started the interface will be placed in u/u state. + # This is not what we intended to do when disabling an interface. + return self._cmd(f'systemctl restart dhclient@{ifname}.service') + else: + self._cmd(f'systemctl stop dhclient@{ifname}.service') + + # cleanup old config files + for file in [config_file, options_file, pid_file, lease_file]: + if os.path.isfile(file): + os.remove(file) + + + def set_dhcpv6(self, enable): + """ + Enable/Disable DHCPv6 client on a given interface. + """ + if enable not in [True, False]: + raise ValueError() + + ifname = self.ifname + config_file = f'/run/dhcp6c/dhcp6c.{ifname}.conf' + + if enable and 'disable' not in self._config: + render(config_file, 'dhcp-client/ipv6.tmpl', + self._config, trim_blocks=True) + + # We must ignore any return codes. This is required to enable DHCPv6-PD + # for interfaces which are yet not up and running. + return self._popen(f'systemctl restart dhcp6c@{ifname}.service') + else: + self._popen(f'systemctl stop dhcp6c@{ifname}.service') + + if os.path.isfile(config_file): + os.remove(config_file) + + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # Cache the configuration - it will be reused inside e.g. DHCP handler + # XXX: maybe pass the option via __init__ in the future and rename this + # method to apply()? + self._config = config + + # Update interface description + self.set_alias(config.get('description', '')) + + # Ignore link state changes + value = '2' if 'disable_link_detect' in config else '1' + self.set_link_detect(value) + + # Configure assigned interface IP addresses. No longer + # configured addresses will be removed first + new_addr = config.get('address', []) + + # XXX: T2636 workaround: convert string to a list with one element + if isinstance(new_addr, str): + new_addr = [new_addr] + + # always ensure DHCP client is stopped (when not configured explicitly) + if 'dhcp' not in new_addr: + self.del_addr('dhcp') + + # always ensure DHCPv6 client is stopped (when not configured as client + # for IPv6 address or prefix delegation + dhcpv6pd = vyos_dict_search('dhcpv6_options.pd', config) + if 'dhcpv6' not in new_addr or dhcpv6pd == None: + self.del_addr('dhcpv6') + + # determine IP addresses which are assigned to the interface and build a + # list of addresses which are no longer in the dict so they can be removed + cur_addr = self.get_addr() + for addr in list_diff(cur_addr, new_addr): + self.del_addr(addr) + + for addr in new_addr: + self.add_addr(addr) + + # start DHCPv6 client when only PD was configured + if dhcpv6pd != None: + self.set_dhcpv6(True) + + # There are some items in the configuration which can only be applied + # if this instance is not bound to a bridge. This should be checked + # by the caller but better save then sorry! + if not any(k in ['is_bond_member', 'is_bridge_member'] for k in config): + # Bind interface to given VRF or unbind it if vrf node is not set. + # unbinding will call 'ip link set dev eth0 nomaster' which will + # also drop the interface out of a bridge or bond - thus this is + # checked before + self.set_vrf(config.get('vrf', '')) + + # Configure ARP cache timeout in milliseconds - has default value + tmp = vyos_dict_search('ip.arp_cache_timeout', config) + value = tmp if (tmp != None) else '30' + self.set_arp_cache_tmo(value) + + # Configure ARP filter configuration + tmp = vyos_dict_search('ip.disable_arp_filter', config) + value = '0' if (tmp != None) else '1' + self.set_arp_filter(value) + + # Configure ARP accept + tmp = vyos_dict_search('ip.enable_arp_accept', config) + value = '1' if (tmp != None) else '0' + self.set_arp_accept(value) + + # Configure ARP announce + tmp = vyos_dict_search('ip.enable_arp_announce', config) + value = '1' if (tmp != None) else '0' + self.set_arp_announce(value) + + # Configure ARP ignore + tmp = vyos_dict_search('ip.enable_arp_ignore', config) + value = '1' if (tmp != None) else '0' + self.set_arp_ignore(value) + + # Enable proxy-arp on this interface + tmp = vyos_dict_search('ip.enable_proxy_arp', config) + value = '1' if (tmp != None) else '0' + self.set_proxy_arp(value) + + # Enable private VLAN proxy ARP on this interface + tmp = vyos_dict_search('ip.proxy_arp_pvlan', config) + value = '1' if (tmp != None) else '0' + self.set_proxy_arp_pvlan(value) + + # IPv6 forwarding + tmp = vyos_dict_search('ipv6.disable_forwarding', config) + value = '0' if (tmp != None) else '1' + self.set_ipv6_forwarding(value) + + # IPv6 router advertisements + tmp = vyos_dict_search('ipv6.address.autoconf', config) + value = '2' if (tmp != None) else '1' + if 'dhcpv6' in new_addr: + value = '2' + self.set_ipv6_accept_ra(value) + + # IPv6 address autoconfiguration + tmp = vyos_dict_search('ipv6.address.autoconf', config) + value = '1' if (tmp != None) else '0' + self.set_ipv6_autoconf(value) + + # IPv6 Duplicate Address Detection (DAD) tries + tmp = vyos_dict_search('ipv6.dup_addr_detect_transmits', config) + value = tmp if (tmp != None) else '1' + self.set_ipv6_dad_messages(value) + + # MTU - Maximum Transfer Unit + if 'mtu' in config: + self.set_mtu(config.get('mtu')) + + # Delete old IPv6 EUI64 addresses before changing MAC + tmp = vyos_dict_search('ipv6.address.eui64_old', config) + if tmp: + for addr in tmp: + self.del_ipv6_eui64_address(addr) + + # Change interface MAC address - re-set to real hardware address (hw-id) + # if custom mac is removed. Skip if bond member. + if 'is_bond_member' not in config: + mac = config.get('hw_id') + if 'mac' in config: + mac = config.get('mac') + if mac: + self.set_mac(mac) + + # Manage IPv6 link-local addresses + tmp = vyos_dict_search('ipv6.address.no_default_link_local', config) + # we must check explicitly for None type as if the key is set we will + # get an empty dict (<class 'dict'>) + if tmp is not None: + self.del_ipv6_eui64_address('fe80::/64') + else: + self.add_ipv6_eui64_address('fe80::/64') + + # Add IPv6 EUI-based addresses + tmp = vyos_dict_search('ipv6.address.eui64', config) + if tmp: + # XXX: T2636 workaround: convert string to a list with one element + if isinstance(tmp, str): + tmp = [tmp] + for addr in tmp: + self.add_ipv6_eui64_address(addr) + + # re-add ourselves to any bridge we might have fallen out of + if 'is_bridge_member' in config: + bridge = config.get('is_bridge_member') + self.add_to_bridge(bridge) + + # remove no longer required 802.1ad (Q-in-Q VLANs) + for vif_s_id in config.get('vif_s_remove', {}): + self.del_vlan(vif_s_id) + + # create/update 802.1ad (Q-in-Q VLANs) + ifname = config['ifname'] + for vif_s_id, vif_s in config.get('vif_s', {}).items(): + tmp=get_ethertype(vif_s.get('ethertype', '0x88A8')) + s_vlan = self.add_vlan(vif_s_id, ethertype=tmp) + vif_s['ifname'] = f'{ifname}.{vif_s_id}' + s_vlan.update(vif_s) + + # remove no longer required client VLAN (vif-c) + for vif_c_id in vif_s.get('vif_c_remove', {}): + s_vlan.del_vlan(vif_c_id) + + # create/update client VLAN (vif-c) interface + for vif_c_id, vif_c in vif_s.get('vif_c', {}).items(): + c_vlan = s_vlan.add_vlan(vif_c_id) + vif_c['ifname'] = f'{ifname}.{vif_s_id}.{vif_c_id}' + c_vlan.update(vif_c) + + # remove no longer required 802.1q VLAN interfaces + for vif_id in config.get('vif_remove', {}): + self.del_vlan(vif_id) + + # create/update 802.1q VLAN interfaces + for vif_id, vif in config.get('vif', {}).items(): + vlan = self.add_vlan(vif_id) + vif['ifname'] = f'{ifname}.{vif_id}' + vlan.update(vif) diff --git a/python/vyos/ifconfig/l2tpv3.py b/python/vyos/ifconfig/l2tpv3.py new file mode 100644 index 000000000..34147eb38 --- /dev/null +++ b/python/vyos/ifconfig/l2tpv3.py @@ -0,0 +1,113 @@ +# 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 + + +@Interface.register +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. + """ + + default = { + 'type': 'l2tp', + } + definition = { + **Interface.definition, + **{ + 'section': 'l2tpeth', + 'prefixes': ['l2tpeth', ], + 'bridgeable': True, + } + } + options = Interface.options + \ + ['tunnel_id', 'peer_tunnel_id', 'local_port', 'remote_port', + 'encapsulation', 'local_address', 'remote_address', 'session_id', + 'peer_session_id'] + + def _create(self): + # create tunnel interface + cmd = 'ip l2tp add tunnel tunnel_id {tunnel_id}' + cmd += ' peer_tunnel_id {peer_tunnel_id}' + cmd += ' udp_sport {local_port}' + cmd += ' udp_dport {remote_port}' + cmd += ' encap {encapsulation}' + cmd += ' local {local_address}' + cmd += ' remote {remote_address}' + self._cmd(cmd.format(**self.config)) + + # setup session + cmd = 'ip l2tp add session name {ifname}' + cmd += ' tunnel_id {tunnel_id}' + cmd += ' session_id {session_id}' + cmd += ' peer_session_id {peer_session_id}' + self._cmd(cmd.format(**self.config)) + + # interface is always A/D down. It needs to be enabled explicitly + self.set_admin_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_admin_state('down') + + if self.config['tunnel_id'] and self.config['session_id']: + cmd = 'ip l2tp del session tunnel_id {tunnel_id}' + cmd += ' session_id {session_id}' + self._cmd(cmd.format(**self.config)) + + if self.config['tunnel_id']: + cmd = 'ip l2tp del tunnel tunnel_id {tunnel_id}' + self._cmd(cmd.format(**self.config)) + + @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..2b4ebfdcc --- /dev/null +++ b/python/vyos/ifconfig/loopback.py @@ -0,0 +1,89 @@ +# 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 + + +@Interface.register +class LoopbackIf(Interface): + """ + The loopback device is a special, virtual network interface that your router + uses to communicate with itself. + """ + _persistent_addresses = ['127.0.0.1/8', '::1/128'] + default = { + 'type': 'loopback', + } + definition = { + **Interface.definition, + **{ + 'section': 'loopback', + 'prefixes': ['lo', ], + 'bridgeable': True, + } + } + + name = 'loopback' + + 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 self._persistent_addresses: + # 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) + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + addr = config.get('address', []) + # XXX workaround for T2636, convert IP address string to a list + # with one element + if isinstance(addr, str): + addr = [addr] + + # We must ensure that the loopback addresses are never deleted from the system + addr += self._persistent_addresses + + # Update IP address entry in our dictionary + config.update({'address' : addr}) + + # call base class + super().update(config) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/macsec.py b/python/vyos/ifconfig/macsec.py new file mode 100644 index 000000000..6f570d162 --- /dev/null +++ b/python/vyos/ifconfig/macsec.py @@ -0,0 +1,92 @@ +# Copyright 2020 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 + +@Interface.register +class MACsecIf(Interface): + """ + MACsec is an IEEE standard (IEEE 802.1AE) for MAC security, introduced in + 2006. It defines a way to establish a protocol independent connection + between two hosts with data confidentiality, authenticity and/or integrity, + using GCM-AES-128. MACsec operates on the Ethernet layer and as such is a + layer 2 protocol, which means it's designed to secure traffic within a + layer 2 network, including DHCP or ARP requests. It does not compete with + other security solutions such as IPsec (layer 3) or TLS (layer 4), as all + those solutions are used for their own specific use cases. + """ + + default = { + 'type': 'macsec', + 'security_cipher': '', + 'source_interface': '' + } + definition = { + **Interface.definition, + **{ + 'section': 'macsec', + 'prefixes': ['macsec', ], + }, + } + options = Interface.options + \ + ['security_cipher', 'source_interface'] + + def _create(self): + """ + Create MACsec interface in OS kernel. Interface is administrative + down by default. + """ + # create tunnel interface + cmd = 'ip link add link {source_interface} {ifname} type {type}' + cmd += ' cipher {security_cipher}' + self._cmd(cmd.format(**self.config)) + + # interface is always A/D down. It needs to be enabled explicitly + self.set_admin_state('down') + + @staticmethod + def get_config(): + """ + MACsec interfaces require a configuration when they are added using + iproute2. This static method will provide the configuration dictionary + used by this class. + + Example: + >> dict = MACsecIf().get_config() + """ + config = { + 'security_cipher': '', + 'source_interface': '', + } + return config + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # call base class first + super().update(config) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/macvlan.py b/python/vyos/ifconfig/macvlan.py new file mode 100644 index 000000000..b068ce873 --- /dev/null +++ b/python/vyos/ifconfig/macvlan.py @@ -0,0 +1,89 @@ +# 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 copy import deepcopy + +from vyos.ifconfig.interface import Interface +from vyos.ifconfig.vlan import VLAN + + +@Interface.register +@VLAN.enable +class MACVLANIf(Interface): + """ + Abstraction of a Linux MACvlan interface + """ + + default = { + 'type': 'macvlan', + 'address': '', + 'source_interface': '', + 'mode': '', + } + definition = { + **Interface.definition, + **{ + 'section': 'pseudo-ethernet', + 'prefixes': ['peth', ], + }, + } + options = Interface.options + \ + ['source_interface', 'mode'] + + def _create(self): + # please do not change the order when assembling the command + cmd = 'ip link add {ifname}' + if self.config['source_interface']: + cmd += ' link {source_interface}' + cmd += ' type macvlan' + if self.config['mode']: + cmd += ' mode {mode}' + self._cmd(cmd.format(**self.config)) + + def set_mode(self, mode): + ifname = self.config['ifname'] + cmd = f'ip link set dev {ifname} type macvlan mode {mode}' + return self._cmd(cmd) + + @classmethod + def get_config(cls): + """ + MACVLAN interfaces require a configuration when they are added using + iproute2. This method will provide the configuration dictionary used + by this class. + + Example: + >> dict = MACVLANIf().get_config() + """ + return deepcopy(cls.default) + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # call base class first + super().update(config) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/operational.py b/python/vyos/ifconfig/operational.py new file mode 100644 index 000000000..d585c1873 --- /dev/null +++ b/python/vyos/ifconfig/operational.py @@ -0,0 +1,179 @@ +# 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 time import time +from datetime import datetime +from functools import reduce + +from tabulate import tabulate + +from vyos.ifconfig import Control + + +class Operational(Control): + """ + A class able to load Interface statistics + """ + + cache_magic = 'XYZZYX' + + _stat_names = { + 'rx': ['bytes', 'packets', 'errors', 'dropped', 'overrun', 'mcast'], + 'tx': ['bytes', 'packets', 'errors', 'dropped', 'carrier', 'collisions'], + } + + _stats_dir = { + 'rx': ['rx_bytes', 'rx_packets', 'rx_errors', 'rx_dropped', 'rx_over_errors', 'multicast'], + 'tx': ['tx_bytes', 'tx_packets', 'tx_errors', 'tx_dropped', 'tx_carrier_errors', 'collisions'], + } + + # a list made of the content of _stats_dir['rx'] + _stats_dir['tx'] + _stats_all = reduce(lambda x, y: x+y, _stats_dir.values()) + + # this is not an interface but will be able to be controlled like one + _sysfs_get = { + 'oper_state':{ + 'location': '/sys/class/net/{ifname}/operstate', + }, + } + + + @classmethod + def cachefile (cls, ifname): + # the file where we are saving the counters + return f'/var/run/vyatta/{ifname}.stats' + + + def __init__(self, ifname): + """ + Operational provide access to the counters of an interface + It behave like an interface when it comes to access sysfs + + interface is an instance of the interface for which we want + to look at (a subclass of Interface, such as EthernetIf) + """ + + # add a self.config to minic Interface behaviour and make + # coding similar. Perhaps part of class Interface could be + # moved into a shared base class. + self.config = { + 'ifname': ifname, + 'create': False, + 'debug': False, + } + super().__init__(**self.config) + self.ifname = ifname + + # adds all the counters of an interface + for stat in self._stats_all: + self._sysfs_get[stat] = { + 'location': '/sys/class/net/{ifname}/statistics/'+stat, + } + + def get_state(self): + """ + Get interface operational state + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').operational.get_sate() + 'up' + """ + # https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-net + # "unknown", "notpresent", "down", "lowerlayerdown", "testing", "dormant", "up" + return self.get_interface('oper_state') + + @classmethod + def strtime (cls, epoc): + """ + represent an epoc/unix date in the format used by operation commands + """ + return datetime.fromtimestamp(epoc).strftime("%a %b %d %R:%S %Z %Y") + + def save_counters(self, stats): + """ + record the provided stats to a file keeping vyatta compatibility + """ + + with open(self.cachefile(self.ifname), 'w') as f: + f.write(self.cache_magic) + f.write('\n') + f.write(str(int(time()))) + f.write('\n') + for k,v in stats.items(): + if v: + f.write(f'{k},{v}\n') + + def load_counters(self): + """ + load the stats from a file keeping vyatta compatibility + return a dict() with the value for each interface counter for the cache + """ + ifname = self.config['ifname'] + + stats = {} + no_stats = {} + for name in self._stats_all: + stats[name] = 0 + no_stats[name] = 0 + + try: + with open(self.cachefile(self.ifname),'r') as f: + magic = f.readline().strip() + if magic != self.cache_magic: + print(f'bad magic {ifname}') + return no_stats + stats['timestamp'] = f.readline().strip() + for line in f: + k, v = line.split(',') + stats[k] = int(v) + return stats + except IOError: + return no_stats + + def clear_counters(self, counters=None): + clear = self._stats_all if counters is None else [] + stats = self.load_counters() + for counter, value in stats.items(): + stats[counter] = 0 if counter in clear else value + self.save_counters(stats) + + def reset_counters(self): + os.remove(self.cachefile(self.ifname)) + + def get_stats(self): + """ return a dict() with the value for each interface counter """ + stats = {} + for counter in self._stats_all: + stats[counter] = int(self.get_interface(counter)) + return stats + + def formated_stats(self, indent=4): + tabs = [] + stats = self.get_stats() + for rtx in self._stats_dir: + tabs.append([f'{rtx.upper()}:', ] + [_ for _ in self._stat_names[rtx]]) + tabs.append(['', ] + [stats[_] for _ in self._stats_dir[rtx]]) + + s = tabulate( + tabs, + stralign="right", + numalign="right", + tablefmt="plain" + ) + + p = ' '*indent + return f'{p}' + s.replace('\n', f'\n{p}') diff --git a/python/vyos/ifconfig/pppoe.py b/python/vyos/ifconfig/pppoe.py new file mode 100644 index 000000000..787245696 --- /dev/null +++ b/python/vyos/ifconfig/pppoe.py @@ -0,0 +1,41 @@ +# Copyright 2020 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 + + +@Interface.register +class PPPoEIf(Interface): + default = { + 'type': 'pppoe', + } + definition = { + **Interface.definition, + **{ + 'section': 'pppoe', + 'prefixes': ['pppoe', ], + }, + } + + # stub this interface is created in the configure script + + def _create(self): + # we can not create this interface as it is managed outside + pass + + def _delete(self): + # we can not create this interface as it is managed outside + pass diff --git a/python/vyos/ifconfig/section.py b/python/vyos/ifconfig/section.py new file mode 100644 index 000000000..173a90bb4 --- /dev/null +++ b/python/vyos/ifconfig/section.py @@ -0,0 +1,189 @@ +# Copyright 2020 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 re +import netifaces + + +class Section: + # the known interface prefixes + _prefixes = {} + _classes = [] + + # class need to define: definition['prefixes'] + # the interface prefixes declared by a class used to name interface with + # prefix[0-9]*(\.[0-9]+)?(\.[0-9]+)?, such as lo, eth0 or eth0.1.2 + + @classmethod + def register(cls, klass): + """ + A function to use as decorator the interfaces classes + It register the prefix for the interface (eth, dum, vxlan, ...) + with the class which can handle it (EthernetIf, DummyIf,VXLANIf, ...) + """ + if not klass.definition.get('prefixes',[]): + raise RuntimeError(f'valid interface prefixes not defined for {klass.__name__}') + + cls._classes.append(klass) + + for ifprefix in klass.definition['prefixes']: + if ifprefix in cls._prefixes: + raise RuntimeError(f'only one class can be registered for prefix "{ifprefix}" type') + cls._prefixes[ifprefix] = klass + + return klass + + @classmethod + def _basename (cls, name, vlan): + """ + remove the number at the end of interface name + name: name of the interface + vlan: if vlan is True, do not stop at the vlan number + """ + name = name.rstrip('0123456789') + name = name.rstrip('.') + if vlan: + name = name.rstrip('0123456789.') + return name + + @classmethod + def section(cls, name, vlan=True): + """ + return the name of a section an interface should be under + name: name of the interface (eth0, dum1, ...) + vlan: should we try try to remove the VLAN from the number + """ + name = cls._basename(name, vlan) + + if name in cls._prefixes: + return cls._prefixes[name].definition['section'] + return '' + + @classmethod + def sections(cls): + """ + return all the sections we found under 'set interfaces' + """ + return list(set([cls._prefixes[_].definition['section'] for _ in cls._prefixes])) + + @classmethod + def klass(cls, name, vlan=True): + name = cls._basename(name, vlan) + if name in cls._prefixes: + return cls._prefixes[name] + raise ValueError(f'No type found for interface name: {name}') + + @classmethod + def _intf_under_section (cls,section=''): + """ + return a generator with the name of the configured interface + which are under a section + """ + interfaces = netifaces.interfaces() + + for ifname in interfaces: + ifsection = cls.section(ifname) + if not ifsection: + continue + + if section and ifsection != section: + continue + + yield ifname + + @classmethod + def _sort_interfaces(cls, generator): + """ + return a list of the sorted interface by number, vlan, qinq + """ + def key(ifname): + value = 0 + parts = re.split(r'([^0-9]+)([0-9]+)[.]?([0-9]+)?[.]?([0-9]+)?', ifname) + length = len(parts) + name = parts[1] if length >= 3 else parts[0] + # the +1 makes sure eth0.0.0 after eth0.0 + number = int(parts[2]) + 1 if length >= 4 and parts[2] is not None else 0 + vlan = int(parts[3]) + 1 if length >= 5 and parts[3] is not None else 0 + qinq = int(parts[4]) + 1 if length >= 6 and parts[4] is not None else 0 + + # so that "lo" (or short names) are handled (as "loa") + for n in (name + 'aaa')[:3]: + value *= 100 + value += (ord(n) - ord('a')) + value += number + # vlan are 16 bits, so this can not overflow + value = (value << 16) + vlan + value = (value << 16) + qinq + return value + + l = list(generator) + l.sort(key=key) + return l + + @classmethod + def interfaces(cls, section=''): + """ + return a list of the name of the configured interface which are under a section + if no section is provided, then it returns all configured interfaces + """ + + return cls._sort_interfaces(cls._intf_under_section(section)) + + @classmethod + def _intf_with_feature(cls, feature=''): + """ + return a generator with the name of the configured interface which have + a particular feature set in their definition such as: + bondable, broadcast, bridgeable, ... + """ + for klass in cls._classes: + if klass.definition[feature]: + yield klass.definition['section'] + + @classmethod + def feature(cls, feature=''): + """ + return list with the name of the configured interface which have + a particular feature set in their definition such as: + bondable, broadcast, bridgeable, ... + """ + return list(cls._intf_with_feature(feature)) + + @classmethod + def reserved(cls): + """ + return list with the interface name prefixes + eth, lo, vxlan, dum, ... + """ + return list(cls._prefixes.keys()) + + @classmethod + def get_config_path(cls, name): + """ + get config path to interface with .vif or .vif-s.vif-c + example: eth0.1.2 -> 'ethernet eth0 vif-s 1 vif-c 2' + Returns False if interface name is invalid (not found in sections) + """ + sect = cls.section(name) + if sect: + splinterface = name.split('.') + intfpath = f'{sect} {splinterface[0]}' + if len(splinterface) == 2: + intfpath += f' vif {splinterface[1]}' + elif len(splinterface) == 3: + intfpath += f' vif-s {splinterface[1]} vif-c {splinterface[2]}' + return intfpath + else: + return False diff --git a/python/vyos/ifconfig/stp.py b/python/vyos/ifconfig/stp.py new file mode 100644 index 000000000..5e83206c2 --- /dev/null +++ b/python/vyos/ifconfig/stp.py @@ -0,0 +1,70 @@ +# 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 assert_positive + + +class STP: + """ + A spanning-tree capable interface. This applies only to bridge port member + interfaces! + """ + + @classmethod + def enable (cls, adaptee): + adaptee._sysfs_set = {**adaptee._sysfs_set, **cls._sysfs_set} + adaptee.set_path_cost = cls.set_path_cost + adaptee.set_path_priority = cls.set_path_priority + return adaptee + + _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' + }, + } + + 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/tunnel.py b/python/vyos/ifconfig/tunnel.py new file mode 100644 index 000000000..85c22b5b4 --- /dev/null +++ b/python/vyos/ifconfig/tunnel.py @@ -0,0 +1,338 @@ +# 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/>. + +# https://developers.redhat.com/blog/2019/05/17/an-introduction-to-linux-virtual-interfaces-tunnels/ +# https://community.hetzner.com/tutorials/linux-setup-gre-tunnel + + +from copy import deepcopy + +from vyos.ifconfig.interface import Interface +from vyos.ifconfig.afi import IP4, IP6 +from vyos.validate import assert_list + +def enable_to_on(value): + if value == 'enable': + return 'on' + if value == 'disable': + return 'off' + raise ValueError(f'expect enable or disable but got "{value}"') + + +@Interface.register +class _Tunnel(Interface): + """ + _Tunnel: private base class for tunnels + https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/tunnel.c + https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/ip6tunnel.c + """ + definition = { + **Interface.definition, + **{ + 'section': 'tunnel', + 'prefixes': ['tun',], + 'bridgeable': False, + }, + } + + # TODO: This is surely used for more than tunnels + # TODO: could be refactored elsewhere + _command_set = {**Interface._command_set, **{ + 'multicast': { + 'validate': lambda v: assert_list(v, ['enable', 'disable']), + 'convert': enable_to_on, + 'shellcmd': 'ip link set dev {ifname} multicast {value}', + }, + 'allmulticast': { + 'validate': lambda v: assert_list(v, ['enable', 'disable']), + 'convert': enable_to_on, + 'shellcmd': 'ip link set dev {ifname} allmulticast {value}', + }, + }} + + # use for "options" and "updates" + # If an key is only in the options list, it can only be set at creation time + # the create comand will only be make using the key in options + + # If an option is in the updates list, it can be updated + # upon, the creation, all key not yet applied will be updated + + # multicast/allmulticast can not be part of the create command + + # options matrix: + # with ip = 4, we have multicast + # wiht ip = 6, nothing + # with tunnel = 4, we have tos, ttl, key + # with tunnel = 6, we have encaplimit, hoplimit, tclass, flowlabel + + # TODO: For multicast, it is allowed on IP6IP6 and Sit6RD + # TODO: to match vyatta but it should be checked for correctness + + updates = [] + + create = '' + change = '' + delete = '' + + ip = [] # AFI of the families which can be used in the tunnel + tunnel = 0 # invalid - need to be set by subclasses + + def __init__(self, ifname, **config): + self.config = deepcopy(config) if config else {} + super().__init__(ifname, **config) + + def _create(self): + # add " option-name option-name-value ..." for all options set + options = " ".join(["{} {}".format(k, self.config[k]) + for k in self.options if k in self.config and self.config[k]]) + self._cmd('{} {}'.format(self.create.format(**self.config), options)) + self.set_admin_state('down') + + def _delete(self): + self.set_admin_state('down') + cmd = self.delete.format(**self.config) + return self._cmd(cmd) + + def set_interface(self, option, value): + try: + return Interface.set_interface(self, option, value) + except Exception: + pass + + if value == '': + # remove the value so that it is not used + self.config.pop(option, '') + + if self.change: + self._cmd('{} {} {}'.format( + self.change.format(**self.config), option, value)) + return True + + @classmethod + def get_config(cls): + return dict(zip(cls.options, ['']*len(cls.options))) + + +class GREIf(_Tunnel): + """ + GRE: Generic Routing Encapsulation + + For more information please refer to: + RFC1701, RFC1702, RFC2784 + https://tools.ietf.org/html/rfc2784 + https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/link_gre.c + """ + + definition = { + **_Tunnel.definition, + **{ + 'bridgeable': True, + }, + } + + ip = [IP4, IP6] + tunnel = IP4 + + default = {'type': 'gre'} + required = ['local', ] # mGRE is a GRE without remote endpoint + + options = ['local', 'remote', 'dev', 'ttl', 'tos', 'key'] + updates = ['local', 'remote', 'dev', 'ttl', 'tos', + 'mtu', 'multicast', 'allmulticast'] + + create = 'ip tunnel add {ifname} mode {type}' + change = 'ip tunnel cha {ifname}' + delete = 'ip tunnel del {ifname}' + + +# GreTap also called GRE Bridge +class GRETapIf(_Tunnel): + """ + GRETapIF: GreIF using TAP instead of TUN + + https://en.wikipedia.org/wiki/TUN/TAP + """ + + # no multicast, ttl or tos for gretap + + definition = { + **_Tunnel.definition, + **{ + 'bridgeable': True, + }, + } + + ip = [IP4, ] + tunnel = IP4 + + default = {'type': 'gretap'} + required = ['local', ] + + options = ['local', 'remote', ] + updates = ['mtu', ] + + create = 'ip link add {ifname} type {type}' + change = '' + delete = 'ip link del {ifname}' + + +class IP6GREIf(_Tunnel): + """ + IP6Gre: IPv6 Support for Generic Routing Encapsulation (GRE) + + For more information please refer to: + https://tools.ietf.org/html/rfc7676 + https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/link_gre6.c + """ + + ip = [IP4, IP6] + tunnel = IP6 + + default = {'type': 'ip6gre'} + required = ['local', 'remote'] + + options = ['local', 'remote', 'dev', 'encaplimit', + 'hoplimit', 'tclass', 'flowlabel'] + updates = ['local', 'remote', 'dev', 'encaplimit', + 'hoplimit', 'tclass', 'flowlabel', + 'mtu', 'multicast', 'allmulticast'] + + create = 'ip tunnel add {ifname} mode {type}' + change = 'ip tunnel cha {ifname} mode {type}' + delete = 'ip tunnel del {ifname}' + + # using "ip tunnel change" without using "mode" causes errors + # sudo ip tunnel add tun100 mode ip6gre local ::1 remote 1::1 + # sudo ip tunnel cha tun100 hoplimit 100 + # *** stack smashing detected ** *: < unknown > terminated + # sudo ip tunnel cha tun100 local: : 2 + # Error: an IP address is expected rather than "::2" + # works if mode is explicit + + +class IPIPIf(_Tunnel): + """ + IPIP: IP Encapsulation within IP + + For more information please refer to: + https://tools.ietf.org/html/rfc2003 + """ + + # IPIP does not allow to pass multicast, unlike GRE + # but the interface itself can be set with multicast + + ip = [IP4,] + tunnel = IP4 + + default = {'type': 'ipip'} + required = ['local', 'remote'] + + options = ['local', 'remote', 'dev', 'ttl', 'tos', 'key'] + updates = ['local', 'remote', 'dev', 'ttl', 'tos', + 'mtu', 'multicast', 'allmulticast'] + + create = 'ip tunnel add {ifname} mode {type}' + change = 'ip tunnel cha {ifname}' + delete = 'ip tunnel del {ifname}' + + +class IPIP6If(_Tunnel): + """ + IPIP6: IPv4 over IPv6 tunnel + + For more information please refer to: + https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/link_ip6tnl.c + """ + + ip = [IP4,] + tunnel = IP6 + + default = {'type': 'ipip6'} + required = ['local', 'remote'] + + options = ['local', 'remote', 'dev', 'encaplimit', + 'hoplimit', 'tclass', 'flowlabel'] + updates = ['local', 'remote', 'dev', 'encaplimit', + 'hoplimit', 'tclass', 'flowlabel', + 'mtu', 'multicast', 'allmulticast'] + + create = 'ip -6 tunnel add {ifname} mode {type}' + change = 'ip -6 tunnel cha {ifname}' + delete = 'ip -6 tunnel del {ifname}' + + +class IP6IP6If(IPIP6If): + """ + IP6IP6: IPv6 over IPv6 tunnel + + For more information please refer to: + https://tools.ietf.org/html/rfc2473 + """ + + ip = [IP6,] + + default = {'type': 'ip6ip6'} + + +class SitIf(_Tunnel): + """ + Sit: Simple Internet Transition + + For more information please refer to: + https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/link_iptnl.c + """ + + ip = [IP6, IP4] + tunnel = IP4 + + default = {'type': 'sit'} + required = ['local', 'remote'] + + options = ['local', 'remote', 'dev', 'ttl', 'tos', 'key'] + updates = ['local', 'remote', 'dev', 'ttl', 'tos', + 'mtu', 'multicast', 'allmulticast'] + + create = 'ip tunnel add {ifname} mode {type}' + change = 'ip tunnel cha {ifname}' + delete = 'ip tunnel del {ifname}' + + +class Sit6RDIf(SitIf): + """ + Sit6RDIf: Simple Internet Transition with 6RD + + https://en.wikipedia.org/wiki/IPv6_rapid_deployment + """ + + ip = [IP6,] + + required = ['remote', '6rd-prefix'] + + # TODO: check if key can really be used with 6RD + options = ['remote', 'ttl', 'tos', 'key', '6rd-prefix', '6rd-relay-prefix'] + updates = ['remote', 'ttl', 'tos', + 'mtu', 'multicast', 'allmulticast'] + + def _create(self): + # do not call _Tunnel.create, building fully here + + create = 'ip tunnel add {ifname} mode {type} remote {remote}' + self._cmd(create.format(**self.config)) + self.set_interface('state','down') + + set6rd = 'ip tunnel 6rd dev {ifname} 6rd-prefix {6rd-prefix}' + if '6rd-relay-prefix' in self.config: + set6rd += ' 6rd-relay-prefix {6rd-relay-prefix}' + self._cmd(set6rd.format(**self.config)) diff --git a/python/vyos/ifconfig/vlan.py b/python/vyos/ifconfig/vlan.py new file mode 100644 index 000000000..d68e8f6cd --- /dev/null +++ b/python/vyos/ifconfig/vlan.py @@ -0,0 +1,142 @@ +# 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 + + +# This is an internal implementation class +class VLAN: + """ + This class handels the creation and removal of a VLAN interface. It serves + as base class for BondIf and EthernetIf. + """ + + _novlan_remove = lambda : None + + @classmethod + def enable (cls,adaptee): + adaptee._novlan_remove = adaptee.remove + adaptee.remove = cls.remove + adaptee.add_vlan = cls.add_vlan + adaptee.del_vlan = cls.del_vlan + adaptee.definition['vlan'] = True + return adaptee + + 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() + """ + ifname = self.config['ifname'] + + # 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(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(ifname + r'(?:\.\d+)', f)] + + for vlan in vlan_ifs: + # self.__class__ is already VLAN.enabled + self.__class__(vlan)._novlan_remove() + + # All subinterfaces are now removed, continue on the physical interface + self._novlan_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 MACVLANIf + >>> i = MACVLANIf('eth0') + >>> i.add_vlan(10) + """ + vlan_ifname = self.config['ifname'] + '.' + str(vlan_id) + if os.path.exists(f'/sys/class/net/{vlan_ifname}'): + return self.__class__(vlan_ifname) + + 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.ifname, vlan=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 self.__class__(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 MACVLANIf + >>> i = MACVLANIf('eth0.10') + >>> i.del_vlan() + """ + ifname = self.config['ifname'] + self.__class__(f'{ifname}.{vlan_id}')._novlan_remove() diff --git a/python/vyos/ifconfig/vrrp.py b/python/vyos/ifconfig/vrrp.py new file mode 100644 index 000000000..01a7cc7ab --- /dev/null +++ b/python/vyos/ifconfig/vrrp.py @@ -0,0 +1,151 @@ +# 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 json +import signal +from time import time +from time import sleep + +from tabulate import tabulate + +from vyos import airbag +from vyos import util + + +class VRRPError(Exception): + pass + +class VRRPNoData(VRRPError): + pass + +class VRRP(object): + _vrrp_prefix = '00:00:5E:00:01:' + location = { + 'pid': '/run/keepalived.pid', + 'fifo': '/run/keepalived_notify_fifo', + 'state': '/tmp/keepalived.data', + 'stats': '/tmp/keepalived.stats', + 'json': '/tmp/keepalived.json', + 'daemon': '/etc/default/keepalived', + 'config': '/etc/keepalived/keepalived.conf', + 'vyos': '/run/keepalived_config.dict', + } + + _signal = { + 'state': signal.SIGUSR1, + 'stats': signal.SIGUSR2, + 'json': signal.SIGRTMIN + 2, + } + + _name = { + 'state': 'information', + 'stats': 'statistics', + 'json': 'data', + } + + state = { + 0: 'INIT', + 1: 'BACKUP', + 2: 'MASTER', + 3: 'FAULT', + # UNKNOWN + } + + def __init__(self,ifname): + self.ifname = ifname + + def enabled(self): + return self.ifname in self.active_interfaces() + + @classmethod + def active_interfaces(cls): + if not os.path.exists(cls.location['pid']): + return [] + data = cls.collect('json') + return [group['data']['ifp_ifname'] for group in json.loads(data)] + + @classmethod + def decode_state(cls, code): + return cls.state.get(code,'UNKNOWN') + + # used in conf mode + @classmethod + def is_running(cls): + if not os.path.exists(cls.location['pid']): + return False + return util.process_running(cls.location['pid']) + + @classmethod + def collect(cls, what): + fname = cls.location[what] + try: + # send signal to generate the configuration file + pid = util.read_file(cls.location['pid']) + os.kill(int(pid), cls._signal[what]) + + # should look for file size change? + sleep(0.2) + return util.read_file(fname) + except FileNotFoundError: + raise VRRPNoData("VRRP data is not available (process not running or no active groups)") + except Exception: + name = cls._name[what] + raise VRRPError(f'VRRP {name} is not available') + finally: + if os.path.exists(fname): + os.remove(fname) + + @classmethod + def disabled(cls): + if not os.path.exists(cls.location['vyos']): + return [] + + disabled = [] + config = json.loads(util.read_file(cls.location['vyos'])) + + # add disabled groups to the list + for group in config['vrrp_groups']: + if group['disable']: + disabled.append( + [group['name'], group['interface'], group['vrid'], 'DISABLED', '']) + + # return list with disabled instances + return disabled + + @classmethod + def format(cls, data): + headers = ["Name", "Interface", "VRID", "State", "Priority", "Last Transition"] + groups = [] + + data = json.loads(data) + for group in data: + data = group['data'] + + name = data['iname'] + intf = data['ifp_ifname'] + vrid = data['vrid'] + state = cls.decode_state(data["state"]) + priority = data['effective_priority'] + + since = int(time() - float(data['last_transition'])) + last = util.seconds_to_human(since) + + groups.append([name, intf, vrid, state, priority, last]) + + # add to the active list disabled instances + groups.extend(cls.disabled()) + return(tabulate(groups, headers)) + diff --git a/python/vyos/ifconfig/vti.py b/python/vyos/ifconfig/vti.py new file mode 100644 index 000000000..56ebe01d1 --- /dev/null +++ b/python/vyos/ifconfig/vti.py @@ -0,0 +1,31 @@ +# Copyright 2020 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 + + +@Interface.register +class VTIIf(Interface): + default = { + 'type': 'vti', + } + definition = { + **Interface.definition, + **{ + 'section': 'vti', + 'prefixes': ['vti', ], + }, + } diff --git a/python/vyos/ifconfig/vtun.py b/python/vyos/ifconfig/vtun.py new file mode 100644 index 000000000..60c178b9a --- /dev/null +++ b/python/vyos/ifconfig/vtun.py @@ -0,0 +1,44 @@ +# Copyright 2020 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 + + +@Interface.register +class VTunIf(Interface): + default = { + 'type': 'vtun', + } + definition = { + **Interface.definition, + **{ + 'section': 'openvpn', + 'prefixes': ['vtun', ], + 'bridgeable': True, + }, + } + + # stub this interface is created in the configure script + + def _create(self): + # we can not create this interface as it is managed outside + # it requires configuring OpenVPN + pass + + def _delete(self): + # we can not create this interface as it is managed outside + # it requires configuring OpenVPN + pass diff --git a/python/vyos/ifconfig/vxlan.py b/python/vyos/ifconfig/vxlan.py new file mode 100644 index 000000000..18a500336 --- /dev/null +++ b/python/vyos/ifconfig/vxlan.py @@ -0,0 +1,130 @@ +# 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 copy import deepcopy + +from vyos import ConfigError +from vyos.ifconfig.interface import Interface + + +@Interface.register +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', + 'group': '', + 'port': 8472, # The Linux implementation of VXLAN pre-dates + # the IANA's selection of a standard destination port + 'remote': '', + 'source_address': '', + 'source_interface': '', + 'vni': 0 + } + definition = { + **Interface.definition, + **{ + 'section': 'vxlan', + 'prefixes': ['vxlan', ], + 'bridgeable': True, + } + } + options = Interface.options + \ + ['group', 'remote', 'source_interface', 'port', 'vni', 'source_address'] + + mapping = { + 'ifname': 'add', + 'vni': 'id', + 'port': 'dstport', + 'source_address': 'local', + 'source_interface': 'dev', + } + + def _create(self): + cmdline = ['ifname', 'type', 'vni', 'port'] + + if self.config['source_address']: + cmdline.append('source_address') + + if self.config['remote']: + cmdline.append('remote') + + if self.config['group'] or self.config['source_interface']: + if self.config['group'] and self.config['source_interface']: + cmdline.append('group') + cmdline.append('source_interface') + else: + ifname = self.config['ifname'] + raise ConfigError( + f'VXLAN "{ifname}" is missing mandatory underlay multicast' + 'group or source interface for a multicast network.') + + cmd = 'ip link' + for key in cmdline: + value = self.config.get(key, '') + if not value: + continue + cmd += ' {} {}'.format(self.mapping.get(key, key), value) + + self._cmd(cmd) + + @classmethod + def get_config(cls): + """ + 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() + """ + return deepcopy(cls.default) + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # call base class first + super().update(config) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py new file mode 100644 index 000000000..fad4ef282 --- /dev/null +++ b/python/vyos/ifconfig/wireguard.py @@ -0,0 +1,247 @@ +# 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 time +from datetime import timedelta + +from hurry.filesize import size +from hurry.filesize import alternative + +from vyos.config import Config +from vyos.ifconfig import Interface +from vyos.ifconfig import Operational +from vyos.validate import is_ipv6 + +class WireGuardOperational(Operational): + def _dump(self): + """Dump wireguard data in a python friendly way.""" + last_device = None + output = {} + + # Dump wireguard connection data + _f = self._cmd('wg show all dump') + for line in _f.split('\n'): + if not line: + # Skip empty lines and last line + continue + items = line.split('\t') + + if last_device != items[0]: + # We are currently entering a new node + device, private_key, public_key, listen_port, fw_mark = items + last_device = device + + output[device] = { + 'private_key': None if private_key == '(none)' else private_key, + 'public_key': None if public_key == '(none)' else public_key, + 'listen_port': int(listen_port), + 'fw_mark': None if fw_mark == 'off' else int(fw_mark), + 'peers': {}, + } + else: + # We are entering a peer + device, public_key, preshared_key, endpoint, allowed_ips, latest_handshake, transfer_rx, transfer_tx, persistent_keepalive = items + if allowed_ips == '(none)': + allowed_ips = [] + else: + allowed_ips = allowed_ips.split('\t') + output[device]['peers'][public_key] = { + 'preshared_key': None if preshared_key == '(none)' else preshared_key, + 'endpoint': None if endpoint == '(none)' else endpoint, + 'allowed_ips': allowed_ips, + 'latest_handshake': None if latest_handshake == '0' else int(latest_handshake), + 'transfer_rx': int(transfer_rx), + 'transfer_tx': int(transfer_tx), + 'persistent_keepalive': None if persistent_keepalive == 'off' else int(persistent_keepalive), + } + return output + + def show_interface(self): + wgdump = self._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"]) + + answer = "interface: {}\n".format(self.config['ifname']) + if (description): + answer += " description: {}\n".format(description) + if (ips): + answer += " address: {}\n".format(", ".join(ips)) + + answer += " public key: {}\n".format(wgdump['public_key']) + answer += " private key: (hidden)\n" + answer += " listening port: {}\n".format(wgdump['listen_port']) + answer += "\n" + + 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] + + answer += " peer: {}\n".format(peer) + answer += " public key: {}\n".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'])) + answer += " latest handshake: {}\n".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" + answer += " status: {}\n".format(status) + + if wgpeer['endpoint'] is not None: + answer += " endpoint: {}\n".format(wgpeer['endpoint']) + + if wgpeer['allowed_ips'] is not None: + answer += " allowed ips: {}\n".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) + answer += " transfer: {} received, {} sent\n".format( + rx_size, tx_size) + + if wgpeer['persistent_keepalive'] is not None: + answer += " persistent keepalive: every {} seconds\n".format( + wgpeer['persistent_keepalive']) + answer += '\n' + return answer + super().formated_stats() + + +@Interface.register +class WireGuardIf(Interface): + OperationalClass = WireGuardOperational + + default = { + 'type': 'wireguard', + 'port': 0, + 'private_key': None, + 'pubkey': None, + 'psk': '', + 'allowed_ips': [], + 'fwmark': 0x00, + 'endpoint': None, + 'keepalive': 0 + } + definition = { + **Interface.definition, + **{ + 'section': 'wireguard', + 'prefixes': ['wg', ], + 'bridgeable': True, + } + } + options = Interface.options + \ + ['port', 'private_key', 'pubkey', 'psk', + 'allowed_ips', 'fwmark', 'endpoint', 'keepalive'] + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # remove no longer associated peers first + if 'peer_remove' in config: + for tmp in config['peer_remove']: + peer = config['peer_remove'][tmp] + peer['ifname'] = config['ifname'] + + cmd = 'wg set {ifname} peer {pubkey} remove' + self._cmd(cmd.format(**peer)) + + # Wireguard base command is identical for every peer + base_cmd = 'wg set {ifname} private-key {private_key}' + if 'port' in config: + base_cmd += ' listen-port {port}' + if 'fwmark' in config: + base_cmd += ' fwmark {fwmark}' + + base_cmd = base_cmd.format(**config) + + for tmp in config['peer']: + peer = config['peer'][tmp] + + # start of with a fresh 'wg' command + cmd = base_cmd + ' peer {pubkey}' + + # If no PSK is given remove it by using /dev/null - passing keys via + # the shell (usually bash) is considered insecure, thus we use a file + no_psk_file = '/dev/null' + psk_file = no_psk_file + if 'preshared_key' in peer: + psk_file = '/tmp/tmp.wireguard.psk' + with open(psk_file, 'w') as f: + f.write(peer['preshared_key']) + cmd += f' preshared-key {psk_file}' + + # Persistent keepalive is optional + if 'persistent_keepalive'in peer: + cmd += ' persistent-keepalive {persistent_keepalive}' + + # Multiple allowed-ip ranges can be defined - ensure we are always + # dealing with a list + if isinstance(peer['allowed_ips'], str): + peer['allowed_ips'] = [peer['allowed_ips']] + cmd += ' allowed-ips ' + ','.join(peer['allowed_ips']) + + # Endpoint configuration is optional + if {'address', 'port'} <= set(peer): + if is_ipv6(config['address']): + cmd += ' endpoint [{address}]:{port}' + else: + cmd += ' endpoint {address}:{port}' + + self._cmd(cmd.format(**peer)) + + # PSK key file is not required to be stored persistently as its backed by CLI + if psk_file != no_psk_file and os.path.exists(psk_file): + os.remove(psk_file) + + # call base class + super().update(config) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) + diff --git a/python/vyos/ifconfig/wireless.py b/python/vyos/ifconfig/wireless.py new file mode 100644 index 000000000..a50346ffa --- /dev/null +++ b/python/vyos/ifconfig/wireless.py @@ -0,0 +1,102 @@ +# Copyright 2020 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 VLAN + + +@Interface.register +@VLAN.enable +class WiFiIf(Interface): + """ + Handle WIFI/WLAN interfaces. + """ + + default = { + 'type': 'wifi', + 'phy': 'phy0' + } + definition = { + **Interface.definition, + **{ + 'section': 'wireless', + 'prefixes': ['wlan', ], + 'bridgeable': True, + } + } + options = Interface.options + \ + ['phy', 'op_mode'] + + def _create(self): + # all interfaces will be added in monitor mode + cmd = 'iw phy {phy} interface add {ifname} type monitor' \ + .format(**self.config) + self._cmd(cmd) + + # wireless interface is administratively down by default + self.set_admin_state('down') + + def _delete(self): + cmd = 'iw dev {ifname} del' \ + .format(**self.config) + self._cmd(cmd) + + @staticmethod + def get_config(): + """ + WiFi interfaces require a configuration when they are added using + iw (type/phy). This static method will provide the configuration + ictionary used by this class. + + Example: + >> conf = WiFiIf().get_config() + """ + config = { + 'phy': 'phy0' + } + return config + + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # call base class first + super().update(config) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) + + +@Interface.register +class WiFiModemIf(WiFiIf): + definition = { + **WiFiIf.definition, + **{ + 'section': 'wirelessmodem', + 'prefixes': ['wlm', ], + } + } |