diff options
Diffstat (limited to 'src/conf_mode')
-rwxr-xr-x | src/conf_mode/interfaces-bonding.py | 41 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-bridge.py | 17 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-dummy.py | 18 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-ethernet.py | 39 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-pppoe.py | 149 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-pseudo-ethernet.py | 54 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-vxlan.py | 7 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-wireless.py | 46 | ||||
-rwxr-xr-x | src/conf_mode/system-login-banner.py | 19 | ||||
-rwxr-xr-x | src/conf_mode/system-login.py | 8 | ||||
-rwxr-xr-x | src/conf_mode/vrf.py | 281 |
11 files changed, 591 insertions, 88 deletions
diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py index dcb0b59ed..a75beabd1 100755 --- a/src/conf_mode/interfaces-bonding.py +++ b/src/conf_mode/interfaces-bonding.py @@ -21,7 +21,7 @@ from sys import exit from netifaces import interfaces from vyos.ifconfig import BondIf -from vyos.ifconfig_vlan import apply_vlan_config +from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config from vyos.configdict import list_diff, vlan_to_dict from vyos.config import Config from vyos import ConfigError @@ -58,7 +58,8 @@ default_config_data = { 'vif_s': [], 'vif_s_remove': [], 'vif': [], - 'vif_remove': [] + 'vif_remove': [], + 'vrf': '' } @@ -221,8 +222,10 @@ def get_config(): if conf.exists('primary'): bond['primary'] = conf.return_value('primary') - # re-set configuration level to parse new nodes - conf.set_level(cfg_base) + # retrieve VRF instance + if conf.exists('vrf'): + bond['vrf'] = conf.return_value('vrf') + # get vif-s interfaces (currently effective) - to determine which vif-s # interface is no longer present and needs to be removed eff_intf = conf.list_effective_nodes('vif-s') @@ -265,26 +268,12 @@ def verify(bond): raise ConfigError('Interface "{}" is not part of the bond' \ .format(bond['primary'])) + vrf_name = bond['vrf'] + if vrf_name and vrf_name not in interfaces(): + raise ConfigError(f'VRF "{vrf_name}" does not exist') - # DHCPv6 parameters-only and temporary address are mutually exclusive - for vif_s in bond['vif_s']: - if vif_s['dhcpv6_prm_only'] and vif_s['dhcpv6_temporary']: - raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - - for vif_c in vif_s['vif_c']: - if vif_c['dhcpv6_prm_only'] and vif_c['dhcpv6_temporary']: - raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - - for vif in bond['vif']: - if vif['dhcpv6_prm_only'] and vif['dhcpv6_temporary']: - raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - - - for vif_s in bond['vif_s']: - for vif in bond['vif']: - if vif['id'] == vif_s['id']: - raise ConfigError('Can not use identical ID on vif and vif-s interface') - + # use common function to verify VLAN configuration + verify_vlan_config(bond) conf = Config() for intf in bond['member']: @@ -472,6 +461,12 @@ def apply(bond): for addr in bond['address']: b.add_addr(addr) + # assign to VRF + if bond['vrf']: + b.add_vrf(bond['vrf']) + else: + b.del_vrf(bond['vrf']) + # remove no longer required service VLAN interfaces (vif-s) for vif_s in bond['vif_s_remove']: b.del_vlan(vif_s) diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py index 0810d63d6..f189ce36d 100755 --- a/src/conf_mode/interfaces-bridge.py +++ b/src/conf_mode/interfaces-bridge.py @@ -52,7 +52,8 @@ default_config_data = { 'member': [], 'member_remove': [], 'priority': 32768, - 'stp': 0 + 'stp': 0, + 'vrf': '' } def get_config(): @@ -191,12 +192,20 @@ def get_config(): if conf.exists('stp'): bridge['stp'] = 1 + # retrieve VRF instance + if conf.exists('vrf'): + bridge['vrf'] = conf.return_value('vrf') + return bridge def verify(bridge): if bridge['dhcpv6_prm_only'] and bridge['dhcpv6_temporary']: raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') + vrf_name = bridge['vrf'] + if vrf_name and vrf_name not in interfaces(): + raise ConfigError(f'VRF "{vrf_name}" does not exist') + conf = Config() for br in conf.list_nodes('interfaces bridge'): # it makes no sense to verify ourself in this case @@ -286,6 +295,12 @@ def apply(bridge): # store DHCPv6 config dictionary - used later on when addresses are aquired br.set_dhcpv6_options(opt) + # assign to VRF + if bridge['vrf']: + br.add_vrf(bridge['vrf']) + else: + br.del_vrf(bridge['vrf']) + # Change interface MAC address if bridge['mac']: br.set_mac(bridge['mac']) diff --git a/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py index e79e6222d..10cae5d7d 100755 --- a/src/conf_mode/interfaces-dummy.py +++ b/src/conf_mode/interfaces-dummy.py @@ -18,6 +18,7 @@ import os from copy import deepcopy from sys import exit +from netifaces import interfaces from vyos.ifconfig import DummyIf from vyos.configdict import list_diff @@ -30,7 +31,8 @@ default_config_data = { 'deleted': False, 'description': '', 'disable': False, - 'intf': '' + 'intf': '', + 'vrf': '' } def get_config(): @@ -69,9 +71,17 @@ def get_config(): act_addr = conf.return_values('address') dummy['address_remove'] = list_diff(eff_addr, act_addr) + # retrieve VRF instance + if conf.exists('vrf'): + dummy['vrf'] = conf.return_value('vrf') + return dummy def verify(dummy): + vrf_name = dummy['vrf'] + if vrf_name and vrf_name not in interfaces(): + raise ConfigError(f'VRF "{vrf_name}" does not exist') + return None def generate(dummy): @@ -95,6 +105,12 @@ def apply(dummy): for addr in dummy['address']: d.add_addr(addr) + # assign to VRF + if dummy['vrf']: + d.add_vrf(dummy['vrf']) + else: + d.del_vrf(dummy['vrf']) + # disable interface on demand if dummy['disable']: d.set_state('down') diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py index 43cc22589..6d779c94c 100755 --- a/src/conf_mode/interfaces-ethernet.py +++ b/src/conf_mode/interfaces-ethernet.py @@ -16,11 +16,12 @@ import os -from copy import deepcopy from sys import exit +from copy import deepcopy +from netifaces import interfaces from vyos.ifconfig import EthernetIf -from vyos.ifconfig_vlan import apply_vlan_config +from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config from vyos.configdict import list_diff, vlan_to_dict from vyos.config import Config from vyos import ConfigError @@ -59,7 +60,8 @@ default_config_data = { 'vif_s': [], 'vif_s_remove': [], 'vif': [], - 'vif_remove': [] + 'vif_remove': [], + 'vrf': '' } def get_config(): @@ -197,6 +199,10 @@ def get_config(): if conf.exists('speed'): eth['speed'] = conf.return_value('speed') + # retrieve VRF instance + if conf.exists('vrf'): + eth['vrf'] = conf.return_value('vrf') + # re-set configuration level to parse new nodes conf.set_level(cfg_base) # get vif-s interfaces (currently effective) - to determine which vif-s @@ -243,6 +249,10 @@ def verify(eth): if eth['dhcpv6_prm_only'] and eth['dhcpv6_temporary']: raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') + vrf_name = eth['vrf'] + if vrf_name and vrf_name not in interfaces(): + raise ConfigError(f'VRF "{vrf_name}" does not exist') + conf = Config() # some options can not be changed when interface is enslaved to a bond for bond in conf.list_nodes('interfaces bonding'): @@ -250,21 +260,10 @@ def verify(eth): bond_member = conf.return_values('interfaces bonding ' + bond + ' member interface') if eth['intf'] in bond_member: if eth['address']: - raise ConfigError('Can not assign address to interface {} which is a member of {}').format(eth['intf'], bond) - - # DHCPv6 parameters-only and temporary address are mutually exclusive - for vif_s in eth['vif_s']: - if vif_s['dhcpv6_prm_only'] and vif_s['dhcpv6_temporary']: - raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - - for vif_c in vif_s['vif_c']: - if vif_c['dhcpv6_prm_only'] and vif_c['dhcpv6_temporary']: - raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - - for vif in eth['vif']: - if vif['dhcpv6_prm_only'] and vif['dhcpv6_temporary']: - raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') + raise ConfigError('Can not assign address to interface {} which is a member of {}'.format(eth['intf'], bond)) + # use common function to verify VLAN configuration + verify_vlan_config(eth) return None def generate(eth): @@ -367,6 +366,12 @@ def apply(eth): for addr in eth['address']: e.add_addr(addr) + # assign to VRF + if eth['vrf']: + e.add_vrf(eth['vrf']) + else: + e.del_vrf(eth['vrf']) + # remove no longer required service VLAN interfaces (vif-s) for vif_s in eth['vif_s_remove']: e.del_vlan(vif_s) diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py index 8ec78bab3..f948070ee 100755 --- a/src/conf_mode/interfaces-pppoe.py +++ b/src/conf_mode/interfaces-pppoe.py @@ -20,9 +20,9 @@ from sys import exit from copy import deepcopy from jinja2 import Template from subprocess import Popen, PIPE -from time import sleep from pwd import getpwnam from grp import getgrnam +from stat import S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IXGRP, S_IROTH, S_IXOTH from vyos.config import Config from vyos.ifconfig import Interface @@ -30,9 +30,7 @@ from vyos import ConfigError from netifaces import interfaces # Please be careful if you edit the template. -config_pppoe_tmpl = """ -### Autogenerated by interfaces-pppoe.py ### - +config_pppoe_tmpl = """### Autogenerated by interfaces-pppoe.py ### {% if description %} # {{ description }} {% endif %} @@ -92,14 +90,85 @@ usepeerdns {% endif %} {% if ipv6_enable -%} +ipv6 +ipv6cp-use-ipaddr {% endif %} {% if service_name -%} rp_pppoe_service "{{ service_name }}" {% endif %} +{% if on_demand %} +demand +{% endif %} """ -PPP_LOGFILE = '/var/log/vyatta/ppp_{}.log' +# Please be careful if you edit the template. +# There must be no blank line at the top pf the script file +config_pppoe_ipv6_up_tmpl = """#!/bin/sh + +# As PPPoE is an "on demand" interface we need to re-configure it when it +# becomes up + +if [ "$6" != "{{ intf }}" ]; then + exit +fi + +# add some info to syslog +DIALER_PID=$(cat /var/run/{{ intf }}.pid) +logger -t pppd[$DIALER_PID] "executing $0" +logger -t pppd[$DIALER_PID] "configuring dialer interface $6 via $2" + +echo "{{ description }}" > /sys/class/net/{{ intf }}/ifalias + +{% if ipv6_autoconf -%} + + +# Configure interface-specific Host/Router behaviour. +# Note: It is recommended to have the same setting on all interfaces; mixed +# router/host scenarios are rather uncommon. Possible values are: +# +# 0 Forwarding disabled +# 1 Forwarding enabled +# +echo 1 > /proc/sys/net/ipv6/conf/{{ intf }}/forwarding + +# Accept Router Advertisements; autoconfigure using them. +# +# It also determines whether or not to transmit Router +# Solicitations. If and only if the functional setting is to +# accept Router Advertisements, Router Solicitations will be +# transmitted. Possible values are: +# +# 0 Do not accept Router Advertisements. +# 1 Accept Router Advertisements if forwarding is disabled. +# 2 Overrule forwarding behaviour. Accept Router Advertisements +# even if forwarding is enabled. +# +echo 2 > /proc/sys/net/ipv6/conf/{{ intf }}/accept_ra + +# Autoconfigure addresses using Prefix Information in Router Advertisements. +echo 1 > /proc/sys/net/ipv6/conf/{{ intf }}/autoconfigure +{% endif %} +""" + +config_pppoe_ip_pre_up_tmpl = """#!/bin/sh + +# As PPPoE is an "on demand" interface we need to re-configure it when it +# becomes up + +if [ "$6" != "pppoe0" ]; then + exit +fi + +# add some info to syslog +DIALER_PID=$(cat /var/run/{{ intf }}.pid) +logger -t pppd[$DIALER_PID] "executing $0" + +{% if vrf -%} +logger -t pppd[$DIALER_PID] "configuring dialer interface $6 for VRF {{ vrf }}" +ip link set dev {{ intf }} master {{ vrf }} +{% endif %} + +""" default_config_data = { 'access_concentrator': '', @@ -108,7 +177,7 @@ default_config_data = { 'on_demand': False, 'default_route': 'auto', 'deleted': False, - 'description': '', + 'description': '\0', 'disable': False, 'intf': '', 'idle_timeout': '', @@ -120,7 +189,8 @@ default_config_data = { 'name_server': True, 'remote_address': '', 'service_name': '', - 'source_interface': '' + 'source_interface': '', + 'vrf': '' } def subprocess_cmd(command): @@ -137,7 +207,7 @@ def get_config(): raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') pppoe['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - pppoe['logfile'] = PPP_LOGFILE.format(pppoe['intf']) + pppoe['logfile'] = f"/var/log/vyatta/ppp_{pppoe['intf']}.log" # Check if interface has been removed if not conf.exists(base_path + [pppoe['intf']]): @@ -193,7 +263,7 @@ def get_config(): # Physical Interface used for this PPPoE session if conf.exists(['source-interface']): - pppoe['source_interface'] = conf.return_value('source-interface') + pppoe['source_interface'] = conf.return_value(['source-interface']) # Maximum Transmission Unit (MTU) if conf.exists(['mtu']): @@ -211,6 +281,10 @@ def get_config(): if conf.exists(['service-name']): pppoe['service_name'] = conf.return_value(['service-name']) + # retrieve VRF instance + if conf.exists('vrf'): + pppoe['vrf'] = conf.return_value(['vrf']) + return pppoe def verify(pppoe): @@ -219,18 +293,24 @@ def verify(pppoe): return None if not pppoe['source_interface']: - raise ConfigError('PPPoE source interface is missing') + raise ConfigError('PPPoE source interface missing') + + if not pppoe['source_interface'] in interfaces(): + raise ConfigError(f"PPPoE source interface {pppoe['source_interface']} does not exist") - if pppoe['source_interface'] not in interfaces(): - raise ConfigError('PPPoE source interface does not exist') + vrf_name = pppoe['vrf'] + if vrf_name and vrf_name not in interfaces(): + raise ConfigError(f'VRF {vrf_name} does not exist') return None def generate(pppoe): - config_file_pppoe = '/etc/ppp/peers/{}'.format(pppoe['intf']) + config_file_pppoe = f"/etc/ppp/peers/{pppoe['intf']}" + ip_pre_up_script_file = f"/etc/ppp/ip-pre-up.d/9999-vyos-vrf-{pppoe['intf']}" + ipv6_if_up_script_file = f"/etc/ppp/ipv6-up.d/50-vyos-{pppoe['intf']}-autoconf" # Always hang-up PPPoE connection prior generating new configuration file - cmd = 'systemctl stop ppp@{}.service'.format(pppoe['intf']) + cmd = f"systemctl stop ppp@{pppoe['intf']}.service" subprocess_cmd(cmd) if pppoe['deleted']: @@ -238,6 +318,12 @@ def generate(pppoe): if os.path.exists(config_file_pppoe): os.unlink(config_file_pppoe) + if os.path.exists(ipv6_if_up_script_file): + os.unlink(ipv6_if_up_script_file) + + if os.path.exists(ip_pre_up_script_file): + os.unlink(ip_pre_up_script_file) + else: # Create PPP configuration files tmpl = Template(config_pppoe_tmpl) @@ -245,6 +331,21 @@ def generate(pppoe): with open(config_file_pppoe, 'w') as f: f.write(config_text) + tmpl = Template(config_pppoe_ip_pre_up_tmpl) + config_text = tmpl.render(pppoe) + with open(ip_pre_up_script_file, 'w') as f: + f.write(config_text) + + tmpl = Template(config_pppoe_ipv6_up_tmpl) + config_text = tmpl.render(pppoe) + with open(ipv6_if_up_script_file, 'w') as f: + f.write(config_text) + + bitmask = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | \ + S_IROTH | S_IXOTH + os.chmod(ip_pre_up_script_file, bitmask) + os.chmod(ipv6_if_up_script_file, bitmask) + return None def apply(pppoe): @@ -254,7 +355,7 @@ def apply(pppoe): if not pppoe['disable']: # dial PPPoE connection - cmd = 'systemctl start ppp@{}.service'.format(pppoe['intf']) + cmd = f"systemctl start ppp@{pppoe['intf']}.service" subprocess_cmd(cmd) # make logfile owned by root / vyattacfg @@ -263,24 +364,6 @@ def apply(pppoe): gid = getgrnam('vyattacfg').gr_gid os.chown(pppoe['logfile'], uid, gid) - # better late then sorry ... but we can only set interface alias after - # pppd has been launched and created the interface - cnt = 0 - while pppoe['intf'] not in interfaces(): - cnt += 1 - if cnt == 50: - break - - # sleep 250ms - sleep(0.250) - - try: - # we need to catch the exception if the interface is not up due to - # reason stated above - Interface(pppoe['intf']).set_alias(pppoe['description']) - except: - pass - return None if __name__ == '__main__': diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py index 3d36da226..989b1432b 100755 --- a/src/conf_mode/interfaces-pseudo-ethernet.py +++ b/src/conf_mode/interfaces-pseudo-ethernet.py @@ -21,7 +21,8 @@ from sys import exit from netifaces import interfaces from vyos.ifconfig import MACVLANIf -from vyos.configdict import list_diff +from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config +from vyos.configdict import list_diff, vlan_to_dict from vyos.config import Config from vyos import ConfigError @@ -52,7 +53,8 @@ default_config_data = { 'vif_s': [], 'vif_s_remove': [], 'vif': [], - 'vif_remove': [] + 'vif_remove': [], + 'vrf': '' } def get_config(): @@ -158,6 +160,10 @@ def get_config(): if conf.exists(['mode']): peth['mode'] = conf.return_value(['mode']) + # retrieve VRF instance + if conf.exists('vrf'): + peth['vrf'] = conf.return_value('vrf') + # re-set configuration level to parse new nodes conf.set_level(cfg_base) # get vif-s interfaces (currently effective) - to determine which vif-s @@ -196,6 +202,15 @@ def verify(peth): if not peth['link']: raise ConfigError('Link device must be set for virtual ethernet {}'.format(peth['intf'])) + if not peth['link'] in interfaces(): + raise ConfigError('Pseudo-ethernet source interface does not exist') + + vrf_name = peth['vrf'] + if vrf_name and vrf_name not in interfaces(): + raise ConfigError(f'VRF "{vrf_name}" does not exist') + + # use common function to verify VLAN configuration + verify_vlan_config(peth) return None def generate(peth): @@ -282,6 +297,12 @@ def apply(peth): # Enable private VLAN proxy ARP on this interface p.set_proxy_arp_pvlan(peth['ip_proxy_arp_pvlan']) + # assign to VRF + if peth['vrf']: + p.add_vrf(peth['vrf']) + else: + p.del_vrf(peth['vrf']) + # Change interface MAC address if peth['mac']: p.set_mac(peth['mac']) @@ -303,6 +324,35 @@ def apply(peth): for addr in peth['address']: p.add_addr(addr) + # remove no longer required service VLAN interfaces (vif-s) + for vif_s in peth['vif_s_remove']: + p.del_vlan(vif_s) + + # create service VLAN interfaces (vif-s) + for vif_s in peth['vif_s']: + s_vlan = p.add_vlan(vif_s['id'], ethertype=vif_s['ethertype']) + apply_vlan_config(s_vlan, vif_s) + + # remove no longer required client VLAN interfaces (vif-c) + # on lower service VLAN interface + for vif_c in vif_s['vif_c_remove']: + s_vlan.del_vlan(vif_c) + + # create client VLAN interfaces (vif-c) + # on lower service VLAN interface + for vif_c in vif_s['vif_c']: + c_vlan = s_vlan.add_vlan(vif_c['id']) + apply_vlan_config(c_vlan, vif_c) + + # remove no longer required VLAN interfaces (vif) + for vif in peth['vif_remove']: + p.del_vlan(vif) + + # create VLAN interfaces (vif) + for vif in peth['vif']: + vlan = p.add_vlan(vif['id']) + apply_vlan_config(vlan, vif) + return None if __name__ == '__main__': diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py index dabfe4836..c9ef0fe9c 100755 --- a/src/conf_mode/interfaces-vxlan.py +++ b/src/conf_mode/interfaces-vxlan.py @@ -134,8 +134,11 @@ def verify(vxlan): if vxlan['mtu'] < 1500: print('WARNING: RFC7348 recommends VXLAN tunnels preserve a 1500 byte MTU') - if vxlan['group'] and not vxlan['link']: - raise ConfigError('Multicast VXLAN requires an underlaying interface ') + if vxlan['group']: + if not vxlan['link']: + raise ConfigError('Multicast VXLAN requires an underlaying interface ') + if not vxlan['link'] in interfaces(): + raise ConfigError('VXLAN source interface does not exist') if not (vxlan['group'] or vxlan['remote']): raise ConfigError('Group or remote must be configured') diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py index 5289208d9..19e1f01b8 100755 --- a/src/conf_mode/interfaces-wireless.py +++ b/src/conf_mode/interfaces-wireless.py @@ -25,9 +25,10 @@ from grp import getgrnam from subprocess import Popen, PIPE from psutil import pid_exists +from netifaces import interfaces from vyos.ifconfig import EthernetIf -from vyos.ifconfig_vlan import apply_vlan_config +from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config from vyos.configdict import list_diff, vlan_to_dict from vyos.config import Config from vyos import ConfigError @@ -640,9 +641,16 @@ wpa_key_mgmt=WPA-EAP # IP addresses, but this field can be used to force a specific address to be # used, e.g., when the device has multiple IP addresses. radius_client_addr={{ sec_wpa_radius_source }} + +# The own IP address of the access point (used as NAS-IP-Address) +own_ip_addr={{ sec_wpa_radius_source }} +{% else %} +# The own IP address of the access point (used as NAS-IP-Address) +own_ip_addr=127.0.0.1 {% endif %} {% for radius in sec_wpa_radius -%} +{%- if not radius.disabled -%} # RADIUS authentication server auth_server_addr={{ radius.server }} auth_server_port={{ radius.port }} @@ -653,6 +661,7 @@ acct_server_addr={{ radius.server }} acct_server_port={{ radius.acc_port }} acct_server_shared_secret={{ radius.key }} {% endif %} +{% endif %} {% endfor %} {% endif %} @@ -760,6 +769,8 @@ network={ ssid="{{ ssid }}" {%- if sec_wpa_passphrase %} psk="{{ sec_wpa_passphrase }}" +{% else %} + key_mgmt=NONE {% endif %} } @@ -836,7 +847,8 @@ default_config_data = { 'ssid' : '', 'type' : 'monitor', 'vif': [], - 'vif_remove': [] + 'vif_remove': [], + 'vrf': '' } def get_conf_file(conf_type, intf): @@ -1148,6 +1160,10 @@ def get_config(): if conf.exists('mode'): wifi['mode'] = conf.return_value('mode') + # retrieve VRF instance + if conf.exists('vrf'): + wifi['vrf'] = conf.return_value('vrf') + # Wireless physical device if conf.exists('phy'): wifi['phy'] = conf.return_value('phy') @@ -1204,6 +1220,7 @@ def get_config(): radius = { 'server' : server, 'acc_port' : '', + 'disabled': False, 'port' : 1812, 'key' : '' } @@ -1216,6 +1233,10 @@ def get_config(): if conf.exists('accounting'): radius['acc_port'] = radius['port'] + 1 + # Check if RADIUS server was temporary disabled + if conf.exists(['disable']): + radius['disabled'] = True + # RADIUS server shared-secret if conf.exists('key'): radius['key'] = conf.return_value('key') @@ -1248,6 +1269,9 @@ def get_config(): conf.set_level(cfg_base + ' vif ' + vif) wifi['vif'].append(vlan_to_dict(conf)) + # disable interface + if conf.exists('disable'): + wifi['disable'] = True # retrieve configured regulatory domain conf.set_level('system') @@ -1273,7 +1297,6 @@ def verify(wifi): if not wifi['channel']: raise ConfigError('Channel must be set for {}'.format(wifi['intf'])) - if len(wifi['sec_wep_key']) > 4: raise ConfigError('No more then 4 WEP keys configurable') @@ -1293,7 +1316,12 @@ def verify(wifi): if not radius['key']: raise ConfigError('Misssing RADIUS shared secret key for server: {}'.format(radius['server'])) + vrf_name = wifi['vrf'] + if vrf_name and vrf_name not in interfaces(): + raise ConfigError(f'VRF "{vrf_name}" does not exist') + # use common function to verify VLAN configuration + verify_vlan_config(wifi) return None @@ -1390,6 +1418,12 @@ def apply(wifi): # ignore link state changes w.set_link_detect(wifi['disable_link_detect']) + # assign to VRF + if wifi['vrf']: + w.add_vrf(wifi['vrf']) + else: + w.del_vrf(wifi['vrf']) + # Change interface MAC address - re-set to real hardware address (hw-id) # if custom mac is removed if wifi['mac']: @@ -1406,8 +1440,10 @@ def apply(wifi): # configure ARP ignore w.set_arp_ignore(wifi['ip_enable_arp_ignore']) - # enable interface - if not wifi['disable']: + # Enable/Disable interface + if wifi['disable']: + w.set_state('down') + else: w.set_state('up') # Configure interface address(es) diff --git a/src/conf_mode/system-login-banner.py b/src/conf_mode/system-login-banner.py index e66d409bb..20cc16f97 100755 --- a/src/conf_mode/system-login-banner.py +++ b/src/conf_mode/system-login-banner.py @@ -16,6 +16,7 @@ from sys import exit from vyos.config import Config +from vyos import ConfigError motd=""" The programs included with the Debian GNU/Linux system are free software; @@ -49,15 +50,25 @@ def get_config(): # Post-Login banner if conf.exists(['post-login']): tmp = conf.return_value(['post-login']) - tmp = tmp.replace('\\n','\n') - tmp = tmp.replace('\\t','\t') + # post-login banner can be empty as well + if tmp: + tmp = tmp.replace('\\n','\n') + tmp = tmp.replace('\\t','\t') + else: + tmp = '' + banner['motd'] = tmp # Pre-Login banner if conf.exists(['pre-login']): tmp = conf.return_value(['pre-login']) - tmp = tmp.replace('\\n','\n') - tmp = tmp.replace('\\t','\t') + # pre-login banner can be empty as well + if tmp: + tmp = tmp.replace('\\n','\n') + tmp = tmp.replace('\\t','\t') + else: + tmp = '' + banner['issue'] = banner['issue_net'] = tmp return banner diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py index a7fb8ee8f..959e86e5b 100755 --- a/src/conf_mode/system-login.py +++ b/src/conf_mode/system-login.py @@ -196,6 +196,14 @@ def verify(login): if cur_user in login['del_users']: raise ConfigError('Attempting to delete current user: {}'.format(cur_user)) + for user in login['add_users']: + for key in user['public_keys']: + if not key['type']: + raise ConfigError('SSH public key type missing for "{}"!'.format(key['name'])) + + if not key['key']: + raise ConfigError('SSH public key for id "{}" missing!'.format(key['name'])) + # At lease one RADIUS server must not be disabled if len(login['radius_server']) > 0: fail = True diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py new file mode 100755 index 000000000..991c5cb2c --- /dev/null +++ b/src/conf_mode/vrf.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import jinja2 + +from sys import exit +from copy import deepcopy +from json import loads +from subprocess import check_output, CalledProcessError + +from vyos.config import Config +from vyos.configdict import list_diff +from vyos.ifconfig import Interface +from vyos import ConfigError + +config_file = r'/etc/iproute2/rt_tables.d/vyos-vrf.conf' + +# Please be careful if you edit the template. +config_tmpl = """ +### Autogenerated by vrf.py ### +# +# Routing table ID to name mapping reference + +# id vrf name comment +{% for vrf in vrf_add -%} +{{ "%-10s" | format(vrf.table) }} {{ "%-16s" | format(vrf.name) }} # {{ vrf.description }} +{% endfor -%} + +""" + +default_config_data = { + 'bind_to_all': 0, + 'deleted': False, + 'vrf_add': [], + 'vrf_existing': [], + 'vrf_remove': [] +} + +def _cmd(command): + try: + check_output(command.split()) + except CalledProcessError as e: + raise ConfigError(f'Error changing VRF: {e}') + +def list_rules(): + command = 'ip -j -4 rule show' + answer = loads(check_output(command.split()).decode()) + return [_ for _ in answer if _] + +def vrf_interfaces(c, match): + matched = [] + old_level = c.get_level() + c.set_level(['interfaces']) + section = c.get_config_dict([]) + for type in section: + interfaces = section[type] + for name in interfaces: + interface = interfaces[name] + if 'vrf' in interface: + v = interface.get('vrf', '') + if v == match: + matched.append(name) + + c.set_level(old_level) + return matched + +def vrf_routing(c, match): + matched = [] + old_level = c.get_level() + c.set_level(['protocols', 'vrf']) + if match in c.list_nodes([]): + matched.append(match) + + c.set_level(old_level) + return matched + + +def get_config(): + conf = Config() + vrf_config = deepcopy(default_config_data) + + cfg_base = ['vrf'] + if not conf.exists(cfg_base): + # get all currently effetive VRFs and mark them for deletion + vrf_config['vrf_remove'] = conf.list_effective_nodes(cfg_base + ['name']) + else: + # set configuration level base + conf.set_level(cfg_base) + + # Should services be allowed to bind to all VRFs? + if conf.exists(['bind-to-all']): + vrf_config['bind_to_all'] = 1 + + # Determine vrf interfaces (currently effective) - to determine which + # vrf interface is no longer present and needs to be removed + eff_vrf = conf.list_effective_nodes(['name']) + act_vrf = conf.list_nodes(['name']) + vrf_config['vrf_remove'] = list_diff(eff_vrf, act_vrf) + + # read in individual VRF definition and build up + # configuration + for name in conf.list_nodes(['name']): + vrf_inst = { + 'description' : '', + 'members': [], + 'name' : name, + 'table' : '', + 'table_mod': False + } + conf.set_level(cfg_base + ['name', name]) + + if conf.exists(['table']): + # VRF table can't be changed on demand, thus we need to read in the + # current and the effective routing table number + act_table = conf.return_value(['table']) + eff_table = conf.return_effective_value(['table']) + vrf_inst['table'] = act_table + if eff_table and eff_table != act_table: + vrf_inst['table_mod'] = True + + if conf.exists(['description']): + vrf_inst['description'] = conf.return_value(['description']) + + # append individual VRF configuration to global configuration list + vrf_config['vrf_add'].append(vrf_inst) + + # set configuration level base + conf.set_level(cfg_base) + + # check VRFs which need to be removed as they are not allowed to have + # interfaces attached + tmp = [] + for name in vrf_config['vrf_remove']: + vrf_inst = { + 'interfaces': [], + 'name': name, + 'routes': [] + } + + # find member interfaces of this particulat VRF + vrf_inst['interfaces'] = vrf_interfaces(conf, name) + + # find routing protocols used by this VRF + vrf_inst['routes'] = vrf_routing(conf, name) + + # append individual VRF configuration to temporary configuration list + tmp.append(vrf_inst) + + # replace values in vrf_remove with list of dictionaries + # as we need it in verify() - we can't delete a VRF with members attached + vrf_config['vrf_remove'] = tmp + return vrf_config + +def verify(vrf_config): + # ensure VRF is not assigned to any interface + for vrf in vrf_config['vrf_remove']: + if len(vrf['interfaces']) > 0: + raise ConfigError(f"VRF {vrf['name']} can not be deleted. It has active member interfaces!") + + if len(vrf['routes']) > 0: + raise ConfigError(f"VRF {vrf['name']} can not be deleted. It has active routing protocols!") + + table_ids = [] + for vrf in vrf_config['vrf_add']: + # table id is mandatory + if not vrf['table']: + raise ConfigError(f"VRF {vrf['name']} table id is mandatory!") + + # routing table id can't be changed - OS restriction + if vrf['table_mod']: + raise ConfigError(f"VRF {vrf['name']} table id modification is not possible!") + + # VRf routing table ID must be unique on the system + if vrf['table'] in table_ids: + raise ConfigError(f"VRF {vrf['name']} table id {vrf['table']} is not unique!") + + table_ids.append(vrf['table']) + + return None + +def generate(vrf_config): + tmpl = jinja2.Template(config_tmpl) + config_text = tmpl.render(vrf_config) + with open(config_file, 'w') as f: + f.write(config_text) + + return None + +def apply(vrf_config): + # Documentation + # + # - https://github.com/torvalds/linux/blob/master/Documentation/networking/vrf.txt + # - https://github.com/Mellanox/mlxsw/wiki/Virtual-Routing-and-Forwarding-(VRF) + # - https://netdevconf.info/1.1/proceedings/slides/ahern-vrf-tutorial.pdf + # - https://netdevconf.info/1.2/slides/oct6/02_ahern_what_is_l3mdev_slides.pdf + + # set the default VRF global behaviour + bind_all = vrf_config['bind_to_all'] + _cmd(f'sysctl -wq net.ipv4.tcp_l3mdev_accept={bind_all}') + _cmd(f'sysctl -wq net.ipv4.udp_l3mdev_accept={bind_all}') + + for vrf_name in vrf_config['vrf_remove']: + if os.path.isdir(f'/sys/class/net/{vrf_name}'): + _cmd(f'ip link delete dev {vrf_name}') + + for vrf in vrf_config['vrf_add']: + name = vrf['name'] + table = vrf['table'] + + if not os.path.isdir(f'/sys/class/net/{name}'): + # For each VRF apart from your default context create a VRF + # interface with a separate routing table + _cmd(f'ip link add {name} type vrf table {table}') + # Start VRf + _cmd(f'ip link set dev {name} up') + # The kernel Documentation/networking/vrf.txt also recommends + # adding unreachable routes to the VRF routing tables so that routes + # afterwards are taken. + _cmd(f'ip -4 route add vrf {name} unreachable default metric 4278198272') + _cmd(f'ip -6 route add vrf {name} unreachable default metric 4278198272') + + # set VRF description for e.g. SNMP monitoring + Interface(name).set_alias(vrf['description']) + + # Linux routing uses rules to find tables - routing targets are then + # looked up in those tables. If the lookup got a matching route, the + # process ends. + # + # TL;DR; first table with a matching entry wins! + # + # You can see your routing table lookup rules using "ip rule", sadly the + # local lookup is hit before any VRF lookup. Pinging an addresses from the + # VRF will usually find a hit in the local table, and never reach the VRF + # routing table - this is usually not what you want. Thus we will + # re-arrange the tables and move the local lookup furhter down once VRFs + # are enabled. + + # get current preference on local table + local_pref = [r.get('priority') for r in list_rules() if r.get('table') == 'local'][0] + + # change preference when VRFs are enabled and local lookup table is default + if not local_pref and vrf_config['vrf_add']: + for af in ['-4', '-6']: + _cmd(f'ip {af} rule add pref 32765 table local') + _cmd(f'ip {af} rule del pref 0') + + # return to default lookup preference when no VRF is configured + if not vrf_config['vrf_add']: + for af in ['-4', '-6']: + _cmd(f'ip {af} rule add pref 0 table local') + _cmd(f'ip {af} rule del pref 32765') + + # clean out l3mdev-table rule if present + if 1000 in [r.get('priority') for r in list_rules() if r.get('priority') == 1000]: + _cmd(f'ip {af} rule del pref 1000') + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) |