summaryrefslogtreecommitdiff
path: root/src/conf_mode
diff options
context:
space:
mode:
Diffstat (limited to 'src/conf_mode')
-rwxr-xr-xsrc/conf_mode/interfaces-bonding.py41
-rwxr-xr-xsrc/conf_mode/interfaces-bridge.py17
-rwxr-xr-xsrc/conf_mode/interfaces-dummy.py18
-rwxr-xr-xsrc/conf_mode/interfaces-ethernet.py39
-rwxr-xr-xsrc/conf_mode/interfaces-pppoe.py149
-rwxr-xr-xsrc/conf_mode/interfaces-pseudo-ethernet.py54
-rwxr-xr-xsrc/conf_mode/interfaces-vxlan.py7
-rwxr-xr-xsrc/conf_mode/interfaces-wireless.py46
-rwxr-xr-xsrc/conf_mode/system-login-banner.py19
-rwxr-xr-xsrc/conf_mode/system-login.py8
-rwxr-xr-xsrc/conf_mode/vrf.py281
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)