From fe7279454c86a2947b23fb0d769483b7fe2a3cc3 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 13 Oct 2019 12:40:54 +0200 Subject: Sync XML interface description source file pattern and conf script name renamed: interface-bonding.py -> interfaces-bonding.py renamed: interface-bridge.py -> interfaces-bridge.py renamed: interface-dummy.py -> interfaces-dummy.py renamed: interface-ethernet.py -> interfaces-ethernet.py renamed: interface-loopback.py -> interfaces-loopback.py renamed: interface-openvpn.py -> interfaces-openvpn.py renamed: interface-vxlan.py -> interfaces-vxlan.py renamed: interface-wireguard.py -> interfaces-wireguard.py --- interface-definitions/interfaces-bonding.xml | 2 +- interface-definitions/interfaces-bridge.xml | 2 +- interface-definitions/interfaces-dummy.xml | 2 +- interface-definitions/interfaces-ethernet.xml | 2 +- interface-definitions/interfaces-loopback.xml | 2 +- interface-definitions/interfaces-openvpn.xml | 2 +- interface-definitions/interfaces-vxlan.xml | 2 +- interface-definitions/interfaces-wireguard.xml | 2 +- src/conf_mode/interface-bonding.py | 526 -------------- src/conf_mode/interface-bridge.py | 307 -------- src/conf_mode/interface-dummy.py | 114 --- src/conf_mode/interface-ethernet.py | 452 ------------ src/conf_mode/interface-loopback.py | 100 --- src/conf_mode/interface-openvpn.py | 960 ------------------------- src/conf_mode/interface-vxlan.py | 198 ----- src/conf_mode/interface-wireguard.py | 285 -------- src/conf_mode/interfaces-bonding.py | 526 ++++++++++++++ src/conf_mode/interfaces-bridge.py | 307 ++++++++ src/conf_mode/interfaces-dummy.py | 114 +++ src/conf_mode/interfaces-ethernet.py | 452 ++++++++++++ src/conf_mode/interfaces-loopback.py | 100 +++ src/conf_mode/interfaces-openvpn.py | 960 +++++++++++++++++++++++++ src/conf_mode/interfaces-vxlan.py | 198 +++++ src/conf_mode/interfaces-wireguard.py | 285 ++++++++ 24 files changed, 2950 insertions(+), 2950 deletions(-) delete mode 100755 src/conf_mode/interface-bonding.py delete mode 100755 src/conf_mode/interface-bridge.py delete mode 100755 src/conf_mode/interface-dummy.py delete mode 100755 src/conf_mode/interface-ethernet.py delete mode 100755 src/conf_mode/interface-loopback.py delete mode 100755 src/conf_mode/interface-openvpn.py delete mode 100755 src/conf_mode/interface-vxlan.py delete mode 100755 src/conf_mode/interface-wireguard.py create mode 100755 src/conf_mode/interfaces-bonding.py create mode 100755 src/conf_mode/interfaces-bridge.py create mode 100755 src/conf_mode/interfaces-dummy.py create mode 100755 src/conf_mode/interfaces-ethernet.py create mode 100755 src/conf_mode/interfaces-loopback.py create mode 100755 src/conf_mode/interfaces-openvpn.py create mode 100755 src/conf_mode/interfaces-vxlan.py create mode 100755 src/conf_mode/interfaces-wireguard.py diff --git a/interface-definitions/interfaces-bonding.xml b/interface-definitions/interfaces-bonding.xml index 7279e5993..c71482d9c 100644 --- a/interface-definitions/interfaces-bonding.xml +++ b/interface-definitions/interfaces-bonding.xml @@ -2,7 +2,7 @@ - + Bonding interface name 315 diff --git a/interface-definitions/interfaces-bridge.xml b/interface-definitions/interfaces-bridge.xml index c026c29b3..40505d7de 100644 --- a/interface-definitions/interfaces-bridge.xml +++ b/interface-definitions/interfaces-bridge.xml @@ -2,7 +2,7 @@ - + Bridge interface name 470 diff --git a/interface-definitions/interfaces-dummy.xml b/interface-definitions/interfaces-dummy.xml index c9860fe3b..3bc4330e4 100644 --- a/interface-definitions/interfaces-dummy.xml +++ b/interface-definitions/interfaces-dummy.xml @@ -2,7 +2,7 @@ - + Dummy interface name 300 diff --git a/interface-definitions/interfaces-ethernet.xml b/interface-definitions/interfaces-ethernet.xml index 96d2fda10..f51bb3d87 100644 --- a/interface-definitions/interfaces-ethernet.xml +++ b/interface-definitions/interfaces-ethernet.xml @@ -2,7 +2,7 @@ - + Ethernet interface name 318 diff --git a/interface-definitions/interfaces-loopback.xml b/interface-definitions/interfaces-loopback.xml index 267731b1c..0f003bc64 100644 --- a/interface-definitions/interfaces-loopback.xml +++ b/interface-definitions/interfaces-loopback.xml @@ -2,7 +2,7 @@ - + Loopback interface 300 diff --git a/interface-definitions/interfaces-openvpn.xml b/interface-definitions/interfaces-openvpn.xml index 365d80558..42c953fdc 100644 --- a/interface-definitions/interfaces-openvpn.xml +++ b/interface-definitions/interfaces-openvpn.xml @@ -2,7 +2,7 @@ - + OpenVPN tunnel interface name 460 diff --git a/interface-definitions/interfaces-vxlan.xml b/interface-definitions/interfaces-vxlan.xml index b06c2860c..f93711741 100644 --- a/interface-definitions/interfaces-vxlan.xml +++ b/interface-definitions/interfaces-vxlan.xml @@ -2,7 +2,7 @@ - + Virtual extensible LAN interface (VXLAN) 460 diff --git a/interface-definitions/interfaces-wireguard.xml b/interface-definitions/interfaces-wireguard.xml index f2a7cc316..0c32a3bc1 100644 --- a/interface-definitions/interfaces-wireguard.xml +++ b/interface-definitions/interfaces-wireguard.xml @@ -2,7 +2,7 @@ - + WireGuard interface name 459 diff --git a/src/conf_mode/interface-bonding.py b/src/conf_mode/interface-bonding.py deleted file mode 100755 index 8a0f9f84d..000000000 --- a/src/conf_mode/interface-bonding.py +++ /dev/null @@ -1,526 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2019 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import os - -from copy import deepcopy -from sys import exit -from netifaces import interfaces - -from vyos.ifconfig import BondIf, VLANIf -from vyos.configdict import list_diff, vlan_to_dict -from vyos.config import Config -from vyos import ConfigError - -default_config_data = { - 'address': [], - 'address_remove': [], - 'arp_mon_intvl': 0, - 'arp_mon_tgt': [], - 'description': '', - 'deleted': False, - 'dhcp_client_id': '', - 'dhcp_hostname': '', - 'dhcp_vendor_class_id': '', - 'dhcpv6_prm_only': False, - 'dhcpv6_temporary': False, - 'disable': False, - 'disable_link_detect': 1, - 'hash_policy': 'layer2', - 'ip_arp_cache_tmo': 30, - 'ip_proxy_arp': 0, - 'ip_proxy_arp_pvlan': 0, - 'intf': '', - 'mac': '', - 'mode': '802.3ad', - 'member': [], - 'mtu': 1500, - 'primary': '', - 'vif_s': [], - 'vif_s_remove': [], - 'vif': [], - 'vif_remove': [] -} - - -def get_bond_mode(mode): - if mode == 'round-robin': - return 'balance-rr' - elif mode == 'active-backup': - return 'active-backup' - elif mode == 'xor-hash': - return 'balance-xor' - elif mode == 'broadcast': - return 'broadcast' - elif mode == '802.3ad': - return '802.3ad' - elif mode == 'transmit-load-balance': - return 'balance-tlb' - elif mode == 'adaptive-load-balance': - return 'balance-alb' - else: - raise ConfigError('invalid bond mode "{}"'.format(mode)) - - -def apply_vlan_config(vlan, config): - """ - Generic function to apply a VLAN configuration from a dictionary - to a VLAN interface - """ - - if type(vlan) != type(VLANIf("lo")): - raise TypeError() - - # get DHCP config dictionary and update values - opt = vlan.get_dhcp_options() - - if config['dhcp_client_id']: - opt['client_id'] = config['dhcp_client_id'] - - if config['dhcp_hostname']: - opt['hostname'] = config['dhcp_hostname'] - - if config['dhcp_vendor_class_id']: - opt['vendor_class_id'] = config['dhcp_vendor_class_id'] - - # store DHCP config dictionary - used later on when addresses are aquired - vlan.set_dhcp_options(opt) - - # get DHCPv6 config dictionary and update values - opt = vlan.get_dhcpv6_options() - - if config['dhcpv6_prm_only']: - opt['dhcpv6_prm_only'] = True - - if config['dhcpv6_temporary']: - opt['dhcpv6_temporary'] = True - - # store DHCPv6 config dictionary - used later on when addresses are aquired - vlan.set_dhcpv6_options(opt) - - # update interface description used e.g. within SNMP - vlan.set_alias(config['description']) - # ignore link state changes - vlan.set_link_detect(config['disable_link_detect']) - # Maximum Transmission Unit (MTU) - vlan.set_mtu(config['mtu']) - # Change VLAN interface MAC address - if config['mac']: - vlan.set_mac(config['mac']) - - # enable/disable VLAN interface - if config['disable']: - vlan.set_state('down') - else: - vlan.set_state('up') - - # Configure interface address(es) - # - not longer required addresses get removed first - # - newly addresses will be added second - for addr in config['address_remove']: - vlan.del_addr(addr) - for addr in config['address']: - vlan.add_addr(addr) - - -def get_config(): - # initialize kernel module if not loaded - if not os.path.isfile('/sys/class/net/bonding_masters'): - import syslog - syslog.syslog(syslog.LOG_NOTICE, "loading bonding kernel module") - if os.system('modprobe bonding max_bonds=0 miimon=250') != 0: - syslog.syslog(syslog.LOG_NOTICE, "failed loading bonding kernel module") - raise ConfigError("failed loading bonding kernel module") - - bond = deepcopy(default_config_data) - conf = Config() - - # determine tagNode instance - try: - bond['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - except KeyError as E: - print("Interface not specified") - - # check if bond has been removed - cfg_base = 'interfaces bonding ' + bond['intf'] - if not conf.exists(cfg_base): - bond['deleted'] = True - return bond - - # set new configuration level - conf.set_level(cfg_base) - - # retrieve configured interface addresses - if conf.exists('address'): - bond['address'] = conf.return_values('address') - - # get interface addresses (currently effective) - to determine which - # address is no longer valid and needs to be removed - eff_addr = conf.return_effective_values('address') - bond['address_remove'] = list_diff(eff_addr, bond['address']) - - # ARP link monitoring frequency in milliseconds - if conf.exists('arp-monitor interval'): - bond['arp_mon_intvl'] = int(conf.return_value('arp-monitor interval')) - - # IP address to use for ARP monitoring - if conf.exists('arp-monitor target'): - bond['arp_mon_tgt'] = conf.return_values('arp-monitor target') - - # retrieve interface description - if conf.exists('description'): - bond['description'] = conf.return_value('description') - - # get DHCP client identifier - if conf.exists('dhcp-options client-id'): - bond['dhcp_client_id'] = conf.return_value('dhcp-options client-id') - - # DHCP client host name (overrides the system host name) - if conf.exists('dhcp-options host-name'): - bond['dhcp_hostname'] = conf.return_value('dhcp-options host-name') - - # DHCP client vendor identifier - if conf.exists('dhcp-options vendor-class-id'): - bond['dhcp_vendor_class_id'] = conf.return_value('dhcp-options vendor-class-id') - - # DHCPv6 only acquire config parameters, no address - if conf.exists('dhcpv6-options parameters-only'): - bond['dhcpv6_prm_only'] = True - - # DHCPv6 temporary IPv6 address - if conf.exists('dhcpv6-options temporary'): - bond['dhcpv6_temporary'] = True - - # ignore link state changes - if conf.exists('disable-link-detect'): - bond['disable_link_detect'] = 2 - - # disable bond interface - if conf.exists('disable'): - bond['disable'] = True - - # Bonding transmit hash policy - if conf.exists('hash-policy'): - bond['hash_policy'] = conf.return_value('hash-policy') - - # ARP cache entry timeout in seconds - if conf.exists('ip arp-cache-timeout'): - bond['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) - - # Enable proxy-arp on this interface - if conf.exists('ip enable-proxy-arp'): - bond['ip_proxy_arp'] = 1 - - # Enable private VLAN proxy ARP on this interface - if conf.exists('ip proxy-arp-pvlan'): - bond['ip_proxy_arp_pvlan'] = 1 - - # Media Access Control (MAC) address - if conf.exists('mac'): - bond['mac'] = conf.return_value('mac') - - # Bonding mode - if conf.exists('mode'): - bond['mode'] = get_bond_mode(conf.return_value('mode')) - - # Maximum Transmission Unit (MTU) - if conf.exists('mtu'): - bond['mtu'] = int(conf.return_value('mtu')) - - # determine bond member interfaces (currently configured) - if conf.exists('member interface'): - bond['member'] = conf.return_values('member interface') - - # Primary device interface - if conf.exists('primary'): - bond['primary'] = conf.return_value('primary') - - # re-set configuration level to parse new nodes - conf.set_level(cfg_base) - # get vif-s interfaces (currently effective) - to determine which vif-s - # interface is no longer present and needs to be removed - eff_intf = conf.list_effective_nodes('vif-s') - act_intf = conf.list_nodes('vif-s') - bond['vif_s_remove'] = list_diff(eff_intf, act_intf) - - if conf.exists('vif-s'): - for vif_s in conf.list_nodes('vif-s'): - # set config level to vif-s interface - conf.set_level(cfg_base + ' vif-s ' + vif_s) - bond['vif_s'].append(vlan_to_dict(conf)) - - # re-set configuration level to parse new nodes - conf.set_level(cfg_base) - # Determine vif interfaces (currently effective) - to determine which - # vif interface is no longer present and needs to be removed - eff_intf = conf.list_effective_nodes('vif') - act_intf = conf.list_nodes('vif') - bond['vif_remove'] = list_diff(eff_intf, act_intf) - - if conf.exists('vif'): - for vif in conf.list_nodes('vif'): - # set config level to vif interface - conf.set_level(cfg_base + ' vif ' + vif) - bond['vif'].append(vlan_to_dict(conf)) - - return bond - - -def verify(bond): - if len (bond['arp_mon_tgt']) > 16: - raise ConfigError('The maximum number of targets that can be specified is 16') - - if bond['primary']: - if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']: - raise ConfigError('Mode dependency failed, primary not supported ' \ - 'in this mode.'.format()) - - if bond['primary'] not in bond['member']: - raise ConfigError('Interface "{}" is not part of the bond' \ - .format(bond['primary'])) - - - # DHCPv6 parameters-only and temporary address are mutually exclusive - for vif_s in bond['vif_s']: - if vif_s['dhcpv6_prm_only'] and vif_s['dhcpv6_temporary']: - raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - - for vif_c in vif_s['vif_c']: - if vif_c['dhcpv6_prm_only'] and vif_c['dhcpv6_temporary']: - raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - - for vif in bond['vif']: - if vif['dhcpv6_prm_only'] and vif['dhcpv6_temporary']: - raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - - - for vif_s in bond['vif_s']: - for vif in bond['vif']: - if vif['id'] == vif_s['id']: - raise ConfigError('Can not use identical ID on vif and vif-s interface') - - - conf = Config() - for intf in bond['member']: - # a bonding member interface is only allowed to be assigned to one bond! - all_bonds = conf.list_nodes('interfaces bonding') - # We do not need to check our own bond - all_bonds.remove(bond['intf']) - for tmp in all_bonds: - if conf.exists('interfaces bonding ' + tmp + ' member interface ' + intf): - raise ConfigError('can not enslave interface {} which already ' \ - 'belongs to {}'.format(intf, tmp)) - - # can not add interfaces with an assigned address to a bond - if conf.exists('interfaces ethernet ' + intf + ' address'): - raise ConfigError('can not enslave interface {} which has an address ' \ - 'assigned'.format(intf)) - - # bond members are not allowed to be bridge members, too - for tmp in conf.list_nodes('interfaces bridge'): - if conf.exists('interfaces bridge ' + tmp + ' member interface ' + intf): - raise ConfigError('can not enslave interface {} which belongs to ' \ - 'bridge {}'.format(intf, tmp)) - - # bond members are not allowed to be vrrp members, too - for tmp in conf.list_nodes('high-availability vrrp group'): - if conf.exists('high-availability vrrp group ' + tmp + ' interface ' + intf): - raise ConfigError('can not enslave interface {} which belongs to ' \ - 'VRRP group {}'.format(intf, tmp)) - - # bond members are not allowed to be underlaying psuedo-ethernet devices - for tmp in conf.list_nodes('interfaces pseudo-ethernet'): - if conf.exists('interfaces pseudo-ethernet ' + tmp + ' link ' + intf): - raise ConfigError('can not enslave interface {} which belongs to ' \ - 'pseudo-ethernet {}'.format(intf, tmp)) - - # bond members are not allowed to be underlaying vxlan devices - for tmp in conf.list_nodes('interfaces vxlan'): - if conf.exists('interfaces vxlan ' + tmp + ' link ' + intf): - raise ConfigError('can not enslave interface {} which belongs to ' \ - 'vxlan {}'.format(intf, tmp)) - - - if bond['primary']: - if bond['primary'] not in bond['member']: - raise ConfigError('primary interface must be a member interface of {}' \ - .format(bond['intf'])) - - if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']: - raise ConfigError('primary interface only works for mode active-backup, ' \ - 'transmit-load-balance or adaptive-load-balance') - - if bond['arp_mon_intvl'] > 0: - if bond['mode'] in ['802.3ad', 'balance-tlb', 'balance-alb']: - raise ConfigError('ARP link monitoring does not work for mode 802.3ad, ' \ - 'transmit-load-balance or adaptive-load-balance') - - return None - - -def generate(bond): - return None - - -def apply(bond): - b = BondIf(bond['intf']) - - if bond['deleted']: - # delete interface - b.remove() - else: - # Some parameters can not be changed when the bond is up. - # Always disable the bond prior changing anything - b.set_state('down') - - # The bonding mode can not be changed when there are interfaces enslaved - # to this bond, thus we will free all interfaces from the bond first! - for intf in b.get_slaves(): - b.del_port(intf) - - # ARP link monitoring frequency, reset miimon when arp-montior is inactive - # this is done inside BondIf automatically - b.set_arp_interval(bond['arp_mon_intvl']) - - # 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 adresses prior adding new ones, this will remove addresses - # added manually 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, b.get_arp_ip_target().split())) - for addr in arp_tgt_addr: - b.set_arp_ip_target('-' + addr) - - # Add configured ARP target addresses - for addr in bond['arp_mon_tgt']: - b.set_arp_ip_target('+' + addr) - - # update interface description used e.g. within SNMP - b.set_alias(bond['description']) - - # get DHCP config dictionary and update values - opt = b.get_dhcp_options() - - if bond['dhcp_client_id']: - opt['client_id'] = bond['dhcp_client_id'] - - if bond['dhcp_hostname']: - opt['hostname'] = bond['dhcp_hostname'] - - if bond['dhcp_vendor_class_id']: - opt['vendor_class_id'] = bond['dhcp_vendor_class_id'] - - # store DHCP config dictionary - used later on when addresses are aquired - b.set_dhcp_options(opt) - - # get DHCPv6 config dictionary and update values - opt = b.get_dhcpv6_options() - - if bond['dhcpv6_prm_only']: - opt['dhcpv6_prm_only'] = True - - if bond['dhcpv6_temporary']: - opt['dhcpv6_temporary'] = True - - # store DHCPv6 config dictionary - used later on when addresses are aquired - b.set_dhcpv6_options(opt) - - # ignore link state changes - b.set_link_detect(bond['disable_link_detect']) - # Bonding transmit hash policy - b.set_hash_policy(bond['hash_policy']) - # configure ARP cache timeout in milliseconds - b.set_arp_cache_tmo(bond['ip_arp_cache_tmo']) - # Enable proxy-arp on this interface - b.set_proxy_arp(bond['ip_proxy_arp']) - # Enable private VLAN proxy ARP on this interface - b.set_proxy_arp_pvlan(bond['ip_proxy_arp_pvlan']) - - # Change interface MAC address - if bond['mac']: - b.set_mac(bond['mac']) - - # Bonding policy - b.set_mode(bond['mode']) - # Maximum Transmission Unit (MTU) - b.set_mtu(bond['mtu']) - - # Primary device interface - if bond['primary']: - b.set_primary(bond['primary']) - - # Add (enslave) interfaces to bond - for intf in bond['member']: - b.add_port(intf) - - # As the bond interface is always disabled first when changing - # parameters we will only re-enable the interface if it is not - # administratively disabled - if not bond['disable']: - b.set_state('up') - - # Configure interface address(es) - # - not longer required addresses get removed first - # - newly addresses will be added second - for addr in bond['address_remove']: - b.del_addr(addr) - for addr in bond['address']: - b.add_addr(addr) - - # remove no longer required service VLAN interfaces (vif-s) - for vif_s in bond['vif_s_remove']: - b.del_vlan(vif_s) - - # create service VLAN interfaces (vif-s) - for vif_s in bond['vif_s']: - s_vlan = b.add_vlan(vif_s['id'], ethertype=vif_s['ethertype']) - apply_vlan_config(s_vlan, vif_s) - - # remove no longer required client VLAN interfaces (vif-c) - # on lower service VLAN interface - for vif_c in vif_s['vif_c_remove']: - s_vlan.del_vlan(vif_c) - - # create client VLAN interfaces (vif-c) - # on lower service VLAN interface - for vif_c in vif_s['vif_c']: - c_vlan = s_vlan.add_vlan(vif_c['id']) - apply_vlan_config(c_vlan, vif_c) - - # remove no longer required VLAN interfaces (vif) - for vif in bond['vif_remove']: - b.del_vlan(vif) - - # create VLAN interfaces (vif) - for vif in bond['vif']: - vlan = b.add_vlan(vif['id']) - apply_vlan_config(vlan, vif) - - return None - -if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - exit(1) diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py deleted file mode 100755 index 70bf4f528..000000000 --- a/src/conf_mode/interface-bridge.py +++ /dev/null @@ -1,307 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2019 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import os - -from copy import deepcopy -from sys import exit -from netifaces import interfaces - -from vyos.ifconfig import BridgeIf, STPIf -from vyos.configdict import list_diff -from vyos.config import Config -from vyos import ConfigError - -default_config_data = { - 'address': [], - 'address_remove': [], - 'aging': 300, - 'arp_cache_tmo': 30, - 'description': '', - 'deleted': False, - 'dhcp_client_id': '', - 'dhcp_hostname': '', - 'dhcp_vendor_class_id': '', - 'dhcpv6_prm_only': False, - 'dhcpv6_temporary': False, - 'disable': False, - 'disable_link_detect': 1, - 'forwarding_delay': 14, - 'hello_time': 2, - 'igmp_querier': 0, - 'intf': '', - 'mac' : '', - 'max_age': 20, - 'member': [], - 'member_remove': [], - 'priority': 32768, - 'stp': 0 -} - -def get_config(): - bridge = deepcopy(default_config_data) - conf = Config() - - # determine tagNode instance - try: - bridge['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - except KeyError as E: - print("Interface not specified") - - # Check if bridge has been removed - if not conf.exists('interfaces bridge ' + bridge['intf']): - bridge['deleted'] = True - return bridge - - # set new configuration level - conf.set_level('interfaces bridge ' + bridge['intf']) - - # retrieve configured interface addresses - if conf.exists('address'): - bridge['address'] = conf.return_values('address') - - # Determine interface addresses (currently effective) - to determine which - # address is no longer valid and needs to be removed - eff_addr = conf.return_effective_values('address') - bridge['address_remove'] = list_diff(eff_addr, bridge['address']) - - # retrieve aging - how long addresses are retained - if conf.exists('aging'): - bridge['aging'] = int(conf.return_value('aging')) - - # retrieve interface description - if conf.exists('description'): - bridge['description'] = conf.return_value('description') - - # get DHCP client identifier - if conf.exists('dhcp-options client-id'): - bridge['dhcp_client_id'] = conf.return_value('dhcp-options client-id') - - # DHCP client host name (overrides the system host name) - if conf.exists('dhcp-options host-name'): - bridge['dhcp_hostname'] = conf.return_value('dhcp-options host-name') - - # DHCP client vendor identifier - if conf.exists('dhcp-options vendor-class-id'): - bridge['dhcp_vendor_class_id'] = conf.return_value('dhcp-options vendor-class-id') - - # DHCPv6 only acquire config parameters, no address - if conf.exists('dhcpv6-options parameters-only'): - bridge['dhcpv6_prm_only'] = True - - # DHCPv6 temporary IPv6 address - if conf.exists('dhcpv6-options temporary'): - bridge['dhcpv6_temporary'] = True - - # Disable this bridge interface - if conf.exists('disable'): - bridge['disable'] = True - - # Ignore link state changes - if conf.exists('disable-link-detect'): - bridge['disable_link_detect'] = 2 - - # Forwarding delay - if conf.exists('forwarding-delay'): - bridge['forwarding_delay'] = int(conf.return_value('forwarding-delay')) - - # Hello packet advertisment interval - if conf.exists('hello-time'): - bridge['hello_time'] = int(conf.return_value('hello-time')) - - # Enable Internet Group Management Protocol (IGMP) querier - if conf.exists('igmp querier'): - bridge['igmp_querier'] = 1 - - # ARP cache entry timeout in seconds - if conf.exists('ip arp-cache-timeout'): - bridge['arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) - - # Media Access Control (MAC) address - if conf.exists('mac'): - bridge['mac'] = conf.return_value('mac') - - # Interval at which neighbor bridges are removed - if conf.exists('max-age'): - bridge['max_age'] = int(conf.return_value('max-age')) - - # Determine bridge member interface (currently configured) - for intf in conf.list_nodes('member interface'): - # cost and priority initialized with linux defaults - # by reading /sys/devices/virtual/net/br0/brif/eth2/{path_cost,priority} - # after adding interface to bridge after reboot - iface = { - 'name': intf, - 'cost': 100, - 'priority': 32 - } - - if conf.exists('member interface {} cost'.format(intf)): - iface['cost'] = int(conf.return_value('member interface {} cost'.format(intf))) - - if conf.exists('member interface {} priority'.format(intf)): - iface['priority'] = int(conf.return_value('member interface {} priority'.format(intf))) - - bridge['member'].append(iface) - - # Determine bridge member interface (currently effective) - to determine which - # interfaces is no longer assigend to the bridge and thus can be removed - eff_intf = conf.list_effective_nodes('member interface') - act_intf = conf.list_nodes('member interface') - bridge['member_remove'] = list_diff(eff_intf, act_intf) - - # Priority for this bridge - if conf.exists('priority'): - bridge['priority'] = int(conf.return_value('priority')) - - # Enable spanning tree protocol - if conf.exists('stp'): - bridge['stp'] = 1 - - return bridge - -def verify(bridge): - if bridge['dhcpv6_prm_only'] and bridge['dhcpv6_temporary']: - raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - - conf = Config() - for br in conf.list_nodes('interfaces bridge'): - # it makes no sense to verify ourself in this case - if br == bridge['intf']: - continue - - for intf in bridge['member']: - tmp = conf.list_nodes('interfaces bridge {} member interface'.format(br)) - if intf['name'] in tmp: - raise ConfigError('Interface "{}" belongs to bridge "{}" and can not be enslaved.'.format(intf['name'], bridge['intf'])) - - # the interface must exist prior adding it to a bridge - for intf in bridge['member']: - if intf['name'] not in interfaces(): - raise ConfigError('Can not add non existing interface "{}" to bridge "{}"'.format(intf['name'], bridge['intf'])) - - # bridge members are not allowed to be bond members, too - for intf in bridge['member']: - for bond in conf.list_nodes('interfaces bonding'): - if conf.exists('interfaces bonding ' + bond + ' member interface'): - if intf['name'] in conf.return_values('interfaces bonding ' + bond + ' member interface'): - raise ConfigError('Interface {} belongs to bond {}, can not add it to {}'.format(intf['name'], bond, bridge['intf'])) - - return None - -def generate(bridge): - return None - -def apply(bridge): - br = BridgeIf(bridge['intf']) - - if bridge['deleted']: - # delete interface - br.remove() - else: - # enable interface - br.set_state('up') - # set ageing time - br.set_ageing_time(bridge['aging']) - # set bridge forward delay - br.set_forward_delay(bridge['forwarding_delay']) - # set hello time - br.set_hello_time(bridge['hello_time']) - # set max message age - br.set_max_age(bridge['max_age']) - # set bridge priority - br.set_priority(bridge['priority']) - # turn stp on/off - br.set_stp(bridge['stp']) - # enable or disable IGMP querier - br.set_multicast_querier(bridge['igmp_querier']) - # update interface description used e.g. within SNMP - br.set_alias(bridge['description']) - - # get DHCP config dictionary and update values - opt = br.get_dhcp_options() - - if bridge['dhcp_client_id']: - opt['client_id'] = bridge['dhcp_client_id'] - - if bridge['dhcp_hostname']: - opt['hostname'] = bridge['dhcp_hostname'] - - if bridge['dhcp_vendor_class_id']: - opt['vendor_class_id'] = bridge['dhcp_vendor_class_id'] - - # store DHCPv6 config dictionary - used later on when addresses are aquired - br.set_dhcp_options(opt) - - # get DHCPv6 config dictionary and update values - opt = br.get_dhcpv6_options() - - if bridge['dhcpv6_prm_only']: - opt['dhcpv6_prm_only'] = True - - if bridge['dhcpv6_temporary']: - opt['dhcpv6_temporary'] = True - - # store DHCPv6 config dictionary - used later on when addresses are aquired - br.set_dhcpv6_options(opt) - - # Change interface MAC address - if bridge['mac']: - br.set_mac(bridge['mac']) - - # remove interface from bridge - for intf in bridge['member_remove']: - br.del_port( intf['name'] ) - - # add interfaces to bridge - for member in bridge['member']: - br.add_port(member['name']) - - # up/down interface - if bridge['disable']: - br.set_state('down') - - # Configure interface address(es) - # - not longer required addresses get removed first - # - newly addresses will be added second - for addr in bridge['address_remove']: - br.del_addr(addr) - for addr in bridge['address']: - br.add_addr(addr) - - # configure additional bridge member options - for member in bridge['member']: - i = STPIf(member['name']) - # configure ARP cache timeout - i.set_arp_cache_tmo(bridge['arp_cache_tmo']) - # ignore link state changes - i.set_link_detect(bridge['disable_link_detect']) - # set bridge port path cost - i.set_path_cost(member['cost']) - # set bridge port path priority - i.set_path_priority(member['priority']) - - return None - -if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - exit(1) diff --git a/src/conf_mode/interface-dummy.py b/src/conf_mode/interface-dummy.py deleted file mode 100755 index eb0145f65..000000000 --- a/src/conf_mode/interface-dummy.py +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2019 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import os - -from copy import deepcopy -from sys import exit - -from vyos.ifconfig import DummyIf -from vyos.configdict import list_diff -from vyos.config import Config -from vyos import ConfigError - -default_config_data = { - 'address': [], - 'address_remove': [], - 'deleted': False, - 'description': '', - 'disable': False, - 'intf': '' -} - -def get_config(): - dummy = deepcopy(default_config_data) - conf = Config() - - # determine tagNode instance - try: - dummy['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - except KeyError as E: - print("Interface not specified") - - # Check if interface has been removed - if not conf.exists('interfaces dummy ' + dummy['intf']): - dummy['deleted'] = True - return dummy - - # set new configuration level - conf.set_level('interfaces dummy ' + dummy['intf']) - - # retrieve configured interface addresses - if conf.exists('address'): - dummy['address'] = conf.return_values('address') - - # retrieve interface description - if conf.exists('description'): - dummy['description'] = conf.return_value('description') - - # Disable this interface - if conf.exists('disable'): - dummy['disable'] = True - - # Determine interface addresses (currently effective) - to determine which - # address is no longer valid and needs to be removed from the interface - eff_addr = conf.return_effective_values('address') - act_addr = conf.return_values('address') - dummy['address_remove'] = list_diff(eff_addr, act_addr) - - return dummy - -def verify(dummy): - return None - -def generate(dummy): - return None - -def apply(dummy): - d = DummyIf(dummy['intf']) - - # Remove dummy interface - if dummy['deleted']: - d.remove() - else: - # update interface description used e.g. within SNMP - d.set_alias(dummy['description']) - - # Configure interface address(es) - # - not longer required addresses get removed first - # - newly addresses will be added second - for addr in dummy['address_remove']: - d.del_addr(addr) - for addr in dummy['address']: - d.add_addr(addr) - - # disable interface on demand - if dummy['disable']: - d.set_state('down') - else: - d.set_state('up') - - return None - -if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - exit(1) diff --git a/src/conf_mode/interface-ethernet.py b/src/conf_mode/interface-ethernet.py deleted file mode 100755 index cd40aff3e..000000000 --- a/src/conf_mode/interface-ethernet.py +++ /dev/null @@ -1,452 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2019 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import os - -from copy import deepcopy -from sys import exit - -from vyos.ifconfig import EthernetIf, VLANIf -from vyos.configdict import list_diff, vlan_to_dict -from vyos.config import Config -from vyos import ConfigError - -default_config_data = { - 'address': [], - 'address_remove': [], - 'description': '', - 'deleted': False, - 'dhcp_client_id': '', - 'dhcp_hostname': '', - 'dhcp_vendor_class_id': '', - 'dhcpv6_prm_only': False, - 'dhcpv6_temporary': False, - 'disable': False, - 'disable_link_detect': 1, - 'duplex': 'auto', - 'flow_control': 'on', - 'hw_id': '', - 'ip_arp_cache_tmo': 30, - 'ip_proxy_arp': 0, - 'ip_proxy_arp_pvlan': 0, - 'intf': '', - 'mac': '', - 'mtu': 1500, - 'offload_gro': 'off', - 'offload_gso': 'off', - 'offload_sg': 'off', - 'offload_tso': 'off', - 'offload_ufo': 'off', - 'speed': 'auto', - 'vif_s': [], - 'vif_s_remove': [], - 'vif': [], - 'vif_remove': [] -} - - -def apply_vlan_config(vlan, config): - """ - Generic function to apply a VLAN configuration from a dictionary - to a VLAN interface - """ - - if type(vlan) != type(VLANIf("lo")): - raise TypeError() - - # get DHCP config dictionary and update values - opt = vlan.get_dhcp_options() - - if config['dhcp_client_id']: - opt['client_id'] = config['dhcp_client_id'] - - if config['dhcp_hostname']: - opt['hostname'] = config['dhcp_hostname'] - - if config['dhcp_vendor_class_id']: - opt['vendor_class_id'] = config['dhcp_vendor_class_id'] - - # store DHCP config dictionary - used later on when addresses are aquired - vlan.set_dhcp_options(opt) - - # get DHCPv6 config dictionary and update values - opt = vlan.get_dhcpv6_options() - - if config['dhcpv6_prm_only']: - opt['dhcpv6_prm_only'] = True - - if config['dhcpv6_temporary']: - opt['dhcpv6_temporary'] = True - - # store DHCPv6 config dictionary - used later on when addresses are aquired - vlan.set_dhcpv6_options(opt) - - # update interface description used e.g. within SNMP - vlan.set_alias(config['description']) - # ignore link state changes - vlan.set_link_detect(config['disable_link_detect']) - # Maximum Transmission Unit (MTU) - vlan.set_mtu(config['mtu']) - # Change VLAN interface MAC address - if config['mac']: - vlan.set_mac(config['mac']) - - # enable/disable VLAN interface - if config['disable']: - vlan.set_state('down') - else: - vlan.set_state('up') - - # Configure interface address(es) - # - not longer required addresses get removed first - # - newly addresses will be added second - for addr in config['address_remove']: - vlan.del_addr(addr) - for addr in config['address']: - vlan.add_addr(addr) - - -def get_config(): - eth = deepcopy(default_config_data) - conf = Config() - - # determine tagNode instance - try: - eth['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - except KeyError as E: - print("Interface not specified") - - # check if ethernet interface has been removed - cfg_base = 'interfaces ethernet ' + eth['intf'] - if not conf.exists(cfg_base): - eth['deleted'] = True - # we can not bail out early as ethernet interface can not be removed - # Kernel will complain with: RTNETLINK answers: Operation not supported. - # Thus we need to remove individual settings - return eth - - # set new configuration level - conf.set_level(cfg_base) - - # retrieve configured interface addresses - if conf.exists('address'): - eth['address'] = conf.return_values('address') - - # get interface addresses (currently effective) - to determine which - # address is no longer valid and needs to be removed - eff_addr = conf.return_effective_values('address') - eth['address_remove'] = list_diff(eff_addr, eth['address']) - - # retrieve interface description - if conf.exists('description'): - eth['description'] = conf.return_value('description') - - # get DHCP client identifier - if conf.exists('dhcp-options client-id'): - eth['dhcp_client_id'] = conf.return_value('dhcp-options client-id') - - # DHCP client host name (overrides the system host name) - if conf.exists('dhcp-options host-name'): - eth['dhcp_hostname'] = conf.return_value('dhcp-options host-name') - - # DHCP client vendor identifier - if conf.exists('dhcp-options vendor-class-id'): - eth['dhcp_vendor_class_id'] = conf.return_value('dhcp-options vendor-class-id') - - # DHCPv6 only acquire config parameters, no address - if conf.exists('dhcpv6-options parameters-only'): - eth['dhcpv6_prm_only'] = True - - # DHCPv6 temporary IPv6 address - if conf.exists('dhcpv6-options temporary'): - eth['dhcpv6_temporary'] = True - - # ignore link state changes - if conf.exists('disable-link-detect'): - eth['disable_link_detect'] = 2 - - # disable ethernet flow control (pause frames) - if conf.exists('disable-flow-control'): - eth['flow_control'] = 'off' - - # retrieve real hardware address - if conf.exists('hw-id'): - eth['hw_id'] = conf.return_value('hw-id') - - # disable interface - if conf.exists('disable'): - eth['disable'] = True - - # interface duplex - if conf.exists('duplex'): - eth['duplex'] = conf.return_value('duplex') - - # ARP cache entry timeout in seconds - if conf.exists('ip arp-cache-timeout'): - eth['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) - - # Enable proxy-arp on this interface - if conf.exists('ip enable-proxy-arp'): - eth['ip_proxy_arp'] = 1 - - # Enable private VLAN proxy ARP on this interface - if conf.exists('ip proxy-arp-pvlan'): - eth['ip_proxy_arp_pvlan'] = 1 - - # Media Access Control (MAC) address - if conf.exists('mac'): - eth['mac'] = conf.return_value('mac') - - # Maximum Transmission Unit (MTU) - if conf.exists('mtu'): - eth['mtu'] = int(conf.return_value('mtu')) - - # GRO (generic receive offload) - if conf.exists('offload-options generic-receive'): - eth['offload_gro'] = conf.return_value('offload-options generic-receive') - - # GSO (generic segmentation offload) - if conf.exists('offload-options generic-segmentation'): - eth['offload_gso'] = conf.return_value('offload-options generic-segmentation') - - # scatter-gather option - if conf.exists('offload-options scatter-gather'): - eth['offload_sg'] = conf.return_value('offload-options scatter-gather') - - # TSO (TCP segmentation offloading) - if conf.exists('offload-options tcp-segmentation'): - eth['offload_tso'] = conf.return_value('offload-options tcp-segmentation') - - # UDP fragmentation offloading - if conf.exists('offload-options udp-fragmentation'): - eth['offload_ufo'] = conf.return_value('offload-options udp-fragmentation') - - # interface speed - if conf.exists('speed'): - eth['speed'] = conf.return_value('speed') - - # re-set configuration level to parse new nodes - conf.set_level(cfg_base) - # get vif-s interfaces (currently effective) - to determine which vif-s - # interface is no longer present and needs to be removed - eff_intf = conf.list_effective_nodes('vif-s') - act_intf = conf.list_nodes('vif-s') - eth['vif_s_remove'] = list_diff(eff_intf, act_intf) - - if conf.exists('vif-s'): - for vif_s in conf.list_nodes('vif-s'): - # set config level to vif-s interface - conf.set_level(cfg_base + ' vif-s ' + vif_s) - eth['vif_s'].append(vlan_to_dict(conf)) - - # re-set configuration level to parse new nodes - conf.set_level(cfg_base) - # Determine vif interfaces (currently effective) - to determine which - # vif interface is no longer present and needs to be removed - eff_intf = conf.list_effective_nodes('vif') - act_intf = conf.list_nodes('vif') - eth['vif_remove'] = list_diff(eff_intf, act_intf) - - if conf.exists('vif'): - for vif in conf.list_nodes('vif'): - # set config level to vif interface - conf.set_level(cfg_base + ' vif ' + vif) - eth['vif'].append(vlan_to_dict(conf)) - - return eth - - -def verify(eth): - if eth['deleted']: - return None - - if eth['speed'] == 'auto': - if eth['duplex'] != 'auto': - raise ConfigError('If speed is hardcoded, duplex must be hardcoded, too') - - if eth['duplex'] == 'auto': - if eth['speed'] != 'auto': - raise ConfigError('If duplex is hardcoded, speed must be hardcoded, too') - - if eth['dhcpv6_prm_only'] and eth['dhcpv6_temporary']: - raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - - conf = Config() - # some options can not be changed when interface is enslaved to a bond - for bond in conf.list_nodes('interfaces bonding'): - if conf.exists('interfaces bonding ' + bond + ' member interface'): - bond_member = conf.return_values('interfaces bonding ' + bond + ' member interface') - if eth['intf'] in bond_member: - if eth['address']: - raise ConfigError('Can not assign address to interface {} which is a member of {}').format(eth['intf'], bond) - - # DHCPv6 parameters-only and temporary address are mutually exclusive - for vif_s in eth['vif_s']: - if vif_s['dhcpv6_prm_only'] and vif_s['dhcpv6_temporary']: - raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - - for vif_c in vif_s['vif_c']: - if vif_c['dhcpv6_prm_only'] and vif_c['dhcpv6_temporary']: - raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - - for vif in eth['vif']: - if vif['dhcpv6_prm_only'] and vif['dhcpv6_temporary']: - raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - - return None - -def generate(eth): - return None - -def apply(eth): - e = EthernetIf(eth['intf']) - if eth['deleted']: - # delete interface - e.remove() - else: - # update interface description used e.g. within SNMP - e.set_alias(eth['description']) - - # get DHCP config dictionary and update values - opt = e.get_dhcp_options() - - if eth['dhcp_client_id']: - opt['client_id'] = eth['dhcp_client_id'] - - if eth['dhcp_hostname']: - opt['hostname'] = eth['dhcp_hostname'] - - if eth['dhcp_vendor_class_id']: - opt['vendor_class_id'] = eth['dhcp_vendor_class_id'] - - # store DHCP config dictionary - used later on when addresses are aquired - e.set_dhcp_options(opt) - - # get DHCPv6 config dictionary and update values - opt = e.get_dhcpv6_options() - - if eth['dhcpv6_prm_only']: - opt['dhcpv6_prm_only'] = True - - if eth['dhcpv6_temporary']: - opt['dhcpv6_temporary'] = True - - # store DHCPv6 config dictionary - used later on when addresses are aquired - e.set_dhcpv6_options(opt) - - # ignore link state changes - e.set_link_detect(eth['disable_link_detect']) - # disable ethernet flow control (pause frames) - e.set_flow_control(eth['flow_control']) - # configure ARP cache timeout in milliseconds - e.set_arp_cache_tmo(eth['ip_arp_cache_tmo']) - # Enable proxy-arp on this interface - e.set_proxy_arp(eth['ip_proxy_arp']) - # Enable private VLAN proxy ARP on this interface - e.set_proxy_arp_pvlan(eth['ip_proxy_arp_pvlan']) - - # Change interface MAC address - re-set to real hardware address (hw-id) - # if custom mac is removed - if eth['mac']: - e.set_mac(eth['mac']) - else: - e.set_mac(eth['hw_id']) - - # Maximum Transmission Unit (MTU) - e.set_mtu(eth['mtu']) - - # GRO (generic receive offload) - e.set_gro(eth['offload_gro']) - - # GSO (generic segmentation offload) - e.set_gso(eth['offload_gso']) - - # scatter-gather option - e.set_sg(eth['offload_sg']) - - # TSO (TCP segmentation offloading) - e.set_tso(eth['offload_tso']) - - # UDP fragmentation offloading - e.set_ufo(eth['offload_ufo']) - - # Set physical interface speed and duplex - e.set_speed_duplex(eth['speed'], eth['duplex']) - - # Enable/Disable interface - if eth['disable']: - e.set_state('down') - else: - e.set_state('up') - - # Configure interface address(es) - # - not longer required addresses get removed first - # - newly addresses will be added second - for addr in eth['address_remove']: - e.del_addr(addr) - for addr in eth['address']: - e.add_addr(addr) - - # remove no longer required service VLAN interfaces (vif-s) - for vif_s in eth['vif_s_remove']: - e.del_vlan(vif_s) - - # create service VLAN interfaces (vif-s) - for vif_s in eth['vif_s']: - s_vlan = e.add_vlan(vif_s['id'], ethertype=vif_s['ethertype']) - apply_vlan_config(s_vlan, vif_s) - - # remove no longer required client VLAN interfaces (vif-c) - # on lower service VLAN interface - for vif_c in vif_s['vif_c_remove']: - s_vlan.del_vlan(vif_c) - - # create client VLAN interfaces (vif-c) - # on lower service VLAN interface - for vif_c in vif_s['vif_c']: - c_vlan = s_vlan.add_vlan(vif_c['id']) - apply_vlan_config(c_vlan, vif_c) - - # remove no longer required VLAN interfaces (vif) - for vif in eth['vif_remove']: - e.del_vlan(vif) - - # create VLAN interfaces (vif) - for vif in eth['vif']: - # QoS priority mapping can only be set during interface creation - # so we delete the interface first if required. - if vif['egress_qos_changed'] or vif['ingress_qos_changed']: - try: - # on system bootup the above condition is true but the interface - # does not exists, which throws an exception, but that's legal - e.del_vlan(vif['id']) - except: - pass - - vlan = e.add_vlan(vif['id'], ingress_qos=vif['ingress_qos'], egress_qos=vif['egress_qos']) - apply_vlan_config(vlan, vif) - - return None - -if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - exit(1) diff --git a/src/conf_mode/interface-loopback.py b/src/conf_mode/interface-loopback.py deleted file mode 100755 index 10722d137..000000000 --- a/src/conf_mode/interface-loopback.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2019 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import os - -from sys import exit -from copy import deepcopy - -from vyos.ifconfig import LoopbackIf -from vyos.configdict import list_diff -from vyos.config import Config -from vyos import ConfigError - -default_config_data = { - 'address': [], - 'address_remove': [], - 'deleted': False, - 'description': '', -} - - -def get_config(): - loopback = deepcopy(default_config_data) - conf = Config() - - # determine tagNode instance - try: - loopback['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - except KeyError as E: - print("Interface not specified") - - # Check if interface has been removed - if not conf.exists('interfaces loopback ' + loopback['intf']): - loopback['deleted'] = True - - # set new configuration level - conf.set_level('interfaces loopback ' + loopback['intf']) - - # retrieve configured interface addresses - if conf.exists('address'): - loopback['address'] = conf.return_values('address') - - # retrieve interface description - if conf.exists('description'): - loopback['description'] = conf.return_value('description') - - # Determine interface addresses (currently effective) - to determine which - # address is no longer valid and needs to be removed from the interface - eff_addr = conf.return_effective_values('address') - act_addr = conf.return_values('address') - loopback['address_remove'] = list_diff(eff_addr, act_addr) - - return loopback - -def verify(loopback): - return None - -def generate(loopback): - return None - -def apply(loopback): - l = LoopbackIf(loopback['intf']) - if loopback['deleted']: - l.remove() - else: - # update interface description used e.g. within SNMP - l.set_alias(loopback['description']) - - # Configure interface address(es) - # - not longer required addresses get removed first - # - newly addresses will be added second - for addr in loopback['address_remove']: - l.del_addr(addr) - for addr in loopback['address']: - l.add_addr(addr) - - return None - -if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - exit(1) diff --git a/src/conf_mode/interface-openvpn.py b/src/conf_mode/interface-openvpn.py deleted file mode 100755 index 5345bf7a2..000000000 --- a/src/conf_mode/interface-openvpn.py +++ /dev/null @@ -1,960 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2019 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import os -import re -import sys -import stat -import jinja2 - -from copy import deepcopy -from grp import getgrnam -from ipaddress import ip_address,ip_network,IPv4Interface -from netifaces import interfaces -from psutil import pid_exists -from pwd import getpwnam -from subprocess import Popen, PIPE -from time import sleep - -from vyos import ConfigError -from vyos.config import Config -from vyos.ifconfig import Interface -from vyos.validate import is_addr_assigned - -user = 'openvpn' -group = 'openvpn' - -# Please be careful if you edit the template. -config_tmpl = """ -### Autogenerated by interfaces-openvpn.py ### -# -# See https://community.openvpn.net/openvpn/wiki/Openvpn24ManPage -# for individual keyword definition - -{% if description %} -# {{ description }} -{% endif %} - -verb 3 -status /opt/vyatta/etc/openvpn/status/{{ intf }}.status 30 -writepid /var/run/openvpn/{{ intf }}.pid -daemon openvpn-{{ intf }} - -dev-type {{ type }} -dev {{ intf }} -user {{ uid }} -group {{ gid }} -persist-key -iproute /usr/libexec/vyos/system/unpriv-ip - -proto {% if 'tcp-active' in protocol -%}tcp-client{% elif 'tcp-passive' in protocol -%}tcp-server{% else %}udp{% endif %} - -{%- if local_host %} -local {{ local_host }} -{% endif %} - -{%- if local_port %} -lport {{ local_port }} -{% endif %} - -{%- if remote_port %} -rport {{ remote_port }} -{% endif %} - -{%- if remote_host %} -{% for remote in remote_host -%} -remote {{ remote }} -{% endfor -%} -{% endif %} - -{%- if shared_secret_file %} -secret {{ shared_secret_file }} -{% endif %} - -{%- if persistent_tunnel %} -persist-tun -{% endif %} - -{%- if mode %} -{%- if 'client' in mode %} -# -# OpenVPN Client mode -# -client -nobind -{%- elif 'server' in mode %} -# -# OpenVPN Server mode -# -mode server -tls-server -keepalive {{ ping_interval }} {{ ping_restart }} -management /tmp/openvpn-mgmt-intf unix - -{%- if server_topology %} -topology {% if 'point-to-point' in server_topology %}p2p{% else %}subnet{% endif %} -{% endif %} - -{% for ns in server_dns_nameserver -%} -push "dhcp-option DNS {{ ns }}" -{% endfor -%} - -{% for route in server_push_route -%} -push "route {{ route }}" -{% endfor -%} - -{%- if server_domain %} -push "dhcp-option DOMAIN {{ server_domain }}" -{% endif %} - -{%- if server_max_conn %} -max-clients {{ server_max_conn }} -{% endif %} - -{%- if bridge_member %} -server-bridge nogw -{%- else %} -server {{ server_subnet }} -{% endif %} - -{%- if server_reject_unconfigured %} -ccd-exclusive -{% endif %} - -{%- else %} -# -# OpenVPN site-2-site mode -# -ping {{ ping_interval }} -ping-restart {{ ping_restart }} - -{%- if local_address_subnet %} -ifconfig {{ local_address }} {{ local_address_subnet }} -{% elif remote_address %} -ifconfig {{ local_address }} {{ remote_address }} -{% endif %} - -{% endif %} -{% endif %} - -{%- if tls_ca_cert %} -ca {{ tls_ca_cert }} -{% endif %} - -{%- if tls_cert %} -cert {{ tls_cert }} -{% endif %} - -{%- if tls_key %} -key {{ tls_key }} -{% endif %} - -{%- if tls_crl %} -crl-verify {{ tls_crl }} -{% endif %} - -{%- if tls_version_min %} -tls-version-min {{tls_version_min}} -{% endif %} - -{%- if tls_dh %} -dh {{ tls_dh }} -{% endif %} - -{%- if tls_auth %} -tls-auth {{tls_auth}} -{% endif %} - -{%- if 'active' in tls_role %} -tls-client -{%- elif 'passive' in tls_role %} -tls-server -{% endif %} - -{%- if redirect_gateway %} -push "redirect-gateway {{ redirect_gateway }}" -{% endif %} - -{%- if compress_lzo %} -compress lzo -{% endif %} - -{%- if hash %} -auth {{ hash }} -{% endif %} - -{%- if encryption %} -{%- if 'des' in encryption %} -cipher des-cbc -{%- elif '3des' in encryption %} -cipher des-ede3-cbc -{%- elif 'bf128' in encryption %} -cipher bf-cbc -keysize 128 -{%- elif 'bf256' in encryption %} -cipher bf-cbc -keysize 25 -{%- elif 'aes128gcm' in encryption %} -cipher aes-128-gcm -{%- elif 'aes128' in encryption %} -cipher aes-128-cbc -{%- elif 'aes192gcm' in encryption %} -cipher aes-192-gcm -{%- elif 'aes192' in encryption %} -cipher aes-192-cbc -{%- elif 'aes256gcm' in encryption %} -cipher aes-256-gcm -{%- elif 'aes256' in encryption %} -cipher aes-256-cbc -{% endif %} -{% endif %} - -{%- if auth %} -auth-user-pass /tmp/openvpn-{{ intf }}-pw -auth-retry nointeract -{% endif %} - -{%- if client %} -client-config-dir /opt/vyatta/etc/openvpn/ccd/{{ intf }} -{% endif %} - -# DEPRECATED This option will be removed in OpenVPN 2.5 -# Until OpenVPN v2.3 the format of the X.509 Subject fields was formatted like this: -# /C=US/L=Somewhere/CN=John Doe/emailAddress=john@example.com In addition the old -# behaviour was to remap any character other than alphanumeric, underscore ('_'), -# dash ('-'), dot ('.'), and slash ('/') to underscore ('_'). The X.509 Subject -# string as returned by the tls_id environmental variable, could additionally -# contain colon (':') or equal ('='). When using the --compat-names option, this -# old formatting and remapping will be re-enabled again. This is purely implemented -# for compatibility reasons when using older plug-ins or scripts which does not -# handle the new formatting or UTF-8 characters. -# -# See https://phabricator.vyos.net/T1512 -compat-names - -{% for option in options -%} -{{ option }} -{% endfor -%} -""" - -client_tmpl = """ -### Autogenerated by interfaces-openvpn.py ### - -ifconfig-push {{ ip }} {{ remote_netmask }} -{% for route in push_route -%} -push "route {{ route }}" -{% endfor -%} - -{% for net in subnet -%} -iroute {{ net }} -{% endfor -%} - -{%- if disable %} -disable -{% endif %} -""" - -default_config_data = { - 'address': [], - 'auth_user': '', - 'auth_pass': '', - 'auth': False, - 'bridge_member': [], - 'compress_lzo': False, - 'deleted': False, - 'description': '', - 'disable': False, - 'encryption': '', - 'hash': '', - 'intf': '', - 'ping_restart': '60', - 'ping_interval': '10', - 'local_address': '', - 'local_address_subnet': '', - 'local_host': '', - 'local_port': '', - 'mode': '', - 'options': [], - 'persistent_tunnel': False, - 'protocol': '', - 'redirect_gateway': '', - 'remote_address': '', - 'remote_host': [], - 'remote_port': '', - 'client': [], - 'server_domain': '', - 'server_max_conn': '', - 'server_dns_nameserver': [], - 'server_push_route': [], - 'server_reject_unconfigured': False, - 'server_subnet': '', - 'server_topology': '', - 'shared_secret_file': '', - 'tls': False, - 'tls_auth': '', - 'tls_ca_cert': '', - 'tls_cert': '', - 'tls_crl': '', - 'tls_dh': '', - 'tls_key': '', - 'tls_role': '', - 'tls_version_min': '', - 'type': 'tun', - 'uid': user, - 'gid': group, -} - -def subprocess_cmd(command): - p = Popen(command, stdout=PIPE, shell=True) - p.communicate() - -def get_config_name(intf): - cfg_file = r'/opt/vyatta/etc/openvpn/openvpn-{}.conf'.format(intf) - return cfg_file - -def openvpn_mkdir(directory): - # create directory on demand - if not os.path.exists(directory): - os.mkdir(directory) - - # fix permissions - corresponds to mode 755 - os.chmod(directory, stat.S_IRWXU|stat.S_IRGRP|stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH) - uid = getpwnam(user).pw_uid - gid = getgrnam(group).gr_gid - os.chown(directory, uid, gid) - -def fixup_permission(filename, permission=stat.S_IRUSR): - """ - Check if the given file exists and change ownershit to root/vyattacfg - and appripriate file access permissions - default is user and group readable - """ - if os.path.isfile(filename): - os.chmod(filename, permission) - - # make file owned by root / vyattacfg - uid = getpwnam('root').pw_uid - gid = getgrnam('vyattacfg').gr_gid - os.chown(filename, uid, gid) - -def checkCertHeader(header, filename): - """ - Verify if filename contains specified header. - Returns True on success or on file not found to not trigger the exceptions - """ - if not os.path.isfile(filename): - return False - - with open(filename, 'r') as f: - for line in f: - if re.match(header, line): - return True - - return True - -def get_config(): - openvpn = deepcopy(default_config_data) - conf = Config() - - # determine tagNode instance - try: - openvpn['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - except KeyError as E: - print("Interface not specified") - - # Check if interface instance has been removed - if not conf.exists('interfaces openvpn ' + openvpn['intf']): - openvpn['deleted'] = True - return openvpn - - # Check if we belong to any bridge interface - for bridge in conf.list_nodes('interfaces bridge'): - for intf in conf.list_nodes('interfaces bridge {} member interface'.format(bridge)): - if intf == openvpn['intf']: - openvpn['bridge_member'].append(intf) - - # set configuration level - conf.set_level('interfaces openvpn ' + openvpn['intf']) - - # retrieve authentication options - username - if conf.exists('authentication username'): - openvpn['auth_user'] = conf.return_value('authentication username') - openvpn['auth'] = True - - # retrieve authentication options - username - if conf.exists('authentication password'): - openvpn['auth_pass'] = conf.return_value('authentication password') - openvpn['auth'] = True - - # retrieve interface description - if conf.exists('description'): - openvpn['description'] = conf.return_value('description') - - # interface device-type - if conf.exists('device-type'): - openvpn['type'] = conf.return_value('device-type') - - # disable interface - if conf.exists('disable'): - openvpn['disable'] = True - - # data encryption algorithm - if conf.exists('encryption'): - openvpn['encryption'] = conf.return_value('encryption') - - # hash algorithm - if conf.exists('hash'): - openvpn['hash'] = conf.return_value('hash') - - # Maximum number of keepalive packet failures - if conf.exists('keep-alive failure-count') and conf.exists('keep-alive interval'): - fail_count = conf.return_value('keep-alive failure-count') - interval = conf.return_value('keep-alive interval') - openvpn['ping_interval' ] = interval - openvpn['ping_restart' ] = int(interval) * int(fail_count) - - # Local IP address of tunnel - even as it is a tag node - we can only work - # on the first address - if conf.exists('local-address'): - openvpn['local_address'] = conf.list_nodes('local-address')[0] - if conf.exists('local-address {} subnet-mask'.format(openvpn['local_address'])): - openvpn['local_address_subnet'] = conf.return_value('local-address {} subnet-mask'.format(openvpn['local_address'])) - - # Local IP address to accept connections - if conf.exists('local-host'): - openvpn['local_host'] = conf.return_value('local-host') - - # Local port number to accept connections - if conf.exists('local-port'): - openvpn['local_port'] = conf.return_value('local-port') - - # OpenVPN operation mode - if conf.exists('mode'): - mode = conf.return_value('mode') - openvpn['mode'] = mode - - # Additional OpenVPN options - if conf.exists('openvpn-option'): - openvpn['options'] = conf.return_values('openvpn-option') - - # Do not close and reopen interface - if conf.exists('persistent-tunnel'): - openvpn['persistent_tunnel'] = True - - # Communication protocol - if conf.exists('protocol'): - openvpn['protocol'] = conf.return_value('protocol') - - # IP address of remote end of tunnel - if conf.exists('remote-address'): - openvpn['remote_address'] = conf.return_value('remote-address') - - # Remote host to connect to (dynamic if not set) - if conf.exists('remote-host'): - openvpn['remote_host'] = conf.return_values('remote-host') - - # Remote port number to connect to - if conf.exists('remote-port'): - openvpn['remote_port'] = conf.return_value('remote-port') - - # OpenVPN tunnel to be used as the default route - # see https://openvpn.net/community-resources/reference-manual-for-openvpn-2-4/ - # redirect-gateway flags - if conf.exists('replace-default-route'): - openvpn['redirect_gateway'] = 'def1' - - if conf.exists('replace-default-route local'): - openvpn['redirect_gateway'] = 'local def1' - - # Topology for clients - if conf.exists('server topology'): - openvpn['server_topology'] = conf.return_value('server topology') - - # Server-mode subnet (from which client IPs are allocated) - if conf.exists('server subnet'): - network = conf.return_value('server subnet') - tmp = IPv4Interface(network).with_netmask - # convert the network in format: "192.0.2.0 255.255.255.0" for later use in template - openvpn['server_subnet'] = tmp.replace(r'/', ' ') - - # Client-specific settings - for client in conf.list_nodes('server client'): - # set configuration level - conf.set_level('interfaces openvpn ' + openvpn['intf'] + ' server client ' + client) - data = { - 'name': client, - 'disable': False, - 'ip': '', - 'push_route': [], - 'subnet': [], - 'remote_netmask': '' - } - - # note: with "topology subnet", this is " ". - # with "topology p2p", this is " ". - if openvpn['server_topology'] == 'subnet': - # we are only interested in the netmask portion of server_subnet - data['remote_netmask'] = openvpn['server_subnet'].split(' ')[1] - else: - # we need the server subnet in format 192.0.2.0/255.255.255.0 - subnet = openvpn['server_subnet'].replace(' ', r'/') - # get iterator over the usable hosts in the network - tmp = ip_network(subnet).hosts() - # OpenVPN always uses the subnets first available IP address - data['remote_netmask'] = list(tmp)[0] - - # Option to disable client connection - if conf.exists('disable'): - data['disable'] = True - - # IP address of the client - if conf.exists('ip'): - data['ip'] = conf.return_value('ip') - - # Route to be pushed to the client - for network in conf.return_values('push-route'): - tmp = IPv4Interface(network).with_netmask - data['push_route'].append(tmp.replace(r'/', ' ')) - - # Subnet belonging to the client - for network in conf.return_values('subnet'): - tmp = IPv4Interface(network).with_netmask - data['subnet'].append(tmp.replace(r'/', ' ')) - - # Append to global client list - openvpn['client'].append(data) - - # re-set configuration level - conf.set_level('interfaces openvpn ' + openvpn['intf']) - - # DNS suffix to be pushed to all clients - if conf.exists('server domain-name'): - openvpn['server_domain'] = conf.return_value('server domain-name') - - # Number of maximum client connections - if conf.exists('server max-connections'): - openvpn['server_max_conn'] = conf.return_value('server max-connections') - - # Domain Name Server (DNS) - if conf.exists('server name-server'): - openvpn['server_dns_nameserver'] = conf.return_values('server name-server') - - # Route to be pushed to all clients - if conf.exists('server push-route'): - for network in conf.return_values('server push-route'): - tmp = IPv4Interface(network).with_netmask - openvpn['server_push_route'].append(tmp.replace(r'/', ' ')) - - # Reject connections from clients that are not explicitly configured - if conf.exists('server reject-unconfigured-clients'): - openvpn['server_reject_unconfigured'] = True - - # File containing TLS auth static key - if conf.exists('tls auth-file'): - openvpn['tls_auth'] = conf.return_value('tls auth-file') - openvpn['tls'] = True - - # File containing certificate for Certificate Authority (CA) - if conf.exists('tls ca-cert-file'): - openvpn['tls_ca_cert'] = conf.return_value('tls ca-cert-file') - openvpn['tls'] = True - - # File containing certificate for this host - if conf.exists('tls cert-file'): - openvpn['tls_cert'] = conf.return_value('tls cert-file') - openvpn['tls'] = True - - # File containing certificate revocation list (CRL) for this host - if conf.exists('tls crl-file'): - openvpn['tls_crl'] = conf.return_value('tls crl-file') - openvpn['tls'] = True - - # File containing Diffie Hellman parameters (server only) - if conf.exists('tls dh-file'): - openvpn['tls_dh'] = conf.return_value('tls dh-file') - openvpn['tls'] = True - - # File containing this host's private key - if conf.exists('tls key-file'): - openvpn['tls_key'] = conf.return_value('tls key-file') - openvpn['tls'] = True - - # Role in TLS negotiation - if conf.exists('tls role'): - openvpn['tls_role'] = conf.return_value('tls role') - openvpn['tls'] = True - - # Minimum required TLS version - if conf.exists('tls tls-version-min'): - openvpn['tls_version_min'] = conf.return_value('tls tls-version-min') - - if conf.exists('shared-secret-key-file'): - openvpn['shared_secret_file'] = conf.return_value('shared-secret-key-file') - - if conf.exists('use-lzo-compression'): - openvpn['compress_lzo'] = True - - return openvpn - -def verify(openvpn): - if openvpn['deleted']: - return None - - if not openvpn['mode']: - raise ConfigError('Must specify OpenVPN operation mode') - - # Checks which need to be performed on interface rmeoval - if openvpn['deleted']: - # OpenVPN interface can not be deleted if it's still member of a bridge - if openvpn['bridge_member']: - raise ConfigError('Can not delete {} as it is a member interface of bridge {}!'.format(openvpn['intf'], bridge)) - - # - # OpenVPN client mode - VERIFY - # - if openvpn['mode'] == 'client': - if openvpn['local_port']: - raise ConfigError('Cannot specify "local-port" in client mode') - - if openvpn['local_host']: - raise ConfigError('Cannot specify "local-host" in client mode') - - if openvpn['protocol'] == 'tcp-passive': - raise ConfigError('Protocol "tcp-passive" is not valid in client mode') - - if not openvpn['remote_host']: - raise ConfigError('Must specify "remote-host" in client mode') - - if openvpn['tls_dh']: - raise ConfigError('Cannot specify "tls dh-file" in client mode') - - # - # OpenVPN site-to-site - VERIFY - # - if openvpn['mode'] == 'site-to-site': - if not (openvpn['local_address'] or openvpn['bridge_member']): - raise ConfigError('Must specify "local-address" or "bridge member interface"') - - for host in openvpn['remote_host']: - if host == openvpn['remote_address']: - raise ConfigError('"remote-address" cannot be the same as "remote-host"') - - if openvpn['type'] == 'tun': - if not openvpn['remote_address']: - raise ConfigError('Must specify "remote-address"') - - if openvpn['local_address'] == openvpn['remote_address']: - raise ConfigError('"local-address" and "remote-address" cannot be the same') - - if openvpn['local_address'] == openvpn['local_host']: - raise ConfigError('"local-address" cannot be the same as "local-host"') - - else: - if openvpn['local_address'] or openvpn['remote_address']: - raise ConfigError('Cannot specify "local-address" or "remote-address" in client-server mode') - - elif openvpn['bridge_member']: - raise ConfigError('Cannot specify "local-address" or "remote-address" in bridge mode') - - # - # OpenVPN server mode - VERIFY - # - if openvpn['mode'] == 'server': - if openvpn['protocol'] == 'tcp-active': - raise ConfigError('Protocol "tcp-active" is not valid in server mode') - - if openvpn['remote_port']: - raise ConfigError('Cannot specify "remote-port" in server mode') - - if openvpn['remote_host']: - raise ConfigError('Cannot specify "remote-host" in server mode') - - if openvpn['protocol'] == 'tcp-passive' and len(openvpn['remote_host']) > 1: - raise ConfigError('Cannot specify more than 1 "remote-host" with "tcp-passive"') - - if not openvpn['tls_dh']: - raise ConfigError('Must specify "tls dh-file" in server mode') - - if not openvpn['server_subnet']: - if not openvpn['bridge_member']: - raise ConfigError('Must specify "server subnet" option in server mode') - - else: - # checks for both client and site-to-site go here - if openvpn['server_reject_unconfigured']: - raise ConfigError('reject-unconfigured-clients is only supported in OpenVPN server mode') - - if openvpn['server_topology']: - raise ConfigError('The "topology" option is only valid in server mode') - - if (not openvpn['remote_host']) and openvpn['redirect_gateway']: - raise ConfigError('Cannot set "replace-default-route" without "remote-host"') - - # - # OpenVPN common verification section - # not depending on any operation mode - # - - # verify specified IP address is present on any interface on this system - if openvpn['local_host']: - if not is_addr_assigned(openvpn['local_host']): - raise ConfigError('No interface on system with specified local-host IP address: {}'.format(openvpn['local_host'])) - - # TCP active - if openvpn['protocol'] == 'tcp-active': - if openvpn['local_port']: - raise ConfigError('Cannot specify "local-port" with "tcp-active"') - - if not openvpn['remote_host']: - raise ConfigError('Must specify "remote-host" with "tcp-active"') - - # shared secret and TLS - if not (openvpn['shared_secret_file'] or openvpn['tls']): - raise ConfigError('Must specify one of "shared-secret-key-file" and "tls"') - - if openvpn['shared_secret_file'] and openvpn['tls']: - raise ConfigError('Can only specify one of "shared-secret-key-file" and "tls"') - - if openvpn['mode'] in ['client', 'server']: - if not openvpn['tls']: - raise ConfigError('Must specify "tls" in client-server mode') - - # - # TLS/encryption - # - if openvpn['shared_secret_file']: - if openvpn['encryption'] in ['aes128gcm', 'aes192gcm', 'aes256gcm']: - raise ConfigError('GCM encryption with shared-secret-key-file is not supported') - - if not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', openvpn['shared_secret_file']): - raise ConfigError('Specified shared-secret-key-file "{}" is not valid'.format(openvpn['shared_secret_file'])) - - if openvpn['tls']: - if not openvpn['tls_ca_cert']: - raise ConfigError('Must specify "tls ca-cert-file"') - - if not (openvpn['mode'] == 'client' and openvpn['auth']): - if not openvpn['tls_cert']: - raise ConfigError('Must specify "tls cert-file"') - - if not openvpn['tls_key']: - raise ConfigError('Must specify "tls key-file"') - - if not checkCertHeader('-----BEGIN CERTIFICATE-----', openvpn['tls_ca_cert']): - raise ConfigError('Specified ca-cert-file "{}" is invalid'.format(openvpn['tls_ca_cert'])) - - if openvpn['tls_auth']: - if not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', openvpn['tls_auth']): - raise ConfigError('Specified auth-file "{}" is invalid'.format(openvpn['tls_auth'])) - - if openvpn['tls_cert']: - if not checkCertHeader('-----BEGIN CERTIFICATE-----', openvpn['tls_cert']): - raise ConfigError('Specified cert-file "{}" is invalid'.format(openvpn['tls_cert'])) - - if openvpn['tls_key']: - if not checkCertHeader('-----BEGIN (?:RSA )?PRIVATE KEY-----', openvpn['tls_key']): - raise ConfigError('Specified key-file "{}" is not valid'.format(openvpn['tls_key'])) - - if openvpn['tls_crl']: - if not checkCertHeader('-----BEGIN X509 CRL-----', openvpn['tls_crl']): - raise ConfigError('Specified crl-file "{} not valid'.format(openvpn['tls_crl'])) - - if openvpn['tls_dh']: - if not checkCertHeader('-----BEGIN DH PARAMETERS-----', openvpn['tls_dh']): - raise ConfigError('Specified dh-file "{}" is not valid'.format(openvpn['tls_dh'])) - - if openvpn['tls_role']: - if openvpn['mode'] in ['client', 'server']: - if not openvpn['tls_auth']: - raise ConfigError('Cannot specify "tls role" in client-server mode') - - if openvpn['tls_role'] == 'active': - if openvpn['protocol'] == 'tcp-passive': - raise ConfigError('Cannot specify "tcp-passive" when "tls role" is "active"') - - if openvpn['tls_dh']: - raise ConfigError('Cannot specify "tls dh-file" when "tls role" is "active"') - - elif openvpn['tls_role'] == 'passive': - if openvpn['protocol'] == 'tcp-active': - raise ConfigError('Cannot specify "tcp-active" when "tls role" is "passive"') - - if not openvpn['tls_dh']: - raise ConfigError('Must specify "tls dh-file" when "tls role" is "passive"') - - # - # Auth user/pass - # - if openvpn['auth']: - if not openvpn['auth_user']: - raise ConfigError('Username for authentication is missing') - - if not openvpn['auth_pass']: - raise ConfigError('Password for authentication is missing') - - # - # Client - # - subnet = openvpn['server_subnet'].replace(' ', '/') - for client in openvpn['client']: - if not ip_address(client['ip']) in ip_network(subnet): - raise ConfigError('Client IP "{}" not in server subnet "{}'.format(client['ip'], subnet)) - - - - return None - -def generate(openvpn): - if openvpn['deleted'] or openvpn['disable']: - return None - - interface = openvpn['intf'] - directory = os.path.dirname(get_config_name(interface)) - - # create config directory on demand - openvpn_mkdir(directory) - # create status directory on demand - openvpn_mkdir(directory + '/status') - # create client config dir on demand - openvpn_mkdir(directory + '/ccd') - # crete client config dir per interface on demand - openvpn_mkdir(directory + '/ccd/' + interface) - - # Fix file permissons for keys - fixup_permission(openvpn['shared_secret_file']) - fixup_permission(openvpn['tls_key']) - - # Generate User/Password authentication file - if openvpn['auth']: - auth_file = '/tmp/openvpn-{}-pw'.format(interface) - with open(auth_file, 'w') as f: - f.write('{}\n{}'.format(openvpn['auth_user'], openvpn['auth_pass'])) - - fixup_permission(auth_file) - - # get numeric uid/gid - uid = getpwnam(user).pw_uid - gid = getgrnam(group).gr_gid - - # Generate client specific configuration - for client in openvpn['client']: - client_file = directory + '/ccd/' + interface + '/' + client['name'] - tmpl = jinja2.Template(client_tmpl) - client_text = tmpl.render(client) - with open(client_file, 'w') as f: - f.write(client_text) - os.chown(client_file, uid, gid) - - tmpl = jinja2.Template(config_tmpl) - config_text = tmpl.render(openvpn) - - # we need to support quoting of raw parameters from OpenVPN CLI - # see https://phabricator.vyos.net/T1632 - config_text = config_text.replace(""",'"') - - with open(get_config_name(interface), 'w') as f: - f.write(config_text) - os.chown(get_config_name(interface), uid, gid) - - return None - -def apply(openvpn): - pid = 0 - pidfile = '/var/run/openvpn/{}.pid'.format(openvpn['intf']) - if os.path.isfile(pidfile): - pid = 0 - with open(pidfile, 'r') as f: - pid = int(f.read()) - - # Always stop OpenVPN service. We can not send a SIGUSR1 for restart of the - # service as the configuration is not re-read. Stop daemon only if it's - # running - it could have died or killed by someone evil - if pid_exists(pid): - cmd = 'start-stop-daemon --stop --quiet' - cmd += ' --pidfile ' + pidfile - subprocess_cmd(cmd) - - # cleanup old PID file - if os.path.isfile(pidfile): - os.remove(pidfile) - - # Do some cleanup when OpenVPN is disabled/deleted - if openvpn['deleted'] or openvpn['disable']: - # cleanup old configuration file - if os.path.isfile(get_config_name(openvpn['intf'])): - os.remove(get_config_name(openvpn['intf'])) - - # cleanup client config dir - directory = os.path.dirname(get_config_name(openvpn['intf'])) - if os.path.isdir(directory + '/ccd/' + openvpn['intf']): - try: - os.remove(directory + '/ccd/' + openvpn['intf'] + '/*') - except: - pass - - return None - - # On configuration change we need to wait for the 'old' interface to - # vanish from the Kernel, if it is not gone, OpenVPN will report: - # ERROR: Cannot ioctl TUNSETIFF vtun10: Device or resource busy (errno=16) - while openvpn['intf'] in interfaces(): - sleep(0.250) # 250ms - - # No matching OpenVPN process running - maybe it got killed or none - # existed - nevertheless, spawn new OpenVPN process - cmd = 'start-stop-daemon --start --quiet' - cmd += ' --pidfile ' + pidfile - cmd += ' --exec /usr/sbin/openvpn' - # now pass arguments to openvpn binary - cmd += ' --' - cmd += ' --config ' + get_config_name(openvpn['intf']) - - # execute assembled command - subprocess_cmd(cmd) - - # better late then sorry ... but we can only set interface alias after - # OpenVPN has been launched and created the interface - cnt = 0 - while openvpn['intf'] not in interfaces(): - # If VPN tunnel can't be established because the peer/server isn't - # (temporarily) available, the vtun interface never becomes registered - # with the kernel, and the commit would hang if there is no bail out - # condition - cnt += 1 - if cnt == 50: - break - - # sleep 250ms - sleep(0.250) - - try: - # we need to catch the exception if the interface is not up due to - # reason stated above - Interface(openvpn['intf']).set_alias(openvpn['description']) - except: - pass - - return None - - -if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - sys.exit(1) diff --git a/src/conf_mode/interface-vxlan.py b/src/conf_mode/interface-vxlan.py deleted file mode 100755 index 1097ae4d0..000000000 --- a/src/conf_mode/interface-vxlan.py +++ /dev/null @@ -1,198 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2019 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import os - -from sys import exit -from copy import deepcopy - -from vyos.configdict import list_diff -from vyos.config import Config -from vyos.ifconfig import VXLANIf, Interface -from vyos.interfaces import get_type_of_interface -from vyos import ConfigError -from netifaces import interfaces - -default_config_data = { - 'address': [], - 'deleted': False, - 'description': '', - 'disable': False, - 'group': '', - 'intf': '', - 'ip_arp_cache_tmo': 30, - 'ip_proxy_arp': 0, - 'link': '', - 'mtu': 1450, - 'remote': '', - 'remote_port': 8472 # The Linux implementation of VXLAN pre-dates - # the IANA's selection of a standard destination port -} - -def get_config(): - vxlan = deepcopy(default_config_data) - conf = Config() - - # determine tagNode instance - try: - vxlan['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - except KeyError as E: - print("Interface not specified") - - # Check if interface has been removed - if not conf.exists('interfaces vxlan ' + vxlan['intf']): - vxlan['deleted'] = True - return vxlan - - # set new configuration level - conf.set_level('interfaces vxlan ' + vxlan['intf']) - - # retrieve configured interface addresses - if conf.exists('address'): - vxlan['address'] = conf.return_values('address') - - # retrieve interface description - if conf.exists('description'): - vxlan['description'] = conf.return_value('description') - - # Disable this interface - if conf.exists('disable'): - vxlan['disable'] = True - - # VXLAN multicast grou - if conf.exists('group'): - vxlan['group'] = conf.return_value('group') - - # ARP cache entry timeout in seconds - if conf.exists('ip arp-cache-timeout'): - vxlan['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) - - # Enable proxy-arp on this interface - if conf.exists('ip enable-proxy-arp'): - vxlan['ip_proxy_arp'] = 1 - - # VXLAN underlay interface - if conf.exists('link'): - vxlan['link'] = conf.return_value('link') - - # Maximum Transmission Unit (MTU) - if conf.exists('mtu'): - vxlan['mtu'] = int(conf.return_value('mtu')) - - # Remote address of VXLAN tunnel - if conf.exists('remote'): - vxlan['remote'] = conf.return_value('remote') - - # Remote port of VXLAN tunnel - if conf.exists('port'): - vxlan['remote_port'] = int(conf.return_value('port')) - - # Virtual Network Identifier - if conf.exists('vni'): - vxlan['vni'] = conf.return_value('vni') - - return vxlan - - -def verify(vxlan): - if vxlan['deleted']: - # bail out early - return None - - if vxlan['mtu'] < 1500: - print('WARNING: RFC7348 recommends VXLAN tunnels preserve a 1500 byte MTU') - - if vxlan['group'] and not vxlan['link']: - raise ConfigError('Multicast VXLAN requires an underlaying interface ') - - if not (vxlan['group'] or vxlan['remote']): - raise ConfigError('Group or remote must be configured') - - if not vxlan['vni']: - raise ConfigError('Must configure VNI for VXLAN') - - if vxlan['link']: - # VXLAN adds a 50 byte overhead - we need to check the underlaying MTU - # if our configured MTU is at least 50 bytes less - underlay_mtu = int(Interface(vxlan['link']).get_mtu()) - if underlay_mtu < (vxlan['mtu'] + 50): - raise ConfigError('VXLAN has a 50 byte overhead, underlaying device ' \ - 'MTU is to small ({})'.format(underlay_mtu)) - - return None - - -def generate(vxlan): - return None - - -def apply(vxlan): - # Check if the VXLAN interface already exists - if vxlan['intf'] in interfaces(): - v = VXLANIf(vxlan['intf']) - # VXLAN is super picky and the tunnel always needs to be recreated, - # thus we can simply always delete it first. - v.remove() - - if not vxlan['deleted']: - # VXLAN interface needs to be created on-block - # instead of passing a ton of arguments, I just use a dict - # that is managed by vyos.ifconfig - conf = deepcopy(VXLANIf.get_config()) - - # Assign VXLAN instance configuration parameters to config dict - conf['vni'] = vxlan['vni'] - conf['group'] = vxlan['group'] - conf['dev'] = vxlan['link'] - conf['remote'] = vxlan['remote'] - conf['port'] = vxlan['remote_port'] - - # Finally create the new interface - v = VXLANIf(vxlan['intf'], config=conf) - # update interface description used e.g. by SNMP - v.set_alias(vxlan['description']) - # Maximum Transfer Unit (MTU) - v.set_mtu(vxlan['mtu']) - - # configure ARP cache timeout in milliseconds - v.set_arp_cache_tmo(vxlan['ip_arp_cache_tmo']) - # Enable proxy-arp on this interface - v.set_proxy_arp(vxlan['ip_proxy_arp']) - - # Configure interface address(es) - no need to implicitly delete the - # old addresses as they have already been removed by deleting the - # interface above - for addr in vxlan['address']: - v.add_addr(addr) - - # As the bond interface is always disabled first when changing - # parameters we will only re-enable the interface if it is not - # administratively disabled - if not vxlan['disable']: - v.set_state('up') - - return None - - -if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - exit(1) diff --git a/src/conf_mode/interface-wireguard.py b/src/conf_mode/interface-wireguard.py deleted file mode 100755 index 7a684bafa..000000000 --- a/src/conf_mode/interface-wireguard.py +++ /dev/null @@ -1,285 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2018 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# - -import sys -import os -import re -import subprocess -from copy import deepcopy -from netifaces import interfaces - -from vyos import ConfigError -from vyos.config import Config -from vyos.configdict import list_diff -from vyos.ifconfig import WireGuardIf - -kdir = r'/config/auth/wireguard' - - -def _check_kmod(): - if not os.path.exists('/sys/module/wireguard'): - if os.system('sudo modprobe wireguard') != 0: - raise ConfigError("modprobe wireguard failed") - - -def _migrate_default_keys(): - if os.path.exists('{}/private.key'.format(kdir)) and not os.path.exists('{}/default/private.key'.format(kdir)): - old_umask = os.umask(0o027) - location = '{}/default'.format(kdir) - subprocess.call(['sudo mkdir -p ' + location], shell=True) - subprocess.call(['sudo chgrp vyattacfg ' + location], shell=True) - subprocess.call(['sudo chmod 750 ' + location], shell=True) - os.rename('{}/private.key'.format(kdir), - '{}/private.key'.format(location)) - os.rename('{}/public.key'.format(kdir), - '{}/public.key'.format(location)) - os.umask(old_umask) - - -def get_config(): - c = Config() - if not c.exists('interfaces wireguard'): - return None - - dflt_cnf = { - 'intfc': '', - 'addr': [], - 'addr_remove': [], - 'descr': '', - 'lport': None, - 'delete': False, - 'state': 'up', - 'fwmark': 0x00, - 'mtu': 1420, - 'peer': {}, - 'peer_remove': [], - 'pk': '{}/default/private.key'.format(kdir) - } - - if os.getenv('VYOS_TAGNODE_VALUE'): - ifname = str(os.environ['VYOS_TAGNODE_VALUE']) - wg = deepcopy(dflt_cnf) - wg['intfc'] = ifname - wg['descr'] = ifname - else: - print("ERROR: VYOS_TAGNODE_VALUE undefined") - sys.exit(1) - - c.set_level('interfaces wireguard') - - # interface removal state - if not c.exists(ifname) and c.exists_effective(ifname): - wg['delete'] = True - - if not wg['delete']: - c.set_level('interfaces wireguard {}'.format(ifname)) - if c.exists('address'): - wg['addr'] = c.return_values('address') - - # determine addresses which need to be removed - eff_addr = c.return_effective_values('address') - wg['addr_remove'] = list_diff(eff_addr, wg['addr']) - - # ifalias description - if c.exists('description'): - wg['descr'] = c.return_value('description') - - # link state - if c.exists('disable'): - wg['state'] = 'down' - - # local port to listen on - if c.exists('port'): - wg['lport'] = c.return_value('port') - - # fwmark value - if c.exists('fwmark'): - wg['fwmark'] = c.return_value('fwmark') - - # mtu - if c.exists('mtu'): - wg['mtu'] = c.return_value('mtu') - - # private key - if c.exists('private-key'): - wg['pk'] = "{0}/{1}/private.key".format( - kdir, c.return_value('private-key')) - - # peer removal, wg identifies peers by its pubkey - peer_eff = c.list_effective_nodes('peer') - peer_rem = list_diff(peer_eff, c.list_nodes('peer')) - for p in peer_rem: - wg['peer_remove'].append( - c.return_effective_value('peer {} pubkey'.format(p))) - - # peer settings - if c.exists('peer'): - for p in c.list_nodes('peer'): - if not c.exists('peer ' + p + ' disable'): - wg['peer'].update( - { - p: { - 'allowed-ips': [], - 'endpoint': '', - 'pubkey': '' - } - } - ) - # peer allowed-ips - if c.exists('peer ' + p + ' allowed-ips'): - wg['peer'][p]['allowed-ips'] = c.return_values( - 'peer ' + p + ' allowed-ips') - # peer endpoint - if c.exists('peer ' + p + ' endpoint'): - wg['peer'][p]['endpoint'] = c.return_value( - 'peer ' + p + ' endpoint') - # persistent-keepalive - if c.exists('peer ' + p + ' persistent-keepalive'): - wg['peer'][p]['persistent-keepalive'] = c.return_value( - 'peer ' + p + ' persistent-keepalive') - # preshared-key - if c.exists('peer ' + p + ' preshared-key'): - wg['peer'][p]['psk'] = c.return_value( - 'peer ' + p + ' preshared-key') - # peer pubkeys - key_eff = c.return_effective_value( - 'peer {peer} pubkey'.format(peer=p)) - key_cfg = c.return_value( - 'peer {peer} pubkey'.format(peer=p)) - wg['peer'][p]['pubkey'] = key_cfg - - # on a pubkey change we need to remove the pubkey first - # peers are identified by pubkey, so key update means - # peer removal and re-add - if key_eff != key_cfg and key_eff != None: - wg['peer_remove'].append(key_cfg) - - return wg - - -def verify(c): - if not c: - return None - - if not os.path.exists(c['pk']): - raise ConfigError( - "No keys found, generate them by executing: \'run generate wireguard [keypair|named-keypairs]\'") - - if not c['delete']: - if not c['addr']: - raise ConfigError("ERROR: IP address required") - if not c['peer']: - raise ConfigError("ERROR: peer required") - for p in c['peer']: - if not c['peer'][p]['allowed-ips']: - raise ConfigError("ERROR: allowed-ips required for peer " + p) - if not c['peer'][p]['pubkey']: - raise ConfigError("peer pubkey required for peer " + p) - if not c['peer'][p]['endpoint']: - raise ConfigError("peer endpoint required for peer " + p) - - -def apply(c): - # no wg configs left, remove all interface from system - # maybe move it into ifconfig.py - if not c: - net_devs = os.listdir('/sys/class/net/') - for dev in net_devs: - if os.path.isdir('/sys/class/net/' + dev): - buf = open('/sys/class/net/' + dev + '/uevent', 'r').read() - if re.search("DEVTYPE=wireguard", buf, re.I | re.M): - wg_intf = re.sub("INTERFACE=", "", re.search( - "INTERFACE=.*", buf, re.I | re.M).group(0)) - subprocess.call( - ['ip l d dev ' + wg_intf + ' >/dev/null'], shell=True) - return None - - # init wg class - intfc = WireGuardIf(c['intfc']) - - # single interface removal - if c['delete']: - intfc.remove() - return None - - # remove IP addresses - for ip in c['addr_remove']: - intfc.del_addr(ip) - - # add IP addresses - for ip in c['addr']: - intfc.add_addr(ip) - - # interface mtu - intfc.set_mtu(int(c['mtu'])) - - # ifalias for snmp from description - intfc.set_alias(str(c['descr'])) - - # remove peers - if c['peer_remove']: - for pkey in c['peer_remove']: - intfc.remove_peer(pkey) - - # peer pubkey - # setting up the wg interface - intfc.config['private-key'] = c['pk'] - for p in c['peer']: - # peer pubkey - intfc.config['pubkey'] = str(c['peer'][p]['pubkey']) - # peer allowed-ips - intfc.config['allowed-ips'] = c['peer'][p]['allowed-ips'] - # local listen port - if c['lport']: - intfc.config['port'] = c['lport'] - # fwmark - if c['fwmark']: - intfc.config['fwmark'] = c['fwmark'] - # endpoint - if c['peer'][p]['endpoint']: - intfc.config['endpoint'] = c['peer'][p]['endpoint'] - - # persistent-keepalive - if 'persistent-keepalive' in c['peer'][p]: - intfc.config['keepalive'] = c['peer'][p]['persistent-keepalive'] - - # maybe move it into ifconfig.py - # preshared-key - needs to be read from a file - if 'psk' in c['peer'][p]: - psk_file = '/config/auth/wireguard/psk' - old_umask = os.umask(0o077) - open(psk_file, 'w').write(str(c['peer'][p]['psk'])) - os.umask(old_umask) - intfc.config['psk'] = psk_file - intfc.update() - - # interface state - intfc.set_state(c['state']) - - return None - -if __name__ == '__main__': - try: - _check_kmod() - _migrate_default_keys() - c = get_config() - verify(c) - apply(c) - except ConfigError as e: - print(e) - sys.exit(1) diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py new file mode 100755 index 000000000..8a0f9f84d --- /dev/null +++ b/src/conf_mode/interfaces-bonding.py @@ -0,0 +1,526 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os + +from copy import deepcopy +from sys import exit +from netifaces import interfaces + +from vyos.ifconfig import BondIf, VLANIf +from vyos.configdict import list_diff, vlan_to_dict +from vyos.config import Config +from vyos import ConfigError + +default_config_data = { + 'address': [], + 'address_remove': [], + 'arp_mon_intvl': 0, + 'arp_mon_tgt': [], + 'description': '', + 'deleted': False, + 'dhcp_client_id': '', + 'dhcp_hostname': '', + 'dhcp_vendor_class_id': '', + 'dhcpv6_prm_only': False, + 'dhcpv6_temporary': False, + 'disable': False, + 'disable_link_detect': 1, + 'hash_policy': 'layer2', + 'ip_arp_cache_tmo': 30, + 'ip_proxy_arp': 0, + 'ip_proxy_arp_pvlan': 0, + 'intf': '', + 'mac': '', + 'mode': '802.3ad', + 'member': [], + 'mtu': 1500, + 'primary': '', + 'vif_s': [], + 'vif_s_remove': [], + 'vif': [], + 'vif_remove': [] +} + + +def get_bond_mode(mode): + if mode == 'round-robin': + return 'balance-rr' + elif mode == 'active-backup': + return 'active-backup' + elif mode == 'xor-hash': + return 'balance-xor' + elif mode == 'broadcast': + return 'broadcast' + elif mode == '802.3ad': + return '802.3ad' + elif mode == 'transmit-load-balance': + return 'balance-tlb' + elif mode == 'adaptive-load-balance': + return 'balance-alb' + else: + raise ConfigError('invalid bond mode "{}"'.format(mode)) + + +def apply_vlan_config(vlan, config): + """ + Generic function to apply a VLAN configuration from a dictionary + to a VLAN interface + """ + + if type(vlan) != type(VLANIf("lo")): + raise TypeError() + + # get DHCP config dictionary and update values + opt = vlan.get_dhcp_options() + + if config['dhcp_client_id']: + opt['client_id'] = config['dhcp_client_id'] + + if config['dhcp_hostname']: + opt['hostname'] = config['dhcp_hostname'] + + if config['dhcp_vendor_class_id']: + opt['vendor_class_id'] = config['dhcp_vendor_class_id'] + + # store DHCP config dictionary - used later on when addresses are aquired + vlan.set_dhcp_options(opt) + + # get DHCPv6 config dictionary and update values + opt = vlan.get_dhcpv6_options() + + if config['dhcpv6_prm_only']: + opt['dhcpv6_prm_only'] = True + + if config['dhcpv6_temporary']: + opt['dhcpv6_temporary'] = True + + # store DHCPv6 config dictionary - used later on when addresses are aquired + vlan.set_dhcpv6_options(opt) + + # update interface description used e.g. within SNMP + vlan.set_alias(config['description']) + # ignore link state changes + vlan.set_link_detect(config['disable_link_detect']) + # Maximum Transmission Unit (MTU) + vlan.set_mtu(config['mtu']) + # Change VLAN interface MAC address + if config['mac']: + vlan.set_mac(config['mac']) + + # enable/disable VLAN interface + if config['disable']: + vlan.set_state('down') + else: + vlan.set_state('up') + + # Configure interface address(es) + # - not longer required addresses get removed first + # - newly addresses will be added second + for addr in config['address_remove']: + vlan.del_addr(addr) + for addr in config['address']: + vlan.add_addr(addr) + + +def get_config(): + # initialize kernel module if not loaded + if not os.path.isfile('/sys/class/net/bonding_masters'): + import syslog + syslog.syslog(syslog.LOG_NOTICE, "loading bonding kernel module") + if os.system('modprobe bonding max_bonds=0 miimon=250') != 0: + syslog.syslog(syslog.LOG_NOTICE, "failed loading bonding kernel module") + raise ConfigError("failed loading bonding kernel module") + + bond = deepcopy(default_config_data) + conf = Config() + + # determine tagNode instance + try: + bond['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + except KeyError as E: + print("Interface not specified") + + # check if bond has been removed + cfg_base = 'interfaces bonding ' + bond['intf'] + if not conf.exists(cfg_base): + bond['deleted'] = True + return bond + + # set new configuration level + conf.set_level(cfg_base) + + # retrieve configured interface addresses + if conf.exists('address'): + bond['address'] = conf.return_values('address') + + # get interface addresses (currently effective) - to determine which + # address is no longer valid and needs to be removed + eff_addr = conf.return_effective_values('address') + bond['address_remove'] = list_diff(eff_addr, bond['address']) + + # ARP link monitoring frequency in milliseconds + if conf.exists('arp-monitor interval'): + bond['arp_mon_intvl'] = int(conf.return_value('arp-monitor interval')) + + # IP address to use for ARP monitoring + if conf.exists('arp-monitor target'): + bond['arp_mon_tgt'] = conf.return_values('arp-monitor target') + + # retrieve interface description + if conf.exists('description'): + bond['description'] = conf.return_value('description') + + # get DHCP client identifier + if conf.exists('dhcp-options client-id'): + bond['dhcp_client_id'] = conf.return_value('dhcp-options client-id') + + # DHCP client host name (overrides the system host name) + if conf.exists('dhcp-options host-name'): + bond['dhcp_hostname'] = conf.return_value('dhcp-options host-name') + + # DHCP client vendor identifier + if conf.exists('dhcp-options vendor-class-id'): + bond['dhcp_vendor_class_id'] = conf.return_value('dhcp-options vendor-class-id') + + # DHCPv6 only acquire config parameters, no address + if conf.exists('dhcpv6-options parameters-only'): + bond['dhcpv6_prm_only'] = True + + # DHCPv6 temporary IPv6 address + if conf.exists('dhcpv6-options temporary'): + bond['dhcpv6_temporary'] = True + + # ignore link state changes + if conf.exists('disable-link-detect'): + bond['disable_link_detect'] = 2 + + # disable bond interface + if conf.exists('disable'): + bond['disable'] = True + + # Bonding transmit hash policy + if conf.exists('hash-policy'): + bond['hash_policy'] = conf.return_value('hash-policy') + + # ARP cache entry timeout in seconds + if conf.exists('ip arp-cache-timeout'): + bond['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) + + # Enable proxy-arp on this interface + if conf.exists('ip enable-proxy-arp'): + bond['ip_proxy_arp'] = 1 + + # Enable private VLAN proxy ARP on this interface + if conf.exists('ip proxy-arp-pvlan'): + bond['ip_proxy_arp_pvlan'] = 1 + + # Media Access Control (MAC) address + if conf.exists('mac'): + bond['mac'] = conf.return_value('mac') + + # Bonding mode + if conf.exists('mode'): + bond['mode'] = get_bond_mode(conf.return_value('mode')) + + # Maximum Transmission Unit (MTU) + if conf.exists('mtu'): + bond['mtu'] = int(conf.return_value('mtu')) + + # determine bond member interfaces (currently configured) + if conf.exists('member interface'): + bond['member'] = conf.return_values('member interface') + + # Primary device interface + if conf.exists('primary'): + bond['primary'] = conf.return_value('primary') + + # re-set configuration level to parse new nodes + conf.set_level(cfg_base) + # get vif-s interfaces (currently effective) - to determine which vif-s + # interface is no longer present and needs to be removed + eff_intf = conf.list_effective_nodes('vif-s') + act_intf = conf.list_nodes('vif-s') + bond['vif_s_remove'] = list_diff(eff_intf, act_intf) + + if conf.exists('vif-s'): + for vif_s in conf.list_nodes('vif-s'): + # set config level to vif-s interface + conf.set_level(cfg_base + ' vif-s ' + vif_s) + bond['vif_s'].append(vlan_to_dict(conf)) + + # re-set configuration level to parse new nodes + conf.set_level(cfg_base) + # Determine vif interfaces (currently effective) - to determine which + # vif interface is no longer present and needs to be removed + eff_intf = conf.list_effective_nodes('vif') + act_intf = conf.list_nodes('vif') + bond['vif_remove'] = list_diff(eff_intf, act_intf) + + if conf.exists('vif'): + for vif in conf.list_nodes('vif'): + # set config level to vif interface + conf.set_level(cfg_base + ' vif ' + vif) + bond['vif'].append(vlan_to_dict(conf)) + + return bond + + +def verify(bond): + if len (bond['arp_mon_tgt']) > 16: + raise ConfigError('The maximum number of targets that can be specified is 16') + + if bond['primary']: + if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']: + raise ConfigError('Mode dependency failed, primary not supported ' \ + 'in this mode.'.format()) + + if bond['primary'] not in bond['member']: + raise ConfigError('Interface "{}" is not part of the bond' \ + .format(bond['primary'])) + + + # DHCPv6 parameters-only and temporary address are mutually exclusive + for vif_s in bond['vif_s']: + if vif_s['dhcpv6_prm_only'] and vif_s['dhcpv6_temporary']: + raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') + + for vif_c in vif_s['vif_c']: + if vif_c['dhcpv6_prm_only'] and vif_c['dhcpv6_temporary']: + raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') + + for vif in bond['vif']: + if vif['dhcpv6_prm_only'] and vif['dhcpv6_temporary']: + raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') + + + for vif_s in bond['vif_s']: + for vif in bond['vif']: + if vif['id'] == vif_s['id']: + raise ConfigError('Can not use identical ID on vif and vif-s interface') + + + conf = Config() + for intf in bond['member']: + # a bonding member interface is only allowed to be assigned to one bond! + all_bonds = conf.list_nodes('interfaces bonding') + # We do not need to check our own bond + all_bonds.remove(bond['intf']) + for tmp in all_bonds: + if conf.exists('interfaces bonding ' + tmp + ' member interface ' + intf): + raise ConfigError('can not enslave interface {} which already ' \ + 'belongs to {}'.format(intf, tmp)) + + # can not add interfaces with an assigned address to a bond + if conf.exists('interfaces ethernet ' + intf + ' address'): + raise ConfigError('can not enslave interface {} which has an address ' \ + 'assigned'.format(intf)) + + # bond members are not allowed to be bridge members, too + for tmp in conf.list_nodes('interfaces bridge'): + if conf.exists('interfaces bridge ' + tmp + ' member interface ' + intf): + raise ConfigError('can not enslave interface {} which belongs to ' \ + 'bridge {}'.format(intf, tmp)) + + # bond members are not allowed to be vrrp members, too + for tmp in conf.list_nodes('high-availability vrrp group'): + if conf.exists('high-availability vrrp group ' + tmp + ' interface ' + intf): + raise ConfigError('can not enslave interface {} which belongs to ' \ + 'VRRP group {}'.format(intf, tmp)) + + # bond members are not allowed to be underlaying psuedo-ethernet devices + for tmp in conf.list_nodes('interfaces pseudo-ethernet'): + if conf.exists('interfaces pseudo-ethernet ' + tmp + ' link ' + intf): + raise ConfigError('can not enslave interface {} which belongs to ' \ + 'pseudo-ethernet {}'.format(intf, tmp)) + + # bond members are not allowed to be underlaying vxlan devices + for tmp in conf.list_nodes('interfaces vxlan'): + if conf.exists('interfaces vxlan ' + tmp + ' link ' + intf): + raise ConfigError('can not enslave interface {} which belongs to ' \ + 'vxlan {}'.format(intf, tmp)) + + + if bond['primary']: + if bond['primary'] not in bond['member']: + raise ConfigError('primary interface must be a member interface of {}' \ + .format(bond['intf'])) + + if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']: + raise ConfigError('primary interface only works for mode active-backup, ' \ + 'transmit-load-balance or adaptive-load-balance') + + if bond['arp_mon_intvl'] > 0: + if bond['mode'] in ['802.3ad', 'balance-tlb', 'balance-alb']: + raise ConfigError('ARP link monitoring does not work for mode 802.3ad, ' \ + 'transmit-load-balance or adaptive-load-balance') + + return None + + +def generate(bond): + return None + + +def apply(bond): + b = BondIf(bond['intf']) + + if bond['deleted']: + # delete interface + b.remove() + else: + # Some parameters can not be changed when the bond is up. + # Always disable the bond prior changing anything + b.set_state('down') + + # The bonding mode can not be changed when there are interfaces enslaved + # to this bond, thus we will free all interfaces from the bond first! + for intf in b.get_slaves(): + b.del_port(intf) + + # ARP link monitoring frequency, reset miimon when arp-montior is inactive + # this is done inside BondIf automatically + b.set_arp_interval(bond['arp_mon_intvl']) + + # 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 adresses prior adding new ones, this will remove addresses + # added manually 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, b.get_arp_ip_target().split())) + for addr in arp_tgt_addr: + b.set_arp_ip_target('-' + addr) + + # Add configured ARP target addresses + for addr in bond['arp_mon_tgt']: + b.set_arp_ip_target('+' + addr) + + # update interface description used e.g. within SNMP + b.set_alias(bond['description']) + + # get DHCP config dictionary and update values + opt = b.get_dhcp_options() + + if bond['dhcp_client_id']: + opt['client_id'] = bond['dhcp_client_id'] + + if bond['dhcp_hostname']: + opt['hostname'] = bond['dhcp_hostname'] + + if bond['dhcp_vendor_class_id']: + opt['vendor_class_id'] = bond['dhcp_vendor_class_id'] + + # store DHCP config dictionary - used later on when addresses are aquired + b.set_dhcp_options(opt) + + # get DHCPv6 config dictionary and update values + opt = b.get_dhcpv6_options() + + if bond['dhcpv6_prm_only']: + opt['dhcpv6_prm_only'] = True + + if bond['dhcpv6_temporary']: + opt['dhcpv6_temporary'] = True + + # store DHCPv6 config dictionary - used later on when addresses are aquired + b.set_dhcpv6_options(opt) + + # ignore link state changes + b.set_link_detect(bond['disable_link_detect']) + # Bonding transmit hash policy + b.set_hash_policy(bond['hash_policy']) + # configure ARP cache timeout in milliseconds + b.set_arp_cache_tmo(bond['ip_arp_cache_tmo']) + # Enable proxy-arp on this interface + b.set_proxy_arp(bond['ip_proxy_arp']) + # Enable private VLAN proxy ARP on this interface + b.set_proxy_arp_pvlan(bond['ip_proxy_arp_pvlan']) + + # Change interface MAC address + if bond['mac']: + b.set_mac(bond['mac']) + + # Bonding policy + b.set_mode(bond['mode']) + # Maximum Transmission Unit (MTU) + b.set_mtu(bond['mtu']) + + # Primary device interface + if bond['primary']: + b.set_primary(bond['primary']) + + # Add (enslave) interfaces to bond + for intf in bond['member']: + b.add_port(intf) + + # As the bond interface is always disabled first when changing + # parameters we will only re-enable the interface if it is not + # administratively disabled + if not bond['disable']: + b.set_state('up') + + # Configure interface address(es) + # - not longer required addresses get removed first + # - newly addresses will be added second + for addr in bond['address_remove']: + b.del_addr(addr) + for addr in bond['address']: + b.add_addr(addr) + + # remove no longer required service VLAN interfaces (vif-s) + for vif_s in bond['vif_s_remove']: + b.del_vlan(vif_s) + + # create service VLAN interfaces (vif-s) + for vif_s in bond['vif_s']: + s_vlan = b.add_vlan(vif_s['id'], ethertype=vif_s['ethertype']) + apply_vlan_config(s_vlan, vif_s) + + # remove no longer required client VLAN interfaces (vif-c) + # on lower service VLAN interface + for vif_c in vif_s['vif_c_remove']: + s_vlan.del_vlan(vif_c) + + # create client VLAN interfaces (vif-c) + # on lower service VLAN interface + for vif_c in vif_s['vif_c']: + c_vlan = s_vlan.add_vlan(vif_c['id']) + apply_vlan_config(c_vlan, vif_c) + + # remove no longer required VLAN interfaces (vif) + for vif in bond['vif_remove']: + b.del_vlan(vif) + + # create VLAN interfaces (vif) + for vif in bond['vif']: + vlan = b.add_vlan(vif['id']) + apply_vlan_config(vlan, vif) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py new file mode 100755 index 000000000..70bf4f528 --- /dev/null +++ b/src/conf_mode/interfaces-bridge.py @@ -0,0 +1,307 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os + +from copy import deepcopy +from sys import exit +from netifaces import interfaces + +from vyos.ifconfig import BridgeIf, STPIf +from vyos.configdict import list_diff +from vyos.config import Config +from vyos import ConfigError + +default_config_data = { + 'address': [], + 'address_remove': [], + 'aging': 300, + 'arp_cache_tmo': 30, + 'description': '', + 'deleted': False, + 'dhcp_client_id': '', + 'dhcp_hostname': '', + 'dhcp_vendor_class_id': '', + 'dhcpv6_prm_only': False, + 'dhcpv6_temporary': False, + 'disable': False, + 'disable_link_detect': 1, + 'forwarding_delay': 14, + 'hello_time': 2, + 'igmp_querier': 0, + 'intf': '', + 'mac' : '', + 'max_age': 20, + 'member': [], + 'member_remove': [], + 'priority': 32768, + 'stp': 0 +} + +def get_config(): + bridge = deepcopy(default_config_data) + conf = Config() + + # determine tagNode instance + try: + bridge['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + except KeyError as E: + print("Interface not specified") + + # Check if bridge has been removed + if not conf.exists('interfaces bridge ' + bridge['intf']): + bridge['deleted'] = True + return bridge + + # set new configuration level + conf.set_level('interfaces bridge ' + bridge['intf']) + + # retrieve configured interface addresses + if conf.exists('address'): + bridge['address'] = conf.return_values('address') + + # Determine interface addresses (currently effective) - to determine which + # address is no longer valid and needs to be removed + eff_addr = conf.return_effective_values('address') + bridge['address_remove'] = list_diff(eff_addr, bridge['address']) + + # retrieve aging - how long addresses are retained + if conf.exists('aging'): + bridge['aging'] = int(conf.return_value('aging')) + + # retrieve interface description + if conf.exists('description'): + bridge['description'] = conf.return_value('description') + + # get DHCP client identifier + if conf.exists('dhcp-options client-id'): + bridge['dhcp_client_id'] = conf.return_value('dhcp-options client-id') + + # DHCP client host name (overrides the system host name) + if conf.exists('dhcp-options host-name'): + bridge['dhcp_hostname'] = conf.return_value('dhcp-options host-name') + + # DHCP client vendor identifier + if conf.exists('dhcp-options vendor-class-id'): + bridge['dhcp_vendor_class_id'] = conf.return_value('dhcp-options vendor-class-id') + + # DHCPv6 only acquire config parameters, no address + if conf.exists('dhcpv6-options parameters-only'): + bridge['dhcpv6_prm_only'] = True + + # DHCPv6 temporary IPv6 address + if conf.exists('dhcpv6-options temporary'): + bridge['dhcpv6_temporary'] = True + + # Disable this bridge interface + if conf.exists('disable'): + bridge['disable'] = True + + # Ignore link state changes + if conf.exists('disable-link-detect'): + bridge['disable_link_detect'] = 2 + + # Forwarding delay + if conf.exists('forwarding-delay'): + bridge['forwarding_delay'] = int(conf.return_value('forwarding-delay')) + + # Hello packet advertisment interval + if conf.exists('hello-time'): + bridge['hello_time'] = int(conf.return_value('hello-time')) + + # Enable Internet Group Management Protocol (IGMP) querier + if conf.exists('igmp querier'): + bridge['igmp_querier'] = 1 + + # ARP cache entry timeout in seconds + if conf.exists('ip arp-cache-timeout'): + bridge['arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) + + # Media Access Control (MAC) address + if conf.exists('mac'): + bridge['mac'] = conf.return_value('mac') + + # Interval at which neighbor bridges are removed + if conf.exists('max-age'): + bridge['max_age'] = int(conf.return_value('max-age')) + + # Determine bridge member interface (currently configured) + for intf in conf.list_nodes('member interface'): + # cost and priority initialized with linux defaults + # by reading /sys/devices/virtual/net/br0/brif/eth2/{path_cost,priority} + # after adding interface to bridge after reboot + iface = { + 'name': intf, + 'cost': 100, + 'priority': 32 + } + + if conf.exists('member interface {} cost'.format(intf)): + iface['cost'] = int(conf.return_value('member interface {} cost'.format(intf))) + + if conf.exists('member interface {} priority'.format(intf)): + iface['priority'] = int(conf.return_value('member interface {} priority'.format(intf))) + + bridge['member'].append(iface) + + # Determine bridge member interface (currently effective) - to determine which + # interfaces is no longer assigend to the bridge and thus can be removed + eff_intf = conf.list_effective_nodes('member interface') + act_intf = conf.list_nodes('member interface') + bridge['member_remove'] = list_diff(eff_intf, act_intf) + + # Priority for this bridge + if conf.exists('priority'): + bridge['priority'] = int(conf.return_value('priority')) + + # Enable spanning tree protocol + if conf.exists('stp'): + bridge['stp'] = 1 + + return bridge + +def verify(bridge): + if bridge['dhcpv6_prm_only'] and bridge['dhcpv6_temporary']: + raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') + + conf = Config() + for br in conf.list_nodes('interfaces bridge'): + # it makes no sense to verify ourself in this case + if br == bridge['intf']: + continue + + for intf in bridge['member']: + tmp = conf.list_nodes('interfaces bridge {} member interface'.format(br)) + if intf['name'] in tmp: + raise ConfigError('Interface "{}" belongs to bridge "{}" and can not be enslaved.'.format(intf['name'], bridge['intf'])) + + # the interface must exist prior adding it to a bridge + for intf in bridge['member']: + if intf['name'] not in interfaces(): + raise ConfigError('Can not add non existing interface "{}" to bridge "{}"'.format(intf['name'], bridge['intf'])) + + # bridge members are not allowed to be bond members, too + for intf in bridge['member']: + for bond in conf.list_nodes('interfaces bonding'): + if conf.exists('interfaces bonding ' + bond + ' member interface'): + if intf['name'] in conf.return_values('interfaces bonding ' + bond + ' member interface'): + raise ConfigError('Interface {} belongs to bond {}, can not add it to {}'.format(intf['name'], bond, bridge['intf'])) + + return None + +def generate(bridge): + return None + +def apply(bridge): + br = BridgeIf(bridge['intf']) + + if bridge['deleted']: + # delete interface + br.remove() + else: + # enable interface + br.set_state('up') + # set ageing time + br.set_ageing_time(bridge['aging']) + # set bridge forward delay + br.set_forward_delay(bridge['forwarding_delay']) + # set hello time + br.set_hello_time(bridge['hello_time']) + # set max message age + br.set_max_age(bridge['max_age']) + # set bridge priority + br.set_priority(bridge['priority']) + # turn stp on/off + br.set_stp(bridge['stp']) + # enable or disable IGMP querier + br.set_multicast_querier(bridge['igmp_querier']) + # update interface description used e.g. within SNMP + br.set_alias(bridge['description']) + + # get DHCP config dictionary and update values + opt = br.get_dhcp_options() + + if bridge['dhcp_client_id']: + opt['client_id'] = bridge['dhcp_client_id'] + + if bridge['dhcp_hostname']: + opt['hostname'] = bridge['dhcp_hostname'] + + if bridge['dhcp_vendor_class_id']: + opt['vendor_class_id'] = bridge['dhcp_vendor_class_id'] + + # store DHCPv6 config dictionary - used later on when addresses are aquired + br.set_dhcp_options(opt) + + # get DHCPv6 config dictionary and update values + opt = br.get_dhcpv6_options() + + if bridge['dhcpv6_prm_only']: + opt['dhcpv6_prm_only'] = True + + if bridge['dhcpv6_temporary']: + opt['dhcpv6_temporary'] = True + + # store DHCPv6 config dictionary - used later on when addresses are aquired + br.set_dhcpv6_options(opt) + + # Change interface MAC address + if bridge['mac']: + br.set_mac(bridge['mac']) + + # remove interface from bridge + for intf in bridge['member_remove']: + br.del_port( intf['name'] ) + + # add interfaces to bridge + for member in bridge['member']: + br.add_port(member['name']) + + # up/down interface + if bridge['disable']: + br.set_state('down') + + # Configure interface address(es) + # - not longer required addresses get removed first + # - newly addresses will be added second + for addr in bridge['address_remove']: + br.del_addr(addr) + for addr in bridge['address']: + br.add_addr(addr) + + # configure additional bridge member options + for member in bridge['member']: + i = STPIf(member['name']) + # configure ARP cache timeout + i.set_arp_cache_tmo(bridge['arp_cache_tmo']) + # ignore link state changes + i.set_link_detect(bridge['disable_link_detect']) + # set bridge port path cost + i.set_path_cost(member['cost']) + # set bridge port path priority + i.set_path_priority(member['priority']) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py new file mode 100755 index 000000000..eb0145f65 --- /dev/null +++ b/src/conf_mode/interfaces-dummy.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os + +from copy import deepcopy +from sys import exit + +from vyos.ifconfig import DummyIf +from vyos.configdict import list_diff +from vyos.config import Config +from vyos import ConfigError + +default_config_data = { + 'address': [], + 'address_remove': [], + 'deleted': False, + 'description': '', + 'disable': False, + 'intf': '' +} + +def get_config(): + dummy = deepcopy(default_config_data) + conf = Config() + + # determine tagNode instance + try: + dummy['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + except KeyError as E: + print("Interface not specified") + + # Check if interface has been removed + if not conf.exists('interfaces dummy ' + dummy['intf']): + dummy['deleted'] = True + return dummy + + # set new configuration level + conf.set_level('interfaces dummy ' + dummy['intf']) + + # retrieve configured interface addresses + if conf.exists('address'): + dummy['address'] = conf.return_values('address') + + # retrieve interface description + if conf.exists('description'): + dummy['description'] = conf.return_value('description') + + # Disable this interface + if conf.exists('disable'): + dummy['disable'] = True + + # Determine interface addresses (currently effective) - to determine which + # address is no longer valid and needs to be removed from the interface + eff_addr = conf.return_effective_values('address') + act_addr = conf.return_values('address') + dummy['address_remove'] = list_diff(eff_addr, act_addr) + + return dummy + +def verify(dummy): + return None + +def generate(dummy): + return None + +def apply(dummy): + d = DummyIf(dummy['intf']) + + # Remove dummy interface + if dummy['deleted']: + d.remove() + else: + # update interface description used e.g. within SNMP + d.set_alias(dummy['description']) + + # Configure interface address(es) + # - not longer required addresses get removed first + # - newly addresses will be added second + for addr in dummy['address_remove']: + d.del_addr(addr) + for addr in dummy['address']: + d.add_addr(addr) + + # disable interface on demand + if dummy['disable']: + d.set_state('down') + else: + d.set_state('up') + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py new file mode 100755 index 000000000..cd40aff3e --- /dev/null +++ b/src/conf_mode/interfaces-ethernet.py @@ -0,0 +1,452 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os + +from copy import deepcopy +from sys import exit + +from vyos.ifconfig import EthernetIf, VLANIf +from vyos.configdict import list_diff, vlan_to_dict +from vyos.config import Config +from vyos import ConfigError + +default_config_data = { + 'address': [], + 'address_remove': [], + 'description': '', + 'deleted': False, + 'dhcp_client_id': '', + 'dhcp_hostname': '', + 'dhcp_vendor_class_id': '', + 'dhcpv6_prm_only': False, + 'dhcpv6_temporary': False, + 'disable': False, + 'disable_link_detect': 1, + 'duplex': 'auto', + 'flow_control': 'on', + 'hw_id': '', + 'ip_arp_cache_tmo': 30, + 'ip_proxy_arp': 0, + 'ip_proxy_arp_pvlan': 0, + 'intf': '', + 'mac': '', + 'mtu': 1500, + 'offload_gro': 'off', + 'offload_gso': 'off', + 'offload_sg': 'off', + 'offload_tso': 'off', + 'offload_ufo': 'off', + 'speed': 'auto', + 'vif_s': [], + 'vif_s_remove': [], + 'vif': [], + 'vif_remove': [] +} + + +def apply_vlan_config(vlan, config): + """ + Generic function to apply a VLAN configuration from a dictionary + to a VLAN interface + """ + + if type(vlan) != type(VLANIf("lo")): + raise TypeError() + + # get DHCP config dictionary and update values + opt = vlan.get_dhcp_options() + + if config['dhcp_client_id']: + opt['client_id'] = config['dhcp_client_id'] + + if config['dhcp_hostname']: + opt['hostname'] = config['dhcp_hostname'] + + if config['dhcp_vendor_class_id']: + opt['vendor_class_id'] = config['dhcp_vendor_class_id'] + + # store DHCP config dictionary - used later on when addresses are aquired + vlan.set_dhcp_options(opt) + + # get DHCPv6 config dictionary and update values + opt = vlan.get_dhcpv6_options() + + if config['dhcpv6_prm_only']: + opt['dhcpv6_prm_only'] = True + + if config['dhcpv6_temporary']: + opt['dhcpv6_temporary'] = True + + # store DHCPv6 config dictionary - used later on when addresses are aquired + vlan.set_dhcpv6_options(opt) + + # update interface description used e.g. within SNMP + vlan.set_alias(config['description']) + # ignore link state changes + vlan.set_link_detect(config['disable_link_detect']) + # Maximum Transmission Unit (MTU) + vlan.set_mtu(config['mtu']) + # Change VLAN interface MAC address + if config['mac']: + vlan.set_mac(config['mac']) + + # enable/disable VLAN interface + if config['disable']: + vlan.set_state('down') + else: + vlan.set_state('up') + + # Configure interface address(es) + # - not longer required addresses get removed first + # - newly addresses will be added second + for addr in config['address_remove']: + vlan.del_addr(addr) + for addr in config['address']: + vlan.add_addr(addr) + + +def get_config(): + eth = deepcopy(default_config_data) + conf = Config() + + # determine tagNode instance + try: + eth['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + except KeyError as E: + print("Interface not specified") + + # check if ethernet interface has been removed + cfg_base = 'interfaces ethernet ' + eth['intf'] + if not conf.exists(cfg_base): + eth['deleted'] = True + # we can not bail out early as ethernet interface can not be removed + # Kernel will complain with: RTNETLINK answers: Operation not supported. + # Thus we need to remove individual settings + return eth + + # set new configuration level + conf.set_level(cfg_base) + + # retrieve configured interface addresses + if conf.exists('address'): + eth['address'] = conf.return_values('address') + + # get interface addresses (currently effective) - to determine which + # address is no longer valid and needs to be removed + eff_addr = conf.return_effective_values('address') + eth['address_remove'] = list_diff(eff_addr, eth['address']) + + # retrieve interface description + if conf.exists('description'): + eth['description'] = conf.return_value('description') + + # get DHCP client identifier + if conf.exists('dhcp-options client-id'): + eth['dhcp_client_id'] = conf.return_value('dhcp-options client-id') + + # DHCP client host name (overrides the system host name) + if conf.exists('dhcp-options host-name'): + eth['dhcp_hostname'] = conf.return_value('dhcp-options host-name') + + # DHCP client vendor identifier + if conf.exists('dhcp-options vendor-class-id'): + eth['dhcp_vendor_class_id'] = conf.return_value('dhcp-options vendor-class-id') + + # DHCPv6 only acquire config parameters, no address + if conf.exists('dhcpv6-options parameters-only'): + eth['dhcpv6_prm_only'] = True + + # DHCPv6 temporary IPv6 address + if conf.exists('dhcpv6-options temporary'): + eth['dhcpv6_temporary'] = True + + # ignore link state changes + if conf.exists('disable-link-detect'): + eth['disable_link_detect'] = 2 + + # disable ethernet flow control (pause frames) + if conf.exists('disable-flow-control'): + eth['flow_control'] = 'off' + + # retrieve real hardware address + if conf.exists('hw-id'): + eth['hw_id'] = conf.return_value('hw-id') + + # disable interface + if conf.exists('disable'): + eth['disable'] = True + + # interface duplex + if conf.exists('duplex'): + eth['duplex'] = conf.return_value('duplex') + + # ARP cache entry timeout in seconds + if conf.exists('ip arp-cache-timeout'): + eth['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) + + # Enable proxy-arp on this interface + if conf.exists('ip enable-proxy-arp'): + eth['ip_proxy_arp'] = 1 + + # Enable private VLAN proxy ARP on this interface + if conf.exists('ip proxy-arp-pvlan'): + eth['ip_proxy_arp_pvlan'] = 1 + + # Media Access Control (MAC) address + if conf.exists('mac'): + eth['mac'] = conf.return_value('mac') + + # Maximum Transmission Unit (MTU) + if conf.exists('mtu'): + eth['mtu'] = int(conf.return_value('mtu')) + + # GRO (generic receive offload) + if conf.exists('offload-options generic-receive'): + eth['offload_gro'] = conf.return_value('offload-options generic-receive') + + # GSO (generic segmentation offload) + if conf.exists('offload-options generic-segmentation'): + eth['offload_gso'] = conf.return_value('offload-options generic-segmentation') + + # scatter-gather option + if conf.exists('offload-options scatter-gather'): + eth['offload_sg'] = conf.return_value('offload-options scatter-gather') + + # TSO (TCP segmentation offloading) + if conf.exists('offload-options tcp-segmentation'): + eth['offload_tso'] = conf.return_value('offload-options tcp-segmentation') + + # UDP fragmentation offloading + if conf.exists('offload-options udp-fragmentation'): + eth['offload_ufo'] = conf.return_value('offload-options udp-fragmentation') + + # interface speed + if conf.exists('speed'): + eth['speed'] = conf.return_value('speed') + + # re-set configuration level to parse new nodes + conf.set_level(cfg_base) + # get vif-s interfaces (currently effective) - to determine which vif-s + # interface is no longer present and needs to be removed + eff_intf = conf.list_effective_nodes('vif-s') + act_intf = conf.list_nodes('vif-s') + eth['vif_s_remove'] = list_diff(eff_intf, act_intf) + + if conf.exists('vif-s'): + for vif_s in conf.list_nodes('vif-s'): + # set config level to vif-s interface + conf.set_level(cfg_base + ' vif-s ' + vif_s) + eth['vif_s'].append(vlan_to_dict(conf)) + + # re-set configuration level to parse new nodes + conf.set_level(cfg_base) + # Determine vif interfaces (currently effective) - to determine which + # vif interface is no longer present and needs to be removed + eff_intf = conf.list_effective_nodes('vif') + act_intf = conf.list_nodes('vif') + eth['vif_remove'] = list_diff(eff_intf, act_intf) + + if conf.exists('vif'): + for vif in conf.list_nodes('vif'): + # set config level to vif interface + conf.set_level(cfg_base + ' vif ' + vif) + eth['vif'].append(vlan_to_dict(conf)) + + return eth + + +def verify(eth): + if eth['deleted']: + return None + + if eth['speed'] == 'auto': + if eth['duplex'] != 'auto': + raise ConfigError('If speed is hardcoded, duplex must be hardcoded, too') + + if eth['duplex'] == 'auto': + if eth['speed'] != 'auto': + raise ConfigError('If duplex is hardcoded, speed must be hardcoded, too') + + if eth['dhcpv6_prm_only'] and eth['dhcpv6_temporary']: + raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') + + conf = Config() + # some options can not be changed when interface is enslaved to a bond + for bond in conf.list_nodes('interfaces bonding'): + if conf.exists('interfaces bonding ' + bond + ' member interface'): + bond_member = conf.return_values('interfaces bonding ' + bond + ' member interface') + if eth['intf'] in bond_member: + if eth['address']: + raise ConfigError('Can not assign address to interface {} which is a member of {}').format(eth['intf'], bond) + + # DHCPv6 parameters-only and temporary address are mutually exclusive + for vif_s in eth['vif_s']: + if vif_s['dhcpv6_prm_only'] and vif_s['dhcpv6_temporary']: + raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') + + for vif_c in vif_s['vif_c']: + if vif_c['dhcpv6_prm_only'] and vif_c['dhcpv6_temporary']: + raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') + + for vif in eth['vif']: + if vif['dhcpv6_prm_only'] and vif['dhcpv6_temporary']: + raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') + + return None + +def generate(eth): + return None + +def apply(eth): + e = EthernetIf(eth['intf']) + if eth['deleted']: + # delete interface + e.remove() + else: + # update interface description used e.g. within SNMP + e.set_alias(eth['description']) + + # get DHCP config dictionary and update values + opt = e.get_dhcp_options() + + if eth['dhcp_client_id']: + opt['client_id'] = eth['dhcp_client_id'] + + if eth['dhcp_hostname']: + opt['hostname'] = eth['dhcp_hostname'] + + if eth['dhcp_vendor_class_id']: + opt['vendor_class_id'] = eth['dhcp_vendor_class_id'] + + # store DHCP config dictionary - used later on when addresses are aquired + e.set_dhcp_options(opt) + + # get DHCPv6 config dictionary and update values + opt = e.get_dhcpv6_options() + + if eth['dhcpv6_prm_only']: + opt['dhcpv6_prm_only'] = True + + if eth['dhcpv6_temporary']: + opt['dhcpv6_temporary'] = True + + # store DHCPv6 config dictionary - used later on when addresses are aquired + e.set_dhcpv6_options(opt) + + # ignore link state changes + e.set_link_detect(eth['disable_link_detect']) + # disable ethernet flow control (pause frames) + e.set_flow_control(eth['flow_control']) + # configure ARP cache timeout in milliseconds + e.set_arp_cache_tmo(eth['ip_arp_cache_tmo']) + # Enable proxy-arp on this interface + e.set_proxy_arp(eth['ip_proxy_arp']) + # Enable private VLAN proxy ARP on this interface + e.set_proxy_arp_pvlan(eth['ip_proxy_arp_pvlan']) + + # Change interface MAC address - re-set to real hardware address (hw-id) + # if custom mac is removed + if eth['mac']: + e.set_mac(eth['mac']) + else: + e.set_mac(eth['hw_id']) + + # Maximum Transmission Unit (MTU) + e.set_mtu(eth['mtu']) + + # GRO (generic receive offload) + e.set_gro(eth['offload_gro']) + + # GSO (generic segmentation offload) + e.set_gso(eth['offload_gso']) + + # scatter-gather option + e.set_sg(eth['offload_sg']) + + # TSO (TCP segmentation offloading) + e.set_tso(eth['offload_tso']) + + # UDP fragmentation offloading + e.set_ufo(eth['offload_ufo']) + + # Set physical interface speed and duplex + e.set_speed_duplex(eth['speed'], eth['duplex']) + + # Enable/Disable interface + if eth['disable']: + e.set_state('down') + else: + e.set_state('up') + + # Configure interface address(es) + # - not longer required addresses get removed first + # - newly addresses will be added second + for addr in eth['address_remove']: + e.del_addr(addr) + for addr in eth['address']: + e.add_addr(addr) + + # remove no longer required service VLAN interfaces (vif-s) + for vif_s in eth['vif_s_remove']: + e.del_vlan(vif_s) + + # create service VLAN interfaces (vif-s) + for vif_s in eth['vif_s']: + s_vlan = e.add_vlan(vif_s['id'], ethertype=vif_s['ethertype']) + apply_vlan_config(s_vlan, vif_s) + + # remove no longer required client VLAN interfaces (vif-c) + # on lower service VLAN interface + for vif_c in vif_s['vif_c_remove']: + s_vlan.del_vlan(vif_c) + + # create client VLAN interfaces (vif-c) + # on lower service VLAN interface + for vif_c in vif_s['vif_c']: + c_vlan = s_vlan.add_vlan(vif_c['id']) + apply_vlan_config(c_vlan, vif_c) + + # remove no longer required VLAN interfaces (vif) + for vif in eth['vif_remove']: + e.del_vlan(vif) + + # create VLAN interfaces (vif) + for vif in eth['vif']: + # QoS priority mapping can only be set during interface creation + # so we delete the interface first if required. + if vif['egress_qos_changed'] or vif['ingress_qos_changed']: + try: + # on system bootup the above condition is true but the interface + # does not exists, which throws an exception, but that's legal + e.del_vlan(vif['id']) + except: + pass + + vlan = e.add_vlan(vif['id'], ingress_qos=vif['ingress_qos'], egress_qos=vif['egress_qos']) + apply_vlan_config(vlan, vif) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/interfaces-loopback.py b/src/conf_mode/interfaces-loopback.py new file mode 100755 index 000000000..10722d137 --- /dev/null +++ b/src/conf_mode/interfaces-loopback.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os + +from sys import exit +from copy import deepcopy + +from vyos.ifconfig import LoopbackIf +from vyos.configdict import list_diff +from vyos.config import Config +from vyos import ConfigError + +default_config_data = { + 'address': [], + 'address_remove': [], + 'deleted': False, + 'description': '', +} + + +def get_config(): + loopback = deepcopy(default_config_data) + conf = Config() + + # determine tagNode instance + try: + loopback['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + except KeyError as E: + print("Interface not specified") + + # Check if interface has been removed + if not conf.exists('interfaces loopback ' + loopback['intf']): + loopback['deleted'] = True + + # set new configuration level + conf.set_level('interfaces loopback ' + loopback['intf']) + + # retrieve configured interface addresses + if conf.exists('address'): + loopback['address'] = conf.return_values('address') + + # retrieve interface description + if conf.exists('description'): + loopback['description'] = conf.return_value('description') + + # Determine interface addresses (currently effective) - to determine which + # address is no longer valid and needs to be removed from the interface + eff_addr = conf.return_effective_values('address') + act_addr = conf.return_values('address') + loopback['address_remove'] = list_diff(eff_addr, act_addr) + + return loopback + +def verify(loopback): + return None + +def generate(loopback): + return None + +def apply(loopback): + l = LoopbackIf(loopback['intf']) + if loopback['deleted']: + l.remove() + else: + # update interface description used e.g. within SNMP + l.set_alias(loopback['description']) + + # Configure interface address(es) + # - not longer required addresses get removed first + # - newly addresses will be added second + for addr in loopback['address_remove']: + l.del_addr(addr) + for addr in loopback['address']: + l.add_addr(addr) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py new file mode 100755 index 000000000..5345bf7a2 --- /dev/null +++ b/src/conf_mode/interfaces-openvpn.py @@ -0,0 +1,960 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import re +import sys +import stat +import jinja2 + +from copy import deepcopy +from grp import getgrnam +from ipaddress import ip_address,ip_network,IPv4Interface +from netifaces import interfaces +from psutil import pid_exists +from pwd import getpwnam +from subprocess import Popen, PIPE +from time import sleep + +from vyos import ConfigError +from vyos.config import Config +from vyos.ifconfig import Interface +from vyos.validate import is_addr_assigned + +user = 'openvpn' +group = 'openvpn' + +# Please be careful if you edit the template. +config_tmpl = """ +### Autogenerated by interfaces-openvpn.py ### +# +# See https://community.openvpn.net/openvpn/wiki/Openvpn24ManPage +# for individual keyword definition + +{% if description %} +# {{ description }} +{% endif %} + +verb 3 +status /opt/vyatta/etc/openvpn/status/{{ intf }}.status 30 +writepid /var/run/openvpn/{{ intf }}.pid +daemon openvpn-{{ intf }} + +dev-type {{ type }} +dev {{ intf }} +user {{ uid }} +group {{ gid }} +persist-key +iproute /usr/libexec/vyos/system/unpriv-ip + +proto {% if 'tcp-active' in protocol -%}tcp-client{% elif 'tcp-passive' in protocol -%}tcp-server{% else %}udp{% endif %} + +{%- if local_host %} +local {{ local_host }} +{% endif %} + +{%- if local_port %} +lport {{ local_port }} +{% endif %} + +{%- if remote_port %} +rport {{ remote_port }} +{% endif %} + +{%- if remote_host %} +{% for remote in remote_host -%} +remote {{ remote }} +{% endfor -%} +{% endif %} + +{%- if shared_secret_file %} +secret {{ shared_secret_file }} +{% endif %} + +{%- if persistent_tunnel %} +persist-tun +{% endif %} + +{%- if mode %} +{%- if 'client' in mode %} +# +# OpenVPN Client mode +# +client +nobind +{%- elif 'server' in mode %} +# +# OpenVPN Server mode +# +mode server +tls-server +keepalive {{ ping_interval }} {{ ping_restart }} +management /tmp/openvpn-mgmt-intf unix + +{%- if server_topology %} +topology {% if 'point-to-point' in server_topology %}p2p{% else %}subnet{% endif %} +{% endif %} + +{% for ns in server_dns_nameserver -%} +push "dhcp-option DNS {{ ns }}" +{% endfor -%} + +{% for route in server_push_route -%} +push "route {{ route }}" +{% endfor -%} + +{%- if server_domain %} +push "dhcp-option DOMAIN {{ server_domain }}" +{% endif %} + +{%- if server_max_conn %} +max-clients {{ server_max_conn }} +{% endif %} + +{%- if bridge_member %} +server-bridge nogw +{%- else %} +server {{ server_subnet }} +{% endif %} + +{%- if server_reject_unconfigured %} +ccd-exclusive +{% endif %} + +{%- else %} +# +# OpenVPN site-2-site mode +# +ping {{ ping_interval }} +ping-restart {{ ping_restart }} + +{%- if local_address_subnet %} +ifconfig {{ local_address }} {{ local_address_subnet }} +{% elif remote_address %} +ifconfig {{ local_address }} {{ remote_address }} +{% endif %} + +{% endif %} +{% endif %} + +{%- if tls_ca_cert %} +ca {{ tls_ca_cert }} +{% endif %} + +{%- if tls_cert %} +cert {{ tls_cert }} +{% endif %} + +{%- if tls_key %} +key {{ tls_key }} +{% endif %} + +{%- if tls_crl %} +crl-verify {{ tls_crl }} +{% endif %} + +{%- if tls_version_min %} +tls-version-min {{tls_version_min}} +{% endif %} + +{%- if tls_dh %} +dh {{ tls_dh }} +{% endif %} + +{%- if tls_auth %} +tls-auth {{tls_auth}} +{% endif %} + +{%- if 'active' in tls_role %} +tls-client +{%- elif 'passive' in tls_role %} +tls-server +{% endif %} + +{%- if redirect_gateway %} +push "redirect-gateway {{ redirect_gateway }}" +{% endif %} + +{%- if compress_lzo %} +compress lzo +{% endif %} + +{%- if hash %} +auth {{ hash }} +{% endif %} + +{%- if encryption %} +{%- if 'des' in encryption %} +cipher des-cbc +{%- elif '3des' in encryption %} +cipher des-ede3-cbc +{%- elif 'bf128' in encryption %} +cipher bf-cbc +keysize 128 +{%- elif 'bf256' in encryption %} +cipher bf-cbc +keysize 25 +{%- elif 'aes128gcm' in encryption %} +cipher aes-128-gcm +{%- elif 'aes128' in encryption %} +cipher aes-128-cbc +{%- elif 'aes192gcm' in encryption %} +cipher aes-192-gcm +{%- elif 'aes192' in encryption %} +cipher aes-192-cbc +{%- elif 'aes256gcm' in encryption %} +cipher aes-256-gcm +{%- elif 'aes256' in encryption %} +cipher aes-256-cbc +{% endif %} +{% endif %} + +{%- if auth %} +auth-user-pass /tmp/openvpn-{{ intf }}-pw +auth-retry nointeract +{% endif %} + +{%- if client %} +client-config-dir /opt/vyatta/etc/openvpn/ccd/{{ intf }} +{% endif %} + +# DEPRECATED This option will be removed in OpenVPN 2.5 +# Until OpenVPN v2.3 the format of the X.509 Subject fields was formatted like this: +# /C=US/L=Somewhere/CN=John Doe/emailAddress=john@example.com In addition the old +# behaviour was to remap any character other than alphanumeric, underscore ('_'), +# dash ('-'), dot ('.'), and slash ('/') to underscore ('_'). The X.509 Subject +# string as returned by the tls_id environmental variable, could additionally +# contain colon (':') or equal ('='). When using the --compat-names option, this +# old formatting and remapping will be re-enabled again. This is purely implemented +# for compatibility reasons when using older plug-ins or scripts which does not +# handle the new formatting or UTF-8 characters. +# +# See https://phabricator.vyos.net/T1512 +compat-names + +{% for option in options -%} +{{ option }} +{% endfor -%} +""" + +client_tmpl = """ +### Autogenerated by interfaces-openvpn.py ### + +ifconfig-push {{ ip }} {{ remote_netmask }} +{% for route in push_route -%} +push "route {{ route }}" +{% endfor -%} + +{% for net in subnet -%} +iroute {{ net }} +{% endfor -%} + +{%- if disable %} +disable +{% endif %} +""" + +default_config_data = { + 'address': [], + 'auth_user': '', + 'auth_pass': '', + 'auth': False, + 'bridge_member': [], + 'compress_lzo': False, + 'deleted': False, + 'description': '', + 'disable': False, + 'encryption': '', + 'hash': '', + 'intf': '', + 'ping_restart': '60', + 'ping_interval': '10', + 'local_address': '', + 'local_address_subnet': '', + 'local_host': '', + 'local_port': '', + 'mode': '', + 'options': [], + 'persistent_tunnel': False, + 'protocol': '', + 'redirect_gateway': '', + 'remote_address': '', + 'remote_host': [], + 'remote_port': '', + 'client': [], + 'server_domain': '', + 'server_max_conn': '', + 'server_dns_nameserver': [], + 'server_push_route': [], + 'server_reject_unconfigured': False, + 'server_subnet': '', + 'server_topology': '', + 'shared_secret_file': '', + 'tls': False, + 'tls_auth': '', + 'tls_ca_cert': '', + 'tls_cert': '', + 'tls_crl': '', + 'tls_dh': '', + 'tls_key': '', + 'tls_role': '', + 'tls_version_min': '', + 'type': 'tun', + 'uid': user, + 'gid': group, +} + +def subprocess_cmd(command): + p = Popen(command, stdout=PIPE, shell=True) + p.communicate() + +def get_config_name(intf): + cfg_file = r'/opt/vyatta/etc/openvpn/openvpn-{}.conf'.format(intf) + return cfg_file + +def openvpn_mkdir(directory): + # create directory on demand + if not os.path.exists(directory): + os.mkdir(directory) + + # fix permissions - corresponds to mode 755 + os.chmod(directory, stat.S_IRWXU|stat.S_IRGRP|stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH) + uid = getpwnam(user).pw_uid + gid = getgrnam(group).gr_gid + os.chown(directory, uid, gid) + +def fixup_permission(filename, permission=stat.S_IRUSR): + """ + Check if the given file exists and change ownershit to root/vyattacfg + and appripriate file access permissions - default is user and group readable + """ + if os.path.isfile(filename): + os.chmod(filename, permission) + + # make file owned by root / vyattacfg + uid = getpwnam('root').pw_uid + gid = getgrnam('vyattacfg').gr_gid + os.chown(filename, uid, gid) + +def checkCertHeader(header, filename): + """ + Verify if filename contains specified header. + Returns True on success or on file not found to not trigger the exceptions + """ + if not os.path.isfile(filename): + return False + + with open(filename, 'r') as f: + for line in f: + if re.match(header, line): + return True + + return True + +def get_config(): + openvpn = deepcopy(default_config_data) + conf = Config() + + # determine tagNode instance + try: + openvpn['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + except KeyError as E: + print("Interface not specified") + + # Check if interface instance has been removed + if not conf.exists('interfaces openvpn ' + openvpn['intf']): + openvpn['deleted'] = True + return openvpn + + # Check if we belong to any bridge interface + for bridge in conf.list_nodes('interfaces bridge'): + for intf in conf.list_nodes('interfaces bridge {} member interface'.format(bridge)): + if intf == openvpn['intf']: + openvpn['bridge_member'].append(intf) + + # set configuration level + conf.set_level('interfaces openvpn ' + openvpn['intf']) + + # retrieve authentication options - username + if conf.exists('authentication username'): + openvpn['auth_user'] = conf.return_value('authentication username') + openvpn['auth'] = True + + # retrieve authentication options - username + if conf.exists('authentication password'): + openvpn['auth_pass'] = conf.return_value('authentication password') + openvpn['auth'] = True + + # retrieve interface description + if conf.exists('description'): + openvpn['description'] = conf.return_value('description') + + # interface device-type + if conf.exists('device-type'): + openvpn['type'] = conf.return_value('device-type') + + # disable interface + if conf.exists('disable'): + openvpn['disable'] = True + + # data encryption algorithm + if conf.exists('encryption'): + openvpn['encryption'] = conf.return_value('encryption') + + # hash algorithm + if conf.exists('hash'): + openvpn['hash'] = conf.return_value('hash') + + # Maximum number of keepalive packet failures + if conf.exists('keep-alive failure-count') and conf.exists('keep-alive interval'): + fail_count = conf.return_value('keep-alive failure-count') + interval = conf.return_value('keep-alive interval') + openvpn['ping_interval' ] = interval + openvpn['ping_restart' ] = int(interval) * int(fail_count) + + # Local IP address of tunnel - even as it is a tag node - we can only work + # on the first address + if conf.exists('local-address'): + openvpn['local_address'] = conf.list_nodes('local-address')[0] + if conf.exists('local-address {} subnet-mask'.format(openvpn['local_address'])): + openvpn['local_address_subnet'] = conf.return_value('local-address {} subnet-mask'.format(openvpn['local_address'])) + + # Local IP address to accept connections + if conf.exists('local-host'): + openvpn['local_host'] = conf.return_value('local-host') + + # Local port number to accept connections + if conf.exists('local-port'): + openvpn['local_port'] = conf.return_value('local-port') + + # OpenVPN operation mode + if conf.exists('mode'): + mode = conf.return_value('mode') + openvpn['mode'] = mode + + # Additional OpenVPN options + if conf.exists('openvpn-option'): + openvpn['options'] = conf.return_values('openvpn-option') + + # Do not close and reopen interface + if conf.exists('persistent-tunnel'): + openvpn['persistent_tunnel'] = True + + # Communication protocol + if conf.exists('protocol'): + openvpn['protocol'] = conf.return_value('protocol') + + # IP address of remote end of tunnel + if conf.exists('remote-address'): + openvpn['remote_address'] = conf.return_value('remote-address') + + # Remote host to connect to (dynamic if not set) + if conf.exists('remote-host'): + openvpn['remote_host'] = conf.return_values('remote-host') + + # Remote port number to connect to + if conf.exists('remote-port'): + openvpn['remote_port'] = conf.return_value('remote-port') + + # OpenVPN tunnel to be used as the default route + # see https://openvpn.net/community-resources/reference-manual-for-openvpn-2-4/ + # redirect-gateway flags + if conf.exists('replace-default-route'): + openvpn['redirect_gateway'] = 'def1' + + if conf.exists('replace-default-route local'): + openvpn['redirect_gateway'] = 'local def1' + + # Topology for clients + if conf.exists('server topology'): + openvpn['server_topology'] = conf.return_value('server topology') + + # Server-mode subnet (from which client IPs are allocated) + if conf.exists('server subnet'): + network = conf.return_value('server subnet') + tmp = IPv4Interface(network).with_netmask + # convert the network in format: "192.0.2.0 255.255.255.0" for later use in template + openvpn['server_subnet'] = tmp.replace(r'/', ' ') + + # Client-specific settings + for client in conf.list_nodes('server client'): + # set configuration level + conf.set_level('interfaces openvpn ' + openvpn['intf'] + ' server client ' + client) + data = { + 'name': client, + 'disable': False, + 'ip': '', + 'push_route': [], + 'subnet': [], + 'remote_netmask': '' + } + + # note: with "topology subnet", this is " ". + # with "topology p2p", this is " ". + if openvpn['server_topology'] == 'subnet': + # we are only interested in the netmask portion of server_subnet + data['remote_netmask'] = openvpn['server_subnet'].split(' ')[1] + else: + # we need the server subnet in format 192.0.2.0/255.255.255.0 + subnet = openvpn['server_subnet'].replace(' ', r'/') + # get iterator over the usable hosts in the network + tmp = ip_network(subnet).hosts() + # OpenVPN always uses the subnets first available IP address + data['remote_netmask'] = list(tmp)[0] + + # Option to disable client connection + if conf.exists('disable'): + data['disable'] = True + + # IP address of the client + if conf.exists('ip'): + data['ip'] = conf.return_value('ip') + + # Route to be pushed to the client + for network in conf.return_values('push-route'): + tmp = IPv4Interface(network).with_netmask + data['push_route'].append(tmp.replace(r'/', ' ')) + + # Subnet belonging to the client + for network in conf.return_values('subnet'): + tmp = IPv4Interface(network).with_netmask + data['subnet'].append(tmp.replace(r'/', ' ')) + + # Append to global client list + openvpn['client'].append(data) + + # re-set configuration level + conf.set_level('interfaces openvpn ' + openvpn['intf']) + + # DNS suffix to be pushed to all clients + if conf.exists('server domain-name'): + openvpn['server_domain'] = conf.return_value('server domain-name') + + # Number of maximum client connections + if conf.exists('server max-connections'): + openvpn['server_max_conn'] = conf.return_value('server max-connections') + + # Domain Name Server (DNS) + if conf.exists('server name-server'): + openvpn['server_dns_nameserver'] = conf.return_values('server name-server') + + # Route to be pushed to all clients + if conf.exists('server push-route'): + for network in conf.return_values('server push-route'): + tmp = IPv4Interface(network).with_netmask + openvpn['server_push_route'].append(tmp.replace(r'/', ' ')) + + # Reject connections from clients that are not explicitly configured + if conf.exists('server reject-unconfigured-clients'): + openvpn['server_reject_unconfigured'] = True + + # File containing TLS auth static key + if conf.exists('tls auth-file'): + openvpn['tls_auth'] = conf.return_value('tls auth-file') + openvpn['tls'] = True + + # File containing certificate for Certificate Authority (CA) + if conf.exists('tls ca-cert-file'): + openvpn['tls_ca_cert'] = conf.return_value('tls ca-cert-file') + openvpn['tls'] = True + + # File containing certificate for this host + if conf.exists('tls cert-file'): + openvpn['tls_cert'] = conf.return_value('tls cert-file') + openvpn['tls'] = True + + # File containing certificate revocation list (CRL) for this host + if conf.exists('tls crl-file'): + openvpn['tls_crl'] = conf.return_value('tls crl-file') + openvpn['tls'] = True + + # File containing Diffie Hellman parameters (server only) + if conf.exists('tls dh-file'): + openvpn['tls_dh'] = conf.return_value('tls dh-file') + openvpn['tls'] = True + + # File containing this host's private key + if conf.exists('tls key-file'): + openvpn['tls_key'] = conf.return_value('tls key-file') + openvpn['tls'] = True + + # Role in TLS negotiation + if conf.exists('tls role'): + openvpn['tls_role'] = conf.return_value('tls role') + openvpn['tls'] = True + + # Minimum required TLS version + if conf.exists('tls tls-version-min'): + openvpn['tls_version_min'] = conf.return_value('tls tls-version-min') + + if conf.exists('shared-secret-key-file'): + openvpn['shared_secret_file'] = conf.return_value('shared-secret-key-file') + + if conf.exists('use-lzo-compression'): + openvpn['compress_lzo'] = True + + return openvpn + +def verify(openvpn): + if openvpn['deleted']: + return None + + if not openvpn['mode']: + raise ConfigError('Must specify OpenVPN operation mode') + + # Checks which need to be performed on interface rmeoval + if openvpn['deleted']: + # OpenVPN interface can not be deleted if it's still member of a bridge + if openvpn['bridge_member']: + raise ConfigError('Can not delete {} as it is a member interface of bridge {}!'.format(openvpn['intf'], bridge)) + + # + # OpenVPN client mode - VERIFY + # + if openvpn['mode'] == 'client': + if openvpn['local_port']: + raise ConfigError('Cannot specify "local-port" in client mode') + + if openvpn['local_host']: + raise ConfigError('Cannot specify "local-host" in client mode') + + if openvpn['protocol'] == 'tcp-passive': + raise ConfigError('Protocol "tcp-passive" is not valid in client mode') + + if not openvpn['remote_host']: + raise ConfigError('Must specify "remote-host" in client mode') + + if openvpn['tls_dh']: + raise ConfigError('Cannot specify "tls dh-file" in client mode') + + # + # OpenVPN site-to-site - VERIFY + # + if openvpn['mode'] == 'site-to-site': + if not (openvpn['local_address'] or openvpn['bridge_member']): + raise ConfigError('Must specify "local-address" or "bridge member interface"') + + for host in openvpn['remote_host']: + if host == openvpn['remote_address']: + raise ConfigError('"remote-address" cannot be the same as "remote-host"') + + if openvpn['type'] == 'tun': + if not openvpn['remote_address']: + raise ConfigError('Must specify "remote-address"') + + if openvpn['local_address'] == openvpn['remote_address']: + raise ConfigError('"local-address" and "remote-address" cannot be the same') + + if openvpn['local_address'] == openvpn['local_host']: + raise ConfigError('"local-address" cannot be the same as "local-host"') + + else: + if openvpn['local_address'] or openvpn['remote_address']: + raise ConfigError('Cannot specify "local-address" or "remote-address" in client-server mode') + + elif openvpn['bridge_member']: + raise ConfigError('Cannot specify "local-address" or "remote-address" in bridge mode') + + # + # OpenVPN server mode - VERIFY + # + if openvpn['mode'] == 'server': + if openvpn['protocol'] == 'tcp-active': + raise ConfigError('Protocol "tcp-active" is not valid in server mode') + + if openvpn['remote_port']: + raise ConfigError('Cannot specify "remote-port" in server mode') + + if openvpn['remote_host']: + raise ConfigError('Cannot specify "remote-host" in server mode') + + if openvpn['protocol'] == 'tcp-passive' and len(openvpn['remote_host']) > 1: + raise ConfigError('Cannot specify more than 1 "remote-host" with "tcp-passive"') + + if not openvpn['tls_dh']: + raise ConfigError('Must specify "tls dh-file" in server mode') + + if not openvpn['server_subnet']: + if not openvpn['bridge_member']: + raise ConfigError('Must specify "server subnet" option in server mode') + + else: + # checks for both client and site-to-site go here + if openvpn['server_reject_unconfigured']: + raise ConfigError('reject-unconfigured-clients is only supported in OpenVPN server mode') + + if openvpn['server_topology']: + raise ConfigError('The "topology" option is only valid in server mode') + + if (not openvpn['remote_host']) and openvpn['redirect_gateway']: + raise ConfigError('Cannot set "replace-default-route" without "remote-host"') + + # + # OpenVPN common verification section + # not depending on any operation mode + # + + # verify specified IP address is present on any interface on this system + if openvpn['local_host']: + if not is_addr_assigned(openvpn['local_host']): + raise ConfigError('No interface on system with specified local-host IP address: {}'.format(openvpn['local_host'])) + + # TCP active + if openvpn['protocol'] == 'tcp-active': + if openvpn['local_port']: + raise ConfigError('Cannot specify "local-port" with "tcp-active"') + + if not openvpn['remote_host']: + raise ConfigError('Must specify "remote-host" with "tcp-active"') + + # shared secret and TLS + if not (openvpn['shared_secret_file'] or openvpn['tls']): + raise ConfigError('Must specify one of "shared-secret-key-file" and "tls"') + + if openvpn['shared_secret_file'] and openvpn['tls']: + raise ConfigError('Can only specify one of "shared-secret-key-file" and "tls"') + + if openvpn['mode'] in ['client', 'server']: + if not openvpn['tls']: + raise ConfigError('Must specify "tls" in client-server mode') + + # + # TLS/encryption + # + if openvpn['shared_secret_file']: + if openvpn['encryption'] in ['aes128gcm', 'aes192gcm', 'aes256gcm']: + raise ConfigError('GCM encryption with shared-secret-key-file is not supported') + + if not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', openvpn['shared_secret_file']): + raise ConfigError('Specified shared-secret-key-file "{}" is not valid'.format(openvpn['shared_secret_file'])) + + if openvpn['tls']: + if not openvpn['tls_ca_cert']: + raise ConfigError('Must specify "tls ca-cert-file"') + + if not (openvpn['mode'] == 'client' and openvpn['auth']): + if not openvpn['tls_cert']: + raise ConfigError('Must specify "tls cert-file"') + + if not openvpn['tls_key']: + raise ConfigError('Must specify "tls key-file"') + + if not checkCertHeader('-----BEGIN CERTIFICATE-----', openvpn['tls_ca_cert']): + raise ConfigError('Specified ca-cert-file "{}" is invalid'.format(openvpn['tls_ca_cert'])) + + if openvpn['tls_auth']: + if not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', openvpn['tls_auth']): + raise ConfigError('Specified auth-file "{}" is invalid'.format(openvpn['tls_auth'])) + + if openvpn['tls_cert']: + if not checkCertHeader('-----BEGIN CERTIFICATE-----', openvpn['tls_cert']): + raise ConfigError('Specified cert-file "{}" is invalid'.format(openvpn['tls_cert'])) + + if openvpn['tls_key']: + if not checkCertHeader('-----BEGIN (?:RSA )?PRIVATE KEY-----', openvpn['tls_key']): + raise ConfigError('Specified key-file "{}" is not valid'.format(openvpn['tls_key'])) + + if openvpn['tls_crl']: + if not checkCertHeader('-----BEGIN X509 CRL-----', openvpn['tls_crl']): + raise ConfigError('Specified crl-file "{} not valid'.format(openvpn['tls_crl'])) + + if openvpn['tls_dh']: + if not checkCertHeader('-----BEGIN DH PARAMETERS-----', openvpn['tls_dh']): + raise ConfigError('Specified dh-file "{}" is not valid'.format(openvpn['tls_dh'])) + + if openvpn['tls_role']: + if openvpn['mode'] in ['client', 'server']: + if not openvpn['tls_auth']: + raise ConfigError('Cannot specify "tls role" in client-server mode') + + if openvpn['tls_role'] == 'active': + if openvpn['protocol'] == 'tcp-passive': + raise ConfigError('Cannot specify "tcp-passive" when "tls role" is "active"') + + if openvpn['tls_dh']: + raise ConfigError('Cannot specify "tls dh-file" when "tls role" is "active"') + + elif openvpn['tls_role'] == 'passive': + if openvpn['protocol'] == 'tcp-active': + raise ConfigError('Cannot specify "tcp-active" when "tls role" is "passive"') + + if not openvpn['tls_dh']: + raise ConfigError('Must specify "tls dh-file" when "tls role" is "passive"') + + # + # Auth user/pass + # + if openvpn['auth']: + if not openvpn['auth_user']: + raise ConfigError('Username for authentication is missing') + + if not openvpn['auth_pass']: + raise ConfigError('Password for authentication is missing') + + # + # Client + # + subnet = openvpn['server_subnet'].replace(' ', '/') + for client in openvpn['client']: + if not ip_address(client['ip']) in ip_network(subnet): + raise ConfigError('Client IP "{}" not in server subnet "{}'.format(client['ip'], subnet)) + + + + return None + +def generate(openvpn): + if openvpn['deleted'] or openvpn['disable']: + return None + + interface = openvpn['intf'] + directory = os.path.dirname(get_config_name(interface)) + + # create config directory on demand + openvpn_mkdir(directory) + # create status directory on demand + openvpn_mkdir(directory + '/status') + # create client config dir on demand + openvpn_mkdir(directory + '/ccd') + # crete client config dir per interface on demand + openvpn_mkdir(directory + '/ccd/' + interface) + + # Fix file permissons for keys + fixup_permission(openvpn['shared_secret_file']) + fixup_permission(openvpn['tls_key']) + + # Generate User/Password authentication file + if openvpn['auth']: + auth_file = '/tmp/openvpn-{}-pw'.format(interface) + with open(auth_file, 'w') as f: + f.write('{}\n{}'.format(openvpn['auth_user'], openvpn['auth_pass'])) + + fixup_permission(auth_file) + + # get numeric uid/gid + uid = getpwnam(user).pw_uid + gid = getgrnam(group).gr_gid + + # Generate client specific configuration + for client in openvpn['client']: + client_file = directory + '/ccd/' + interface + '/' + client['name'] + tmpl = jinja2.Template(client_tmpl) + client_text = tmpl.render(client) + with open(client_file, 'w') as f: + f.write(client_text) + os.chown(client_file, uid, gid) + + tmpl = jinja2.Template(config_tmpl) + config_text = tmpl.render(openvpn) + + # we need to support quoting of raw parameters from OpenVPN CLI + # see https://phabricator.vyos.net/T1632 + config_text = config_text.replace(""",'"') + + with open(get_config_name(interface), 'w') as f: + f.write(config_text) + os.chown(get_config_name(interface), uid, gid) + + return None + +def apply(openvpn): + pid = 0 + pidfile = '/var/run/openvpn/{}.pid'.format(openvpn['intf']) + if os.path.isfile(pidfile): + pid = 0 + with open(pidfile, 'r') as f: + pid = int(f.read()) + + # Always stop OpenVPN service. We can not send a SIGUSR1 for restart of the + # service as the configuration is not re-read. Stop daemon only if it's + # running - it could have died or killed by someone evil + if pid_exists(pid): + cmd = 'start-stop-daemon --stop --quiet' + cmd += ' --pidfile ' + pidfile + subprocess_cmd(cmd) + + # cleanup old PID file + if os.path.isfile(pidfile): + os.remove(pidfile) + + # Do some cleanup when OpenVPN is disabled/deleted + if openvpn['deleted'] or openvpn['disable']: + # cleanup old configuration file + if os.path.isfile(get_config_name(openvpn['intf'])): + os.remove(get_config_name(openvpn['intf'])) + + # cleanup client config dir + directory = os.path.dirname(get_config_name(openvpn['intf'])) + if os.path.isdir(directory + '/ccd/' + openvpn['intf']): + try: + os.remove(directory + '/ccd/' + openvpn['intf'] + '/*') + except: + pass + + return None + + # On configuration change we need to wait for the 'old' interface to + # vanish from the Kernel, if it is not gone, OpenVPN will report: + # ERROR: Cannot ioctl TUNSETIFF vtun10: Device or resource busy (errno=16) + while openvpn['intf'] in interfaces(): + sleep(0.250) # 250ms + + # No matching OpenVPN process running - maybe it got killed or none + # existed - nevertheless, spawn new OpenVPN process + cmd = 'start-stop-daemon --start --quiet' + cmd += ' --pidfile ' + pidfile + cmd += ' --exec /usr/sbin/openvpn' + # now pass arguments to openvpn binary + cmd += ' --' + cmd += ' --config ' + get_config_name(openvpn['intf']) + + # execute assembled command + subprocess_cmd(cmd) + + # better late then sorry ... but we can only set interface alias after + # OpenVPN has been launched and created the interface + cnt = 0 + while openvpn['intf'] not in interfaces(): + # If VPN tunnel can't be established because the peer/server isn't + # (temporarily) available, the vtun interface never becomes registered + # with the kernel, and the commit would hang if there is no bail out + # condition + cnt += 1 + if cnt == 50: + break + + # sleep 250ms + sleep(0.250) + + try: + # we need to catch the exception if the interface is not up due to + # reason stated above + Interface(openvpn['intf']).set_alias(openvpn['description']) + except: + pass + + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py new file mode 100755 index 000000000..1097ae4d0 --- /dev/null +++ b/src/conf_mode/interfaces-vxlan.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os + +from sys import exit +from copy import deepcopy + +from vyos.configdict import list_diff +from vyos.config import Config +from vyos.ifconfig import VXLANIf, Interface +from vyos.interfaces import get_type_of_interface +from vyos import ConfigError +from netifaces import interfaces + +default_config_data = { + 'address': [], + 'deleted': False, + 'description': '', + 'disable': False, + 'group': '', + 'intf': '', + 'ip_arp_cache_tmo': 30, + 'ip_proxy_arp': 0, + 'link': '', + 'mtu': 1450, + 'remote': '', + 'remote_port': 8472 # The Linux implementation of VXLAN pre-dates + # the IANA's selection of a standard destination port +} + +def get_config(): + vxlan = deepcopy(default_config_data) + conf = Config() + + # determine tagNode instance + try: + vxlan['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + except KeyError as E: + print("Interface not specified") + + # Check if interface has been removed + if not conf.exists('interfaces vxlan ' + vxlan['intf']): + vxlan['deleted'] = True + return vxlan + + # set new configuration level + conf.set_level('interfaces vxlan ' + vxlan['intf']) + + # retrieve configured interface addresses + if conf.exists('address'): + vxlan['address'] = conf.return_values('address') + + # retrieve interface description + if conf.exists('description'): + vxlan['description'] = conf.return_value('description') + + # Disable this interface + if conf.exists('disable'): + vxlan['disable'] = True + + # VXLAN multicast grou + if conf.exists('group'): + vxlan['group'] = conf.return_value('group') + + # ARP cache entry timeout in seconds + if conf.exists('ip arp-cache-timeout'): + vxlan['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) + + # Enable proxy-arp on this interface + if conf.exists('ip enable-proxy-arp'): + vxlan['ip_proxy_arp'] = 1 + + # VXLAN underlay interface + if conf.exists('link'): + vxlan['link'] = conf.return_value('link') + + # Maximum Transmission Unit (MTU) + if conf.exists('mtu'): + vxlan['mtu'] = int(conf.return_value('mtu')) + + # Remote address of VXLAN tunnel + if conf.exists('remote'): + vxlan['remote'] = conf.return_value('remote') + + # Remote port of VXLAN tunnel + if conf.exists('port'): + vxlan['remote_port'] = int(conf.return_value('port')) + + # Virtual Network Identifier + if conf.exists('vni'): + vxlan['vni'] = conf.return_value('vni') + + return vxlan + + +def verify(vxlan): + if vxlan['deleted']: + # bail out early + return None + + if vxlan['mtu'] < 1500: + print('WARNING: RFC7348 recommends VXLAN tunnels preserve a 1500 byte MTU') + + if vxlan['group'] and not vxlan['link']: + raise ConfigError('Multicast VXLAN requires an underlaying interface ') + + if not (vxlan['group'] or vxlan['remote']): + raise ConfigError('Group or remote must be configured') + + if not vxlan['vni']: + raise ConfigError('Must configure VNI for VXLAN') + + if vxlan['link']: + # VXLAN adds a 50 byte overhead - we need to check the underlaying MTU + # if our configured MTU is at least 50 bytes less + underlay_mtu = int(Interface(vxlan['link']).get_mtu()) + if underlay_mtu < (vxlan['mtu'] + 50): + raise ConfigError('VXLAN has a 50 byte overhead, underlaying device ' \ + 'MTU is to small ({})'.format(underlay_mtu)) + + return None + + +def generate(vxlan): + return None + + +def apply(vxlan): + # Check if the VXLAN interface already exists + if vxlan['intf'] in interfaces(): + v = VXLANIf(vxlan['intf']) + # VXLAN is super picky and the tunnel always needs to be recreated, + # thus we can simply always delete it first. + v.remove() + + if not vxlan['deleted']: + # VXLAN interface needs to be created on-block + # instead of passing a ton of arguments, I just use a dict + # that is managed by vyos.ifconfig + conf = deepcopy(VXLANIf.get_config()) + + # Assign VXLAN instance configuration parameters to config dict + conf['vni'] = vxlan['vni'] + conf['group'] = vxlan['group'] + conf['dev'] = vxlan['link'] + conf['remote'] = vxlan['remote'] + conf['port'] = vxlan['remote_port'] + + # Finally create the new interface + v = VXLANIf(vxlan['intf'], config=conf) + # update interface description used e.g. by SNMP + v.set_alias(vxlan['description']) + # Maximum Transfer Unit (MTU) + v.set_mtu(vxlan['mtu']) + + # configure ARP cache timeout in milliseconds + v.set_arp_cache_tmo(vxlan['ip_arp_cache_tmo']) + # Enable proxy-arp on this interface + v.set_proxy_arp(vxlan['ip_proxy_arp']) + + # Configure interface address(es) - no need to implicitly delete the + # old addresses as they have already been removed by deleting the + # interface above + for addr in vxlan['address']: + v.add_addr(addr) + + # As the bond interface is always disabled first when changing + # parameters we will only re-enable the interface if it is not + # administratively disabled + if not vxlan['disable']: + v.set_state('up') + + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py new file mode 100755 index 000000000..7a684bafa --- /dev/null +++ b/src/conf_mode/interfaces-wireguard.py @@ -0,0 +1,285 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# + +import sys +import os +import re +import subprocess +from copy import deepcopy +from netifaces import interfaces + +from vyos import ConfigError +from vyos.config import Config +from vyos.configdict import list_diff +from vyos.ifconfig import WireGuardIf + +kdir = r'/config/auth/wireguard' + + +def _check_kmod(): + if not os.path.exists('/sys/module/wireguard'): + if os.system('sudo modprobe wireguard') != 0: + raise ConfigError("modprobe wireguard failed") + + +def _migrate_default_keys(): + if os.path.exists('{}/private.key'.format(kdir)) and not os.path.exists('{}/default/private.key'.format(kdir)): + old_umask = os.umask(0o027) + location = '{}/default'.format(kdir) + subprocess.call(['sudo mkdir -p ' + location], shell=True) + subprocess.call(['sudo chgrp vyattacfg ' + location], shell=True) + subprocess.call(['sudo chmod 750 ' + location], shell=True) + os.rename('{}/private.key'.format(kdir), + '{}/private.key'.format(location)) + os.rename('{}/public.key'.format(kdir), + '{}/public.key'.format(location)) + os.umask(old_umask) + + +def get_config(): + c = Config() + if not c.exists('interfaces wireguard'): + return None + + dflt_cnf = { + 'intfc': '', + 'addr': [], + 'addr_remove': [], + 'descr': '', + 'lport': None, + 'delete': False, + 'state': 'up', + 'fwmark': 0x00, + 'mtu': 1420, + 'peer': {}, + 'peer_remove': [], + 'pk': '{}/default/private.key'.format(kdir) + } + + if os.getenv('VYOS_TAGNODE_VALUE'): + ifname = str(os.environ['VYOS_TAGNODE_VALUE']) + wg = deepcopy(dflt_cnf) + wg['intfc'] = ifname + wg['descr'] = ifname + else: + print("ERROR: VYOS_TAGNODE_VALUE undefined") + sys.exit(1) + + c.set_level('interfaces wireguard') + + # interface removal state + if not c.exists(ifname) and c.exists_effective(ifname): + wg['delete'] = True + + if not wg['delete']: + c.set_level('interfaces wireguard {}'.format(ifname)) + if c.exists('address'): + wg['addr'] = c.return_values('address') + + # determine addresses which need to be removed + eff_addr = c.return_effective_values('address') + wg['addr_remove'] = list_diff(eff_addr, wg['addr']) + + # ifalias description + if c.exists('description'): + wg['descr'] = c.return_value('description') + + # link state + if c.exists('disable'): + wg['state'] = 'down' + + # local port to listen on + if c.exists('port'): + wg['lport'] = c.return_value('port') + + # fwmark value + if c.exists('fwmark'): + wg['fwmark'] = c.return_value('fwmark') + + # mtu + if c.exists('mtu'): + wg['mtu'] = c.return_value('mtu') + + # private key + if c.exists('private-key'): + wg['pk'] = "{0}/{1}/private.key".format( + kdir, c.return_value('private-key')) + + # peer removal, wg identifies peers by its pubkey + peer_eff = c.list_effective_nodes('peer') + peer_rem = list_diff(peer_eff, c.list_nodes('peer')) + for p in peer_rem: + wg['peer_remove'].append( + c.return_effective_value('peer {} pubkey'.format(p))) + + # peer settings + if c.exists('peer'): + for p in c.list_nodes('peer'): + if not c.exists('peer ' + p + ' disable'): + wg['peer'].update( + { + p: { + 'allowed-ips': [], + 'endpoint': '', + 'pubkey': '' + } + } + ) + # peer allowed-ips + if c.exists('peer ' + p + ' allowed-ips'): + wg['peer'][p]['allowed-ips'] = c.return_values( + 'peer ' + p + ' allowed-ips') + # peer endpoint + if c.exists('peer ' + p + ' endpoint'): + wg['peer'][p]['endpoint'] = c.return_value( + 'peer ' + p + ' endpoint') + # persistent-keepalive + if c.exists('peer ' + p + ' persistent-keepalive'): + wg['peer'][p]['persistent-keepalive'] = c.return_value( + 'peer ' + p + ' persistent-keepalive') + # preshared-key + if c.exists('peer ' + p + ' preshared-key'): + wg['peer'][p]['psk'] = c.return_value( + 'peer ' + p + ' preshared-key') + # peer pubkeys + key_eff = c.return_effective_value( + 'peer {peer} pubkey'.format(peer=p)) + key_cfg = c.return_value( + 'peer {peer} pubkey'.format(peer=p)) + wg['peer'][p]['pubkey'] = key_cfg + + # on a pubkey change we need to remove the pubkey first + # peers are identified by pubkey, so key update means + # peer removal and re-add + if key_eff != key_cfg and key_eff != None: + wg['peer_remove'].append(key_cfg) + + return wg + + +def verify(c): + if not c: + return None + + if not os.path.exists(c['pk']): + raise ConfigError( + "No keys found, generate them by executing: \'run generate wireguard [keypair|named-keypairs]\'") + + if not c['delete']: + if not c['addr']: + raise ConfigError("ERROR: IP address required") + if not c['peer']: + raise ConfigError("ERROR: peer required") + for p in c['peer']: + if not c['peer'][p]['allowed-ips']: + raise ConfigError("ERROR: allowed-ips required for peer " + p) + if not c['peer'][p]['pubkey']: + raise ConfigError("peer pubkey required for peer " + p) + if not c['peer'][p]['endpoint']: + raise ConfigError("peer endpoint required for peer " + p) + + +def apply(c): + # no wg configs left, remove all interface from system + # maybe move it into ifconfig.py + if not c: + net_devs = os.listdir('/sys/class/net/') + for dev in net_devs: + if os.path.isdir('/sys/class/net/' + dev): + buf = open('/sys/class/net/' + dev + '/uevent', 'r').read() + if re.search("DEVTYPE=wireguard", buf, re.I | re.M): + wg_intf = re.sub("INTERFACE=", "", re.search( + "INTERFACE=.*", buf, re.I | re.M).group(0)) + subprocess.call( + ['ip l d dev ' + wg_intf + ' >/dev/null'], shell=True) + return None + + # init wg class + intfc = WireGuardIf(c['intfc']) + + # single interface removal + if c['delete']: + intfc.remove() + return None + + # remove IP addresses + for ip in c['addr_remove']: + intfc.del_addr(ip) + + # add IP addresses + for ip in c['addr']: + intfc.add_addr(ip) + + # interface mtu + intfc.set_mtu(int(c['mtu'])) + + # ifalias for snmp from description + intfc.set_alias(str(c['descr'])) + + # remove peers + if c['peer_remove']: + for pkey in c['peer_remove']: + intfc.remove_peer(pkey) + + # peer pubkey + # setting up the wg interface + intfc.config['private-key'] = c['pk'] + for p in c['peer']: + # peer pubkey + intfc.config['pubkey'] = str(c['peer'][p]['pubkey']) + # peer allowed-ips + intfc.config['allowed-ips'] = c['peer'][p]['allowed-ips'] + # local listen port + if c['lport']: + intfc.config['port'] = c['lport'] + # fwmark + if c['fwmark']: + intfc.config['fwmark'] = c['fwmark'] + # endpoint + if c['peer'][p]['endpoint']: + intfc.config['endpoint'] = c['peer'][p]['endpoint'] + + # persistent-keepalive + if 'persistent-keepalive' in c['peer'][p]: + intfc.config['keepalive'] = c['peer'][p]['persistent-keepalive'] + + # maybe move it into ifconfig.py + # preshared-key - needs to be read from a file + if 'psk' in c['peer'][p]: + psk_file = '/config/auth/wireguard/psk' + old_umask = os.umask(0o077) + open(psk_file, 'w').write(str(c['peer'][p]['psk'])) + os.umask(old_umask) + intfc.config['psk'] = psk_file + intfc.update() + + # interface state + intfc.set_state(c['state']) + + return None + +if __name__ == '__main__': + try: + _check_kmod() + _migrate_default_keys() + c = get_config() + verify(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) -- cgit v1.2.3