summaryrefslogtreecommitdiff
path: root/src/conf_mode
diff options
context:
space:
mode:
Diffstat (limited to 'src/conf_mode')
-rwxr-xr-xsrc/conf_mode/conntrack_sync.py6
-rwxr-xr-xsrc/conf_mode/interfaces-bonding.py4
-rwxr-xr-xsrc/conf_mode/interfaces-bridge.py1
-rwxr-xr-xsrc/conf_mode/interfaces-openvpn.py23
-rwxr-xr-xsrc/conf_mode/interfaces-tunnel.py89
-rwxr-xr-xsrc/conf_mode/interfaces-vxlan.py54
-rwxr-xr-xsrc/conf_mode/policy-local-route.py183
-rwxr-xr-xsrc/conf_mode/service_monitoring_telegraf.py175
-rwxr-xr-xsrc/conf_mode/vrf.py105
9 files changed, 495 insertions, 145 deletions
diff --git a/src/conf_mode/conntrack_sync.py b/src/conf_mode/conntrack_sync.py
index 7f22fa2dd..d4bf14d52 100755
--- a/src/conf_mode/conntrack_sync.py
+++ b/src/conf_mode/conntrack_sync.py
@@ -82,9 +82,9 @@ def verify(conntrack):
raise ConfigError('Cannot configure all with other protocol')
if 'listen_address' in conntrack:
- address = conntrack['listen_address']
- if not is_addr_assigned(address):
- raise ConfigError(f'Specified listen-address {address} not assigned to any interface!')
+ for address in conntrack['listen_address']:
+ if not is_addr_assigned(address):
+ raise ConfigError(f'Specified listen-address {address} not assigned to any interface!')
vrrp_group = dict_search('failover_mechanism.vrrp.sync_group', conntrack)
if vrrp_group == None:
diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py
index 431d65f1f..d5be21949 100755
--- a/src/conf_mode/interfaces-bonding.py
+++ b/src/conf_mode/interfaces-bonding.py
@@ -132,10 +132,10 @@ def verify(bond):
return None
if 'arp_monitor' in bond:
- if 'target' in bond['arp_monitor'] and len(int(bond['arp_monitor']['target'])) > 16:
+ if 'target' in bond['arp_monitor'] and len(bond['arp_monitor']['target']) > 16:
raise ConfigError('The maximum number of arp-monitor targets is 16')
- if 'interval' in bond['arp_monitor'] and len(int(bond['arp_monitor']['interval'])) > 0:
+ if 'interval' in bond['arp_monitor'] and int(bond['arp_monitor']['interval']) > 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')
diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py
index 4d3ebc587..f4dba9d4a 100755
--- a/src/conf_mode/interfaces-bridge.py
+++ b/src/conf_mode/interfaces-bridge.py
@@ -22,7 +22,6 @@ from netifaces import interfaces
from vyos.config import Config
from vyos.configdict import get_interface_dict
from vyos.configdict import node_changed
-from vyos.configdict import leaf_node_changed
from vyos.configdict import is_member
from vyos.configdict import is_source_interface
from vyos.configdict import has_vlan_subinterface_configured
diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index ffb022847..38ed127ff 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2021 VyOS maintainers and contributors
+# Copyright (C) 2019-2022 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
@@ -29,6 +29,7 @@ from shutil import rmtree
from vyos.config import Config
from vyos.configdict import get_interface_dict
+from vyos.configdict import leaf_node_changed
from vyos.configverify import verify_vrf
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_diffie_hellman_length
@@ -81,6 +82,10 @@ def get_config(config=None):
base = ['interfaces', 'openvpn']
openvpn = get_interface_dict(conf, base)
+ if 'deleted' not in openvpn:
+ tmp = leaf_node_changed(conf, ['openvpn-option'])
+ if tmp: openvpn['restart_required'] = ''
+
openvpn['auth_user_pass_file'] = '/run/openvpn/{ifname}.pw'.format(**openvpn)
return openvpn
@@ -118,11 +123,12 @@ def verify(openvpn):
if 'local_address' not in openvpn and 'is_bridge_member' not in openvpn:
raise ConfigError('Must specify "local-address" or add interface to bridge')
- if len([addr for addr in openvpn['local_address'] if is_ipv4(addr)]) > 1:
- raise ConfigError('Only one IPv4 local-address can be specified')
+ if 'local_address' in openvpn:
+ if len([addr for addr in openvpn['local_address'] if is_ipv4(addr)]) > 1:
+ raise ConfigError('Only one IPv4 local-address can be specified')
- if len([addr for addr in openvpn['local_address'] if is_ipv6(addr)]) > 1:
- raise ConfigError('Only one IPv6 local-address can be specified')
+ if len([addr for addr in openvpn['local_address'] if is_ipv6(addr)]) > 1:
+ raise ConfigError('Only one IPv6 local-address can be specified')
if openvpn['device_type'] == 'tun':
if 'remote_address' not in openvpn:
@@ -161,7 +167,7 @@ def verify(openvpn):
if dict_search('remote_host', openvpn) in dict_search('remote_address', openvpn):
raise ConfigError('"remote-address" and "remote-host" can not be the same')
- if openvpn['device_type'] == 'tap':
+ if openvpn['device_type'] == 'tap' and 'local_address' in openvpn:
# we can only have one local_address, this is ensured above
v4addr = None
for laddr in openvpn['local_address']:
@@ -516,7 +522,10 @@ def apply(openvpn):
# No matching OpenVPN process running - maybe it got killed or none
# existed - nevertheless, spawn new OpenVPN process
- call(f'systemctl reload-or-restart openvpn@{interface}.service')
+ action = 'reload-or-restart'
+ if 'restart_required' in openvpn:
+ action = 'restart'
+ call(f'systemctl {action} openvpn@{interface}.service')
conf = VTunIf.get_config()
conf['device_type'] = openvpn['device_type']
diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py
index 2798d321f..c4023586a 100755
--- a/src/conf_mode/interfaces-tunnel.py
+++ b/src/conf_mode/interfaces-tunnel.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2021 VyOS maintainers and contributors
+# Copyright (C) 2018-2022 yOS 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
@@ -18,12 +18,9 @@ import os
from sys import exit
from netifaces import interfaces
-from ipaddress import IPv4Address
from vyos.config import Config
-from vyos.configdict import dict_merge
from vyos.configdict import get_interface_dict
-from vyos.configdict import node_changed
from vyos.configdict import leaf_node_changed
from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
@@ -34,8 +31,6 @@ from vyos.configverify import verify_tunnel
from vyos.ifconfig import Interface
from vyos.ifconfig import Section
from vyos.ifconfig import TunnelIf
-from vyos.template import is_ipv4
-from vyos.template import is_ipv6
from vyos.util import get_interface_config
from vyos.util import dict_search
from vyos import ConfigError
@@ -54,8 +49,24 @@ def get_config(config=None):
base = ['interfaces', 'tunnel']
tunnel = get_interface_dict(conf, base)
- tmp = leaf_node_changed(conf, ['encapsulation'])
- if tmp: tunnel.update({'encapsulation_changed': {}})
+ if 'deleted' not in tunnel:
+ tmp = leaf_node_changed(conf, ['encapsulation'])
+ if tmp: tunnel.update({'encapsulation_changed': {}})
+
+ # We also need to inspect other configured tunnels as there are Kernel
+ # restrictions where we need to comply. E.g. GRE tunnel key can't be used
+ # twice, or with multiple GRE tunnels to the same location we must specify
+ # a GRE key
+ conf.set_level(base)
+ tunnel['other_tunnels'] = conf.get_config_dict([], key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
+ # delete our own instance from this dict
+ ifname = tunnel['ifname']
+ del tunnel['other_tunnels'][ifname]
+ # if only one tunnel is present on the system, no need to keep this key
+ if len(tunnel['other_tunnels']) == 0:
+ del tunnel['other_tunnels']
# We must check if our interface is configured to be a DMVPN member
nhrp_base = ['protocols', 'nhrp', 'tunnel']
@@ -76,35 +87,47 @@ def verify(tunnel):
verify_tunnel(tunnel)
- # If tunnel source address any and key not set
+ # If tunnel source is any and gre key is not set
+ interface = tunnel['ifname']
if tunnel['encapsulation'] in ['gre'] and \
dict_search('source_address', tunnel) == '0.0.0.0' and \
dict_search('parameters.ip.key', tunnel) == None:
- raise ConfigError('Tunnel parameters ip key must be set!')
-
- if tunnel['encapsulation'] in ['gre', 'gretap']:
- if dict_search('parameters.ip.key', tunnel) != None:
- # Check pairs tunnel source-address/encapsulation/key with exists tunnels.
- # Prevent the same key for 2 tunnels with same source-address/encap. T2920
- for tunnel_if in Section.interfaces('tunnel'):
- # It makes no sense to run the test for re-used GRE keys on our
- # own interface we are currently working on
- if tunnel['ifname'] == tunnel_if:
- continue
- tunnel_cfg = get_interface_config(tunnel_if)
- # no match on encapsulation - bail out
- if dict_search('linkinfo.info_kind', tunnel_cfg) != tunnel['encapsulation']:
- continue
- new_source_address = dict_search('source_address', tunnel)
- # Convert tunnel key to ip key, format "ip -j link show"
- # 1 => 0.0.0.1, 999 => 0.0.3.231
- orig_new_key = dict_search('parameters.ip.key', tunnel)
- new_key = IPv4Address(int(orig_new_key))
- new_key = str(new_key)
- if dict_search('address', tunnel_cfg) == new_source_address and \
- dict_search('linkinfo.info_data.ikey', tunnel_cfg) == new_key:
- raise ConfigError(f'Key "{orig_new_key}" for source-address "{new_source_address}" ' \
+ raise ConfigError(f'"parameters ip key" must be set for {interface} when '\
+ 'encapsulation is GRE!')
+
+ gre_encapsulations = ['gre', 'gretap']
+ if tunnel['encapsulation'] in gre_encapsulations and 'other_tunnels' in tunnel:
+ # Check pairs tunnel source-address/encapsulation/key with exists tunnels.
+ # Prevent the same key for 2 tunnels with same source-address/encap. T2920
+ for o_tunnel, o_tunnel_conf in tunnel['other_tunnels'].items():
+ # no match on encapsulation - bail out
+ our_encapsulation = tunnel['encapsulation']
+ their_encapsulation = o_tunnel_conf['encapsulation']
+ if our_encapsulation in gre_encapsulations and their_encapsulation \
+ not in gre_encapsulations:
+ continue
+
+ our_address = dict_search('source_address', tunnel)
+ our_key = dict_search('parameters.ip.key', tunnel)
+ their_address = dict_search('source_address', o_tunnel_conf)
+ their_key = dict_search('parameters.ip.key', o_tunnel_conf)
+ if our_key != None:
+ if their_address == our_address and their_key == our_key:
+ raise ConfigError(f'Key "{our_key}" for source-address "{our_address}" ' \
f'is already used for tunnel "{tunnel_if}"!')
+ else:
+ our_source_if = dict_search('source_interface', tunnel)
+ their_source_if = dict_search('source_interface', o_tunnel_conf)
+ our_remote = dict_search('remote', tunnel)
+ their_remote = dict_search('remote', o_tunnel_conf)
+ # If no IP GRE key is defined we can not have more then one GRE tunnel
+ # bound to any one interface/IP address and the same remote. This will
+ # result in a OS PermissionError: add tunnel "gre0" failed: File exists
+ if (their_address == our_address or our_source_if == their_source_if) and \
+ our_remote == their_remote:
+ raise ConfigError(f'Missing required "ip key" parameter when '\
+ 'running more then one GRE based tunnel on the '\
+ 'same source-interface/source-address')
# Keys are not allowed with ipip and sit tunnels
if tunnel['encapsulation'] in ['ipip', 'sit']:
diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py
index c035557f0..5ee4603af 100755
--- a/src/conf_mode/interfaces-vxlan.py
+++ b/src/conf_mode/interfaces-vxlan.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2020 VyOS maintainers and contributors
+# Copyright (C) 2019-2022 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
@@ -21,6 +21,7 @@ from netifaces import interfaces
from vyos.config import Config
from vyos.configdict import get_interface_dict
+from vyos.configdict import leaf_node_changed
from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_mtu_ipv6
@@ -34,8 +35,8 @@ airbag.enable()
def get_config(config=None):
"""
- Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
- interface name will be added or a deleted flag
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at least
+ the interface name will be added or a deleted flag
"""
if config:
conf = config
@@ -44,6 +45,16 @@ def get_config(config=None):
base = ['interfaces', 'vxlan']
vxlan = get_interface_dict(conf, base)
+ # VXLAN interfaces are picky and require recreation if certain parameters
+ # change. But a VXLAN interface should - of course - not be re-created if
+ # it's description or IP address is adjusted. Feels somehow logic doesn't it?
+ for cli_option in ['external', 'gpe', 'group', 'port', 'remote',
+ 'source-address', 'source-interface', 'vni',
+ 'parameters ip dont-fragment', 'parameters ip tos',
+ 'parameters ip ttl']:
+ if leaf_node_changed(conf, cli_option.split()):
+ vxlan.update({'rebuild_required': {}})
+
return vxlan
def verify(vxlan):
@@ -56,8 +67,7 @@ def verify(vxlan):
if 'group' in vxlan:
if 'source_interface' not in vxlan:
- raise ConfigError('Multicast VXLAN requires an underlaying interface ')
-
+ raise ConfigError('Multicast VXLAN requires an underlaying interface')
verify_source_interface(vxlan)
if not any(tmp in ['group', 'remote', 'source_address'] for tmp in vxlan):
@@ -81,22 +91,41 @@ def verify(vxlan):
raise ConfigError(f'Underlaying device MTU is to small ({lower_mtu} '\
f'bytes) for VXLAN overhead ({vxlan_overhead} bytes!)')
+ # Check for mixed IPv4 and IPv6 addresses
+ protocol = None
+ if 'source_address' in vxlan:
+ if is_ipv6(vxlan['source_address']):
+ protocol = 'ipv6'
+ else:
+ protocol = 'ipv4'
+
+ if 'remote' in vxlan:
+ error_msg = 'Can not mix both IPv4 and IPv6 for VXLAN underlay'
+ for remote in vxlan['remote']:
+ if is_ipv6(remote):
+ if protocol == 'ipv4':
+ raise ConfigError(error_msg)
+ protocol = 'ipv6'
+ else:
+ if protocol == 'ipv6':
+ raise ConfigError(error_msg)
+ protocol = 'ipv4'
+
verify_mtu_ipv6(vxlan)
verify_address(vxlan)
return None
-
def generate(vxlan):
return None
-
def apply(vxlan):
# Check if the VXLAN interface already exists
- if vxlan['ifname'] in interfaces():
- v = VXLANIf(vxlan['ifname'])
- # VXLAN is super picky and the tunnel always needs to be recreated,
- # thus we can simply always delete it first.
- v.remove()
+ if 'rebuild_required' in vxlan or 'delete' in vxlan:
+ if vxlan['ifname'] in interfaces():
+ v = VXLANIf(vxlan['ifname'])
+ # VXLAN is super picky and the tunnel always needs to be recreated,
+ # thus we can simply always delete it first.
+ v.remove()
if 'deleted' not in vxlan:
# This is a special type of interface which needs additional parameters
@@ -116,7 +145,6 @@ def apply(vxlan):
return None
-
if __name__ == '__main__':
try:
c = get_config()
diff --git a/src/conf_mode/policy-local-route.py b/src/conf_mode/policy-local-route.py
index 013f22665..0a4597869 100755
--- a/src/conf_mode/policy-local-route.py
+++ b/src/conf_mode/policy-local-route.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020 VyOS maintainers and contributors
+# Copyright (C) 2020-2021 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
@@ -18,6 +18,7 @@ import os
from sys import exit
+from netifaces import interfaces
from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.configdict import node_changed
@@ -35,27 +36,96 @@ def get_config(config=None):
conf = config
else:
conf = Config()
- base = ['policy', 'local-route']
+ base = ['policy']
+
pbr = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
- # delete policy local-route
- dict = {}
- tmp = node_changed(conf, ['policy', 'local-route', 'rule'], key_mangling=('-', '_'))
- if tmp:
- for rule in (tmp or []):
- src = leaf_node_changed(conf, ['policy', 'local-route', 'rule', rule, 'source'])
- if src:
- dict = dict_merge({'rule_remove' : {rule : {'source' : src}}}, dict)
+ for route in ['local_route', 'local_route6']:
+ dict_id = 'rule_remove' if route == 'local_route' else 'rule6_remove'
+ route_key = 'local-route' if route == 'local_route' else 'local-route6'
+ base_rule = base + [route_key, 'rule']
+
+ # delete policy local-route
+ dict = {}
+ tmp = node_changed(conf, base_rule, key_mangling=('-', '_'))
+ if tmp:
+ for rule in (tmp or []):
+ src = leaf_node_changed(conf, base_rule + [rule, 'source'])
+ fwmk = leaf_node_changed(conf, base_rule + [rule, 'fwmark'])
+ iif = leaf_node_changed(conf, base_rule + [rule, 'inbound-interface'])
+ dst = leaf_node_changed(conf, base_rule + [rule, 'destination'])
+ rule_def = {}
+ if src:
+ rule_def = dict_merge({'source' : src}, rule_def)
+ if fwmk:
+ rule_def = dict_merge({'fwmark' : fwmk}, rule_def)
+ if iif:
+ rule_def = dict_merge({'inbound_interface' : iif}, rule_def)
+ if dst:
+ rule_def = dict_merge({'destination' : dst}, rule_def)
+ dict = dict_merge({dict_id : {rule : rule_def}}, dict)
pbr.update(dict)
-
- # delete policy local-route rule x source x.x.x.x
- if 'rule' in pbr:
- for rule in pbr['rule']:
- src = leaf_node_changed(conf, ['policy', 'local-route', 'rule', rule, 'source'])
- if src:
- dict = dict_merge({'rule_remove' : {rule : {'source' : src}}}, dict)
+ if fwmk:
+ dict = dict_merge({'rule_remove' : {rule : {'fwmark' : fwmk}}}, dict)
pbr.update(dict)
+ if not route in pbr:
+ continue
+
+ # delete policy local-route rule x source x.x.x.x
+ # delete policy local-route rule x fwmark x
+ # delete policy local-route rule x destination x.x.x.x
+ if 'rule' in pbr[route]:
+ for rule, rule_config in pbr[route]['rule'].items():
+ src = leaf_node_changed(conf, base_rule + [rule, 'source'])
+ fwmk = leaf_node_changed(conf, base_rule + [rule, 'fwmark'])
+ iif = leaf_node_changed(conf, base_rule + [rule, 'inbound-interface'])
+ dst = leaf_node_changed(conf, base_rule + [rule, 'destination'])
+ # keep track of changes in configuration
+ # otherwise we might remove an existing node although nothing else has changed
+ changed = False
+
+ rule_def = {}
+ # src is None if there are no changes to src
+ if src is None:
+ # if src hasn't changed, include it in the removal selector
+ # if a new selector is added, we have to remove all previous rules without this selector
+ # to make sure we remove all previous rules with this source(s), it will be included
+ if 'source' in rule_config:
+ rule_def = dict_merge({'source': rule_config['source']}, rule_def)
+ else:
+ # if src is not None, it's previous content will be returned
+ # this can be an empty array if it's just being set, or the previous value
+ # either way, something has to be changed and we only want to remove previous values
+ changed = True
+ # set the old value for removal if it's not empty
+ if len(src) > 0:
+ rule_def = dict_merge({'source' : src}, rule_def)
+ if fwmk is None:
+ if 'fwmark' in rule_config:
+ rule_def = dict_merge({'fwmark': rule_config['fwmark']}, rule_def)
+ else:
+ changed = True
+ if len(fwmk) > 0:
+ rule_def = dict_merge({'fwmark' : fwmk}, rule_def)
+ if iif is None:
+ if 'inbound_interface' in rule_config:
+ rule_def = dict_merge({'inbound_interface': rule_config['inbound_interface']}, rule_def)
+ else:
+ changed = True
+ if len(iif) > 0:
+ rule_def = dict_merge({'inbound_interface' : iif}, rule_def)
+ if dst is None:
+ if 'destination' in rule_config:
+ rule_def = dict_merge({'destination': rule_config['destination']}, rule_def)
+ else:
+ changed = True
+ if len(dst) > 0:
+ rule_def = dict_merge({'destination' : dst}, rule_def)
+ if changed:
+ dict = dict_merge({dict_id : {rule : rule_def}}, dict)
+ pbr.update(dict)
+
return pbr
def verify(pbr):
@@ -63,13 +133,25 @@ def verify(pbr):
if not pbr:
return None
- if 'rule' in pbr:
- for rule in pbr['rule']:
- if 'source' not in pbr['rule'][rule]:
- raise ConfigError('Source address required!')
- else:
- if 'set' not in pbr['rule'][rule] or 'table' not in pbr['rule'][rule]['set']:
- raise ConfigError('Table set is required!')
+ for route in ['local_route', 'local_route6']:
+ if not route in pbr:
+ continue
+
+ pbr_route = pbr[route]
+ if 'rule' in pbr_route:
+ for rule in pbr_route['rule']:
+ if 'source' not in pbr_route['rule'][rule] \
+ and 'destination' not in pbr_route['rule'][rule] \
+ and 'fwmark' not in pbr_route['rule'][rule] \
+ and 'inbound_interface' not in pbr_route['rule'][rule]:
+ raise ConfigError('Source or destination address or fwmark or inbound-interface is required!')
+ else:
+ if 'set' not in pbr_route['rule'][rule] or 'table' not in pbr_route['rule'][rule]['set']:
+ raise ConfigError('Table set is required!')
+ if 'inbound_interface' in pbr_route['rule'][rule]:
+ interface = pbr_route['rule'][rule]['inbound_interface']
+ if interface not in interfaces():
+ raise ConfigError(f'Interface "{interface}" does not exist')
return None
@@ -84,18 +166,51 @@ def apply(pbr):
return None
# Delete old rule if needed
- if 'rule_remove' in pbr:
- for rule in pbr['rule_remove']:
- for src in pbr['rule_remove'][rule]['source']:
- call(f'ip rule del prio {rule} from {src}')
+ for rule_rm in ['rule_remove', 'rule6_remove']:
+ if rule_rm in pbr:
+ v6 = " -6" if rule_rm == 'rule6_remove' else ""
+ for rule, rule_config in pbr[rule_rm].items():
+ rule_config['source'] = rule_config['source'] if 'source' in rule_config else ['']
+ for src in rule_config['source']:
+ f_src = '' if src == '' else f' from {src} '
+ rule_config['destination'] = rule_config['destination'] if 'destination' in rule_config else ['']
+ for dst in rule_config['destination']:
+ f_dst = '' if dst == '' else f' to {dst} '
+ rule_config['fwmark'] = rule_config['fwmark'] if 'fwmark' in rule_config else ['']
+ for fwmk in rule_config['fwmark']:
+ f_fwmk = '' if fwmk == '' else f' fwmark {fwmk} '
+ rule_config['inbound_interface'] = rule_config['inbound_interface'] if 'inbound_interface' in rule_config else ['']
+ for iif in rule_config['inbound_interface']:
+ f_iif = '' if iif == '' else f' iif {iif} '
+ call(f'ip{v6} rule del prio {rule} {f_src}{f_dst}{f_fwmk}{f_iif}')
# Generate new config
- if 'rule' in pbr:
- for rule in pbr['rule']:
- table = pbr['rule'][rule]['set']['table']
- if pbr['rule'][rule]['source']:
- for src in pbr['rule'][rule]['source']:
- call(f'ip rule add prio {rule} from {src} lookup {table}')
+ for route in ['local_route', 'local_route6']:
+ if not route in pbr:
+ continue
+
+ v6 = " -6" if route == 'local_route6' else ""
+
+ pbr_route = pbr[route]
+ if 'rule' in pbr_route:
+ for rule, rule_config in pbr_route['rule'].items():
+ table = rule_config['set']['table']
+
+ rule_config['source'] = rule_config['source'] if 'source' in rule_config else ['all']
+ for src in rule_config['source'] or ['all']:
+ f_src = '' if src == '' else f' from {src} '
+ rule_config['destination'] = rule_config['destination'] if 'destination' in rule_config else ['all']
+ for dst in rule_config['destination']:
+ f_dst = '' if dst == '' else f' to {dst} '
+ f_fwmk = ''
+ if 'fwmark' in rule_config:
+ fwmk = rule_config['fwmark']
+ f_fwmk = f' fwmark {fwmk} '
+ f_iif = ''
+ if 'inbound_interface' in rule_config:
+ iif = rule_config['inbound_interface']
+ f_iif = f' iif {iif} '
+ call(f'ip{v6} rule add prio {rule} {f_src}{f_dst}{f_fwmk}{f_iif} lookup {table}')
return None
diff --git a/src/conf_mode/service_monitoring_telegraf.py b/src/conf_mode/service_monitoring_telegraf.py
new file mode 100755
index 000000000..8a972b9fe
--- /dev/null
+++ b/src/conf_mode/service_monitoring_telegraf.py
@@ -0,0 +1,175 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021-2022 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 json
+
+from sys import exit
+from shutil import rmtree
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.ifconfig import Section
+from vyos.template import render
+from vyos.util import call
+from vyos.util import chown
+from vyos.util import cmd
+from vyos.xml import defaults
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+
+base_dir = '/run/telegraf'
+cache_dir = f'/etc/telegraf/.cache'
+config_telegraf = f'{base_dir}/vyos-telegraf.conf'
+custom_scripts_dir = '/etc/telegraf/custom_scripts'
+syslog_telegraf = '/etc/rsyslog.d/50-telegraf.conf'
+systemd_telegraf_service = '/etc/systemd/system/vyos-telegraf.service'
+systemd_telegraf_override_dir = '/etc/systemd/system/vyos-telegraf.service.d'
+systemd_override = f'{systemd_telegraf_override_dir}/10-override.conf'
+
+
+def get_interfaces(type='', vlan=True):
+ """
+ Get interfaces
+ get_interfaces()
+ ['dum0', 'eth0', 'eth1', 'eth1.5', 'lo', 'tun0']
+
+ get_interfaces("dummy")
+ ['dum0']
+ """
+ interfaces = []
+ ifaces = Section.interfaces(type)
+ for iface in ifaces:
+ if vlan == False and '.' in iface:
+ continue
+ interfaces.append(iface)
+
+ return interfaces
+
+def get_nft_filter_chains():
+ """
+ Get nft chains for table filter
+ """
+ nft = cmd('nft --json list table ip filter')
+ nft = json.loads(nft)
+ chain_list = []
+
+ for output in nft['nftables']:
+ if 'chain' in output:
+ chain = output['chain']['name']
+ chain_list.append(chain)
+
+ return chain_list
+
+
+def get_config(config=None):
+
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['service', 'monitoring', 'telegraf']
+ if not conf.exists(base):
+ return None
+
+ monitoring = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ # We have gathered the dict representation of the CLI, but there are default
+ # options which we need to update into the dictionary retrived.
+ default_values = defaults(base)
+ monitoring = dict_merge(default_values, monitoring)
+
+ monitoring['custom_scripts_dir'] = custom_scripts_dir
+ monitoring['interfaces_ethernet'] = get_interfaces('ethernet', vlan=False)
+ monitoring['nft_chains'] = get_nft_filter_chains()
+
+ return monitoring
+
+def verify(monitoring):
+ # bail out early - looks like removal from running config
+ if not monitoring:
+ return None
+
+ if 'authentication' not in monitoring or \
+ 'organization' not in monitoring['authentication'] or \
+ 'token' not in monitoring['authentication']:
+ raise ConfigError(f'Authentication "organization and token" are mandatory!')
+
+ if 'url' not in monitoring:
+ raise ConfigError(f'Monitoring "url" is mandatory!')
+
+ return None
+
+def generate(monitoring):
+ if not monitoring:
+ # Delete config and systemd files
+ config_files = [config_telegraf, systemd_telegraf_service, systemd_override, syslog_telegraf]
+ for file in config_files:
+ if os.path.isfile(file):
+ os.unlink(file)
+
+ # Delete old directories
+ if os.path.isdir(cache_dir):
+ rmtree(cache_dir, ignore_errors=True)
+
+ return None
+
+ # Create telegraf cache dir
+ if not os.path.exists(cache_dir):
+ os.makedirs(cache_dir)
+
+ chown(cache_dir, 'telegraf', 'telegraf')
+
+ # Create systemd override dir
+ if not os.path.exists(systemd_telegraf_override_dir):
+ os.mkdir(systemd_telegraf_override_dir)
+
+ # Create custome scripts dir
+ if not os.path.exists(custom_scripts_dir):
+ os.mkdir(custom_scripts_dir)
+
+ # Render telegraf configuration and systemd override
+ render(config_telegraf, 'monitoring/telegraf.tmpl', monitoring)
+ render(systemd_telegraf_service, 'monitoring/systemd_vyos_telegraf_service.tmpl', monitoring)
+ render(systemd_override, 'monitoring/override.conf.tmpl', monitoring, permission=0o640)
+ render(syslog_telegraf, 'monitoring/syslog_telegraf.tmpl', monitoring)
+
+ chown(base_dir, 'telegraf', 'telegraf')
+
+ return None
+
+def apply(monitoring):
+ # Reload systemd manager configuration
+ call('systemctl daemon-reload')
+ if monitoring:
+ call('systemctl restart vyos-telegraf.service')
+ else:
+ call('systemctl stop vyos-telegraf.service')
+ # Telegraf include custom rsyslog config changes
+ call('systemctl restart rsyslog')
+
+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/vrf.py b/src/conf_mode/vrf.py
index 9d36819be..e64f35ab5 100755
--- a/src/conf_mode/vrf.py
+++ b/src/conf_mode/vrf.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-2021 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 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
@@ -26,16 +26,23 @@ from vyos.template import render
from vyos.util import call
from vyos.util import cmd
from vyos.util import dict_search
+from vyos.util import sysctl
from vyos import ConfigError
from vyos import airbag
airbag.enable()
config_file = r'/etc/iproute2/rt_tables.d/vyos-vrf.conf'
-def list_rules():
- command = 'ip -j -4 rule show'
- answer = loads(cmd(command))
- return [_ for _ in answer if _]
+def has_rule(af : str, priority : int, table : str):
+ """ Check if a given ip rule exists """
+ if af not in ['-4', '-6']:
+ raise ValueError()
+ command = f'ip -j {af} rule show'
+ for tmp in loads(cmd(command)):
+ if {'priority', 'table'} <= set(tmp):
+ if tmp['priority'] == priority and tmp['table'] == table:
+ return True
+ return False
def vrf_interfaces(c, match):
matched = []
@@ -64,7 +71,6 @@ def vrf_routing(c, match):
c.set_level(old_level)
return matched
-
def get_config(config=None):
if config:
conf = config
@@ -138,36 +144,63 @@ def apply(vrf):
bind_all = '0'
if 'bind-to-all' in vrf:
bind_all = '1'
- call(f'sysctl -wq net.ipv4.tcp_l3mdev_accept={bind_all}')
- call(f'sysctl -wq net.ipv4.udp_l3mdev_accept={bind_all}')
+ sysctl('net.ipv4.tcp_l3mdev_accept', bind_all)
+ sysctl('net.ipv4.udp_l3mdev_accept', bind_all)
for tmp in (dict_search('vrf_remove', vrf) or []):
if os.path.isdir(f'/sys/class/net/{tmp}'):
- call(f'ip -4 route del vrf {tmp} unreachable default metric 4278198272')
- call(f'ip -6 route del vrf {tmp} unreachable default metric 4278198272')
call(f'ip link delete dev {tmp}')
if 'name' in vrf:
+ # 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 further down once VRFs
+ # are enabled.
+ #
+ # Thanks to https://stbuehler.de/blog/article/2020/02/29/using_vrf__virtual_routing_and_forwarding__on_linux.html
+
+ for afi in ['-4', '-6']:
+ # move lookup local to pref 32765 (from 0)
+ if not has_rule(afi, 32765, 'local'):
+ call(f'ip {afi} rule add pref 32765 table local')
+ if has_rule(afi, 0, 'local'):
+ call(f'ip {afi} rule del pref 0')
+ # make sure that in VRFs after failed lookup in the VRF specific table
+ # nothing else is reached
+ if not has_rule(afi, 1000, 'l3mdev'):
+ # this should be added by the kernel when a VRF is created
+ # add it here for completeness
+ call(f'ip {afi} rule add pref 1000 l3mdev protocol kernel')
+
+ # add another rule with an unreachable target which only triggers in VRF context
+ # if a route could not be reached
+ if not has_rule(afi, 2000, 'l3mdev'):
+ call(f'ip {afi} rule add pref 2000 l3mdev unreachable')
+
for name, config in vrf['name'].items():
table = config['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
call(f'ip link add {name} type vrf table {table}')
- # The kernel Documentation/networking/vrf.txt also recommends
- # adding unreachable routes to the VRF routing tables so that routes
- # afterwards are taken.
- call(f'ip -4 route add vrf {name} unreachable default metric 4278198272')
- call(f'ip -6 route add vrf {name} unreachable default metric 4278198272')
- # We also should add proper loopback IP addresses to the newly
- # created VRFs for services bound to the loopback address (SNMP, NTP)
- call(f'ip -4 addr add 127.0.0.1/8 dev {name}')
- call(f'ip -6 addr add ::1/128 dev {name}')
# set VRF description for e.g. SNMP monitoring
vrf_if = Interface(name)
+ # We also should add proper loopback IP addresses to the newly
+ # created VRFs for services bound to the loopback address (SNMP, NTP)
+ vrf_if.add_addr('127.0.0.1/8')
+ vrf_if.add_addr('::1/128')
+ # add VRF description if available
vrf_if.set_alias(config.get('description', ''))
+
# Enable/Disable of an interface must always be done at the end of the
# derived class to make use of the ref-counting set_admin_state()
# function. We will only enable the interface if 'up' was called as
@@ -178,38 +211,6 @@ def apply(vrf):
state = 'down' if 'disable' in config else 'up'
vrf_if.set_admin_state(state)
- # 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 'name' in vrf:
- for af in ['-4', '-6']:
- call(f'ip {af} rule add pref 32765 table local')
- call(f'ip {af} rule del pref 0')
-
- # return to default lookup preference when no VRF is configured
- if 'name' not in vrf:
- for af in ['-4', '-6']:
- call(f'ip {af} rule add pref 0 table local')
- call(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]:
- call(f'ip {af} rule del pref 1000')
-
return None
if __name__ == '__main__':