summaryrefslogtreecommitdiff
path: root/src/conf_mode
diff options
context:
space:
mode:
Diffstat (limited to 'src/conf_mode')
-rwxr-xr-xsrc/conf_mode/conntrack_sync.py5
-rwxr-xr-xsrc/conf_mode/container.py38
-rwxr-xr-xsrc/conf_mode/dns_forwarding.py9
-rwxr-xr-xsrc/conf_mode/firewall-interface.py11
-rwxr-xr-xsrc/conf_mode/firewall.py215
-rwxr-xr-xsrc/conf_mode/flow_accounting_conf.py16
-rwxr-xr-xsrc/conf_mode/high-availability.py8
-rwxr-xr-xsrc/conf_mode/interfaces-bonding.py16
-rwxr-xr-xsrc/conf_mode/interfaces-bridge.py6
-rwxr-xr-xsrc/conf_mode/interfaces-openvpn.py43
-rwxr-xr-xsrc/conf_mode/ntp.py20
-rwxr-xr-xsrc/conf_mode/pki.py129
-rwxr-xr-xsrc/conf_mode/policy-route-interface.py12
-rwxr-xr-xsrc/conf_mode/policy-route.py72
-rwxr-xr-xsrc/conf_mode/policy.py10
-rwxr-xr-xsrc/conf_mode/protocols_bgp.py14
-rwxr-xr-xsrc/conf_mode/protocols_eigrp.py123
-rwxr-xr-xsrc/conf_mode/protocols_nhrp.py4
-rwxr-xr-xsrc/conf_mode/protocols_rip.py2
-rwxr-xr-xsrc/conf_mode/service_event_handler.py91
-rwxr-xr-xsrc/conf_mode/service_ipoe-server.py14
-rwxr-xr-xsrc/conf_mode/service_monitoring_telegraf.py53
-rwxr-xr-xsrc/conf_mode/service_router-advert.py34
-rwxr-xr-xsrc/conf_mode/service_sla.py113
-rwxr-xr-xsrc/conf_mode/snmp.py10
-rwxr-xr-xsrc/conf_mode/ssh.py19
-rwxr-xr-xsrc/conf_mode/system-ip.py5
-rwxr-xr-xsrc/conf_mode/system-syslog.py2
-rwxr-xr-xsrc/conf_mode/system_frr.py91
-rwxr-xr-xsrc/conf_mode/vpn_sstp.py8
-rwxr-xr-xsrc/conf_mode/vrf.py20
-rwxr-xr-xsrc/conf_mode/zone_policy.py2
32 files changed, 1059 insertions, 156 deletions
diff --git a/src/conf_mode/conntrack_sync.py b/src/conf_mode/conntrack_sync.py
index 311e01529..c4b2bb488 100755
--- a/src/conf_mode/conntrack_sync.py
+++ b/src/conf_mode/conntrack_sync.py
@@ -116,6 +116,7 @@ def generate(conntrack):
return None
def apply(conntrack):
+ systemd_service = 'conntrackd.service'
if not conntrack:
# Failover mechanism daemon should be indicated that it no longer needs
# to execute conntrackd actions on transition. This is only required
@@ -123,7 +124,7 @@ def apply(conntrack):
if process_named_running('conntrackd'):
resync_vrrp()
- call('systemctl stop conntrackd.service')
+ call(f'systemctl stop {systemd_service}')
return None
# Failover mechanism daemon should be indicated that it needs to execute
@@ -132,7 +133,7 @@ def apply(conntrack):
if not process_named_running('conntrackd'):
resync_vrrp()
- call('systemctl restart conntrackd.service')
+ call(f'systemctl reload-or-restart {systemd_service}')
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py
index 7e1dc5911..ac3dc536b 100755
--- a/src/conf_mode/container.py
+++ b/src/conf_mode/container.py
@@ -15,13 +15,13 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
-import json
from ipaddress import ip_address
from ipaddress import ip_network
from time import sleep
from json import dumps as json_write
+from vyos.base import Warning
from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.configdict import node_changed
@@ -90,10 +90,10 @@ def get_config(config=None):
container['name'][name] = dict_merge(default_values, container['name'][name])
# Delete container network, delete containers
- tmp = node_changed(conf, base + ['container', 'network'])
+ tmp = node_changed(conf, base + ['network'])
if tmp: container.update({'network_remove' : tmp})
- tmp = node_changed(conf, base + ['container', 'name'])
+ tmp = node_changed(conf, base + ['name'])
if tmp: container.update({'container_remove' : tmp})
return container
@@ -110,15 +110,21 @@ def verify(container):
if 'image' not in container_config:
raise ConfigError(f'Container image for "{name}" is mandatory!')
- # verify container image exists locally
- image = container_config['image']
-
# Check if requested container image exists locally. If it does not
- # exist locally - inform the user.
+ # exist locally - inform the user. This is required as there is a
+ # shared container image storage accross all VyOS images. A user can
+ # delete a container image from the system, boot into another version
+ # of VyOS and then it would fail to boot. This is to prevent any
+ # configuration error when container images are deleted from the
+ # global storage. A per image local storage would be a super waste
+ # of diskspace as there will be a full copy (up tu several GB/image)
+ # on upgrade. This is the "cheapest" and fastest solution in terms
+ # of image upgrade and deletion.
+ image = container_config['image']
if run(f'podman image exists {image}') != 0:
- raise ConfigError(f'Image "{image}" used in contianer "{name}" does not exist '\
- f'locally.\nPlease use "add container image {image}" to add it '\
- 'to the system!')
+ Warning(f'Image "{image}" used in contianer "{name}" does not exist '\
+ f'locally. Please use "add container image {image}" to add it '\
+ f'to the system! Container "{name}" will not be started!')
if 'network' in container_config:
if len(container_config['network']) > 1:
@@ -126,7 +132,7 @@ def verify(container):
# Check if the specified container network exists
network_name = list(container_config['network'])[0]
- if network_name not in container['network']:
+ if network_name not in container.get('network', {}):
raise ConfigError(f'Container network "{network_name}" does not exist!')
if 'address' in container_config['network'][network_name]:
@@ -264,12 +270,13 @@ def apply(container):
# Option "--force" allows to delete containers with any status
if 'container_remove' in container:
for name in container['container_remove']:
- call(f'podman stop {name}')
+ call(f'podman stop --time 3 {name}')
call(f'podman rm --force {name}')
# Delete old networks if needed
if 'network_remove' in container:
for network in container['network_remove']:
+ call(f'podman network rm {network}')
tmp = f'/etc/cni/net.d/{network}.conflist'
if os.path.exists(tmp):
os.unlink(tmp)
@@ -279,11 +286,16 @@ def apply(container):
for name, container_config in container['name'].items():
image = container_config['image']
+ if run(f'podman image exists {image}') != 0:
+ # container image does not exist locally - user already got
+ # informed by a WARNING in verfiy() - bail out early
+ continue
+
if 'disable' in container_config:
# check if there is a container by that name running
tmp = _cmd('podman ps -a --format "{{.Names}}"')
if name in tmp:
- _cmd(f'podman stop {name}')
+ _cmd(f'podman stop --time 3 {name}')
_cmd(f'podman rm --force {name}')
continue
diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py
index f1c2d1f43..d0d87d73e 100755
--- a/src/conf_mode/dns_forwarding.py
+++ b/src/conf_mode/dns_forwarding.py
@@ -98,6 +98,9 @@ def get_config(config=None):
dns['authoritative_zone_errors'].append('{}.{}: at least one address is required'.format(subnode, node))
continue
+ if subnode == 'any':
+ subnode = '*'
+
for address in rdata['address']:
zone['records'].append({
'name': subnode,
@@ -263,6 +266,12 @@ def verify(dns):
if 'server' not in dns['domain'][domain]:
raise ConfigError(f'No server configured for domain {domain}!')
+ if 'dns64_prefix' in dns:
+ dns_prefix = dns['dns64_prefix'].split('/')[1]
+ # RFC 6147 requires prefix /96
+ if int(dns_prefix) != 96:
+ raise ConfigError('DNS 6to4 prefix must be of length /96')
+
if ('authoritative_zone_errors' in dns) and dns['authoritative_zone_errors']:
for error in dns['authoritative_zone_errors']:
print(error)
diff --git a/src/conf_mode/firewall-interface.py b/src/conf_mode/firewall-interface.py
index 9a5d278e9..ab1c69259 100755
--- a/src/conf_mode/firewall-interface.py
+++ b/src/conf_mode/firewall-interface.py
@@ -64,6 +64,11 @@ def get_config(config=None):
return if_firewall
+def verify_chain(table, chain):
+ # Verify firewall applied
+ code = run(f'nft list chain {table} {chain}')
+ return code == 0
+
def verify(if_firewall):
# bail out early - looks like removal from running config
if not if_firewall:
@@ -80,6 +85,9 @@ def verify(if_firewall):
if name not in if_firewall['firewall']['name']:
raise ConfigError(f'Invalid firewall name "{name}"')
+ if not verify_chain('ip filter', f'{NAME_PREFIX}{name}'):
+ raise ConfigError('Firewall did not apply')
+
if 'ipv6_name' in if_firewall[direction]:
name = if_firewall[direction]['ipv6_name']
@@ -89,6 +97,9 @@ def verify(if_firewall):
if name not in if_firewall['firewall']['ipv6_name']:
raise ConfigError(f'Invalid firewall ipv6-name "{name}"')
+ if not verify_chain('ip6 filter', f'{NAME6_PREFIX}{name}'):
+ raise ConfigError('Firewall did not apply')
+
return None
def generate(if_firewall):
diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py
index 6924bf555..07eca722f 100755
--- a/src/conf_mode/firewall.py
+++ b/src/conf_mode/firewall.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021 VyOS maintainers and contributors
+# 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
@@ -26,9 +26,17 @@ from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.configdict import node_changed
from vyos.configdiff import get_config_diff, Diff
+from vyos.firewall import geoip_update
+from vyos.firewall import get_ips_domains_dict
+from vyos.firewall import nft_add_set_elements
+from vyos.firewall import nft_flush_set
+from vyos.firewall import nft_init_set
+from vyos.firewall import nft_update_set_elements
from vyos.template import render
+from vyos.util import call
from vyos.util import cmd
from vyos.util import dict_search_args
+from vyos.util import dict_search_recursive
from vyos.util import process_named_running
from vyos.util import run
from vyos.xml import defaults
@@ -79,10 +87,26 @@ nft6_iface_chains = ['VYOS_FW6_FORWARD', 'VYOS_FW6_OUTPUT', 'VYOS_FW6_LOCAL']
valid_groups = [
'address_group',
+ 'domain_group',
'network_group',
'port_group'
]
+nested_group_types = [
+ 'address_group', 'network_group', 'mac_group',
+ 'port_group', 'ipv6_address_group', 'ipv6_network_group'
+]
+
+group_set_prefix = {
+ 'A_': 'address_group',
+ 'A6_': 'ipv6_address_group',
+ 'D_': 'domain_group',
+ 'M_': 'mac_group',
+ 'N_': 'network_group',
+ 'N6_': 'ipv6_network_group',
+ 'P_': 'port_group'
+}
+
snmp_change_type = {
'unknown': 0,
'add': 1,
@@ -138,6 +162,40 @@ def get_firewall_zones(conf):
return {'name': used_v4, 'ipv6_name': used_v6}
+def geoip_updated(conf, firewall):
+ diff = get_config_diff(conf)
+ node_diff = diff.get_child_nodes_diff(['firewall'], expand_nodes=Diff.DELETE, recursive=True)
+
+ out = {
+ 'name': [],
+ 'ipv6_name': [],
+ 'deleted_name': [],
+ 'deleted_ipv6_name': []
+ }
+ updated = False
+
+ for key, path in dict_search_recursive(firewall, 'geoip'):
+ set_name = f'GEOIP_CC_{path[1]}_{path[3]}'
+ if path[0] == 'name':
+ out['name'].append(set_name)
+ elif path[0] == 'ipv6_name':
+ out['ipv6_name'].append(set_name)
+ updated = True
+
+ if 'delete' in node_diff:
+ for key, path in dict_search_recursive(node_diff['delete'], 'geoip'):
+ set_name = f'GEOIP_CC_{path[1]}_{path[3]}'
+ if path[0] == 'name':
+ out['deleted_name'].append(set_name)
+ elif path[0] == 'ipv6-name':
+ out['deleted_ipv6_name'].append(set_name)
+ updated = True
+
+ if updated:
+ return out
+
+ return False
+
def get_config(config=None):
if config:
conf = config
@@ -162,6 +220,8 @@ def get_config(config=None):
key_mangling=('-', '_'), get_first_key=True,
no_tag_node_value_mangle=True)
+ firewall['geoip_updated'] = geoip_updated(conf, firewall)
+
return firewall
def verify_rule(firewall, rule_conf, ipv6):
@@ -207,6 +267,16 @@ def verify_rule(firewall, rule_conf, ipv6):
if side in rule_conf:
side_conf = rule_conf[side]
+ if dict_search_args(side_conf, 'geoip', 'country_code'):
+ if 'address' in side_conf:
+ raise ConfigError('Address and GeoIP cannot both be defined')
+
+ if dict_search_args(side_conf, 'group', 'address_group'):
+ raise ConfigError('Address-group and GeoIP cannot both be defined')
+
+ if dict_search_args(side_conf, 'group', 'network_group'):
+ raise ConfigError('Network-group and GeoIP cannot both be defined')
+
if 'group' in side_conf:
if {'address_group', 'network_group'} <= set(side_conf['group']):
raise ConfigError('Only one address-group or network-group can be specified')
@@ -235,11 +305,34 @@ def verify_rule(firewall, rule_conf, ipv6):
if rule_conf['protocol'] not in ['tcp', 'udp', 'tcp_udp']:
raise ConfigError('Protocol must be tcp, udp, or tcp_udp when specifying a port or port-group')
+def verify_nested_group(group_name, group, groups, seen):
+ if 'include' not in group:
+ return
+
+ for g in group['include']:
+ if g not in groups:
+ raise ConfigError(f'Nested group "{g}" does not exist')
+
+ if g in seen:
+ raise ConfigError(f'Group "{group_name}" has a circular reference')
+
+ seen.append(g)
+
+ if 'include' in groups[g]:
+ verify_nested_group(g, groups[g], groups, seen)
+
def verify(firewall):
if 'config_trap' in firewall and firewall['config_trap'] == 'enable':
if not firewall['trap_targets']:
raise ConfigError(f'Firewall config-trap enabled but "service snmp trap-target" is not defined')
+ if 'group' in firewall:
+ for group_type in nested_group_types:
+ if group_type in firewall['group']:
+ groups = firewall['group'][group_type]
+ for group_name, group in groups.items():
+ verify_nested_group(group_name, group, groups, [])
+
for name in ['name', 'ipv6_name']:
if name in firewall:
for name_id, name_conf in firewall[name].items():
@@ -271,55 +364,75 @@ def verify(firewall):
return None
-def cleanup_rule(table, jump_chain):
- commands = []
- chains = nft_iface_chains if table == 'ip filter' else nft6_iface_chains
- for chain in chains:
- results = cmd(f'nft -a list chain {table} {chain}').split("\n")
- for line in results:
- if f'jump {jump_chain}' in line:
- handle_search = re.search('handle (\d+)', line)
- if handle_search:
- commands.append(f'delete rule {table} {chain} handle {handle_search[1]}')
- return commands
-
def cleanup_commands(firewall):
commands = []
- commands_end = []
+ commands_chains = []
+ commands_sets = []
for table in ['ip filter', 'ip6 filter']:
+ name_node = 'name' if table == 'ip filter' else 'ipv6_name'
+ chain_prefix = NAME_PREFIX if table == 'ip filter' else NAME6_PREFIX
state_chain = 'VYOS_STATE_POLICY' if table == 'ip filter' else 'VYOS_STATE_POLICY6'
- json_str = cmd(f'nft -j list table {table}')
+ iface_chains = nft_iface_chains if table == 'ip filter' else nft6_iface_chains
+
+ geoip_list = []
+ if firewall['geoip_updated']:
+ geoip_key = 'deleted_ipv6_name' if table == 'ip6 filter' else 'deleted_name'
+ geoip_list = dict_search_args(firewall, 'geoip_updated', geoip_key) or []
+
+ json_str = cmd(f'nft -t -j list table {table}')
obj = loads(json_str)
+
if 'nftables' not in obj:
continue
+
for item in obj['nftables']:
if 'chain' in item:
chain = item['chain']['name']
- if chain in ['VYOS_STATE_POLICY', 'VYOS_STATE_POLICY6']:
- if 'state_policy' not in firewall:
- commands.append(f'delete chain {table} {chain}')
- else:
- commands.append(f'flush chain {table} {chain}')
- elif chain not in preserve_chains and not chain.startswith("VZONE"):
- if table == 'ip filter' and dict_search_args(firewall, 'name', chain.replace(NAME_PREFIX, "", 1)) != None:
- commands.append(f'flush chain {table} {chain}')
- elif table == 'ip6 filter' and dict_search_args(firewall, 'ipv6_name', chain.replace(NAME6_PREFIX, "", 1)) != None:
- commands.append(f'flush chain {table} {chain}')
- else:
- commands += cleanup_rule(table, chain)
- commands.append(f'delete chain {table} {chain}')
- elif 'rule' in item:
+ if chain in preserve_chains or chain.startswith("VZONE"):
+ continue
+
+ if chain == state_chain:
+ command = 'delete' if 'state_policy' not in firewall else 'flush'
+ commands_chains.append(f'{command} chain {table} {chain}')
+ elif dict_search_args(firewall, name_node, chain.replace(chain_prefix, "", 1)) != None:
+ commands.append(f'flush chain {table} {chain}')
+ else:
+ commands_chains.append(f'delete chain {table} {chain}')
+
+ if 'rule' in item:
rule = item['rule']
- if rule['chain'] in ['VYOS_FW_FORWARD', 'VYOS_FW_OUTPUT', 'VYOS_FW_LOCAL', 'VYOS_FW6_FORWARD', 'VYOS_FW6_OUTPUT', 'VYOS_FW6_LOCAL']:
- if 'expr' in rule and any([True for expr in rule['expr'] if dict_search_args(expr, 'jump', 'target') == state_chain]):
- if 'state_policy' not in firewall:
- chain = rule['chain']
- handle = rule['handle']
+ chain = rule['chain']
+ handle = rule['handle']
+
+ if chain in iface_chains:
+ target, _ = next(dict_search_recursive(rule['expr'], 'target'))
+
+ if target == state_chain and 'state_policy' not in firewall:
+ commands.append(f'delete rule {table} {chain} handle {handle}')
+
+ if target.startswith(chain_prefix):
+ if dict_search_args(firewall, name_node, target.replace(chain_prefix, "", 1)) == None:
commands.append(f'delete rule {table} {chain} handle {handle}')
- elif 'set' in item:
+
+ if 'set' in item:
set_name = item['set']['name']
- commands_end.append(f'delete set {table} {set_name}')
- return commands + commands_end
+
+ if set_name.startswith('GEOIP_CC_') and set_name in geoip_list:
+ commands_sets.append(f'delete set {table} {set_name}')
+ continue
+
+ if set_name.startswith("RECENT_"):
+ commands_sets.append(f'delete set {table} {set_name}')
+ continue
+
+ for prefix, group_type in group_set_prefix.items():
+ if set_name.startswith(prefix):
+ group_name = set_name.replace(prefix, "", 1)
+ if dict_search_args(firewall, 'group', group_type, group_name) != None:
+ commands_sets.append(f'flush set {table} {set_name}')
+ else:
+ commands_sets.append(f'delete set {table} {set_name}')
+ return commands + commands_chains + commands_sets
def generate(firewall):
if not os.path.exists(nftables_conf):
@@ -328,7 +441,6 @@ def generate(firewall):
firewall['cleanup_commands'] = cleanup_commands(firewall)
render(nftables_conf, 'firewall/nftables.j2', firewall)
- render(nftables_defines_conf, 'firewall/nftables-defines.j2', firewall)
return None
def apply_sysfs(firewall):
@@ -408,6 +520,27 @@ def apply(firewall):
if install_result == 1:
raise ConfigError('Failed to apply firewall')
+ # set fireall group domain-group xxx
+ if 'group' in firewall:
+ if 'domain_group' in firewall['group']:
+ # T970 Enable a resolver (systemd daemon) that checks
+ # domain-group addresses and update entries for domains by timeout
+ # If router loaded without internet connection or for synchronization
+ call('systemctl restart vyos-domain-group-resolve.service')
+ for group, group_config in firewall['group']['domain_group'].items():
+ domains = []
+ if group_config.get('address') is not None:
+ for address in group_config.get('address'):
+ domains.append(address)
+ # Add elements to domain-group, try to resolve domain => ip
+ # and add elements to nft set
+ ip_dict = get_ips_domains_dict(domains)
+ elements = sum(ip_dict.values(), [])
+ nft_init_set(f'D_{group}')
+ nft_add_set_elements(f'D_{group}', elements)
+ else:
+ call('systemctl stop vyos-domain-group-resolve.service')
+
if 'state_policy' in firewall and not state_policy_rule_exists():
for chain in ['VYOS_FW_FORWARD', 'VYOS_FW_OUTPUT', 'VYOS_FW_LOCAL']:
cmd(f'nft insert rule ip filter {chain} jump VYOS_STATE_POLICY')
@@ -420,6 +553,12 @@ def apply(firewall):
if firewall['policy_resync']:
resync_policy_route()
+ if firewall['geoip_updated']:
+ # Call helper script to Update set contents
+ if 'name' in firewall['geoip_updated'] or 'ipv6_name' in firewall['geoip_updated']:
+ print('Updating GeoIP. Please wait...')
+ geoip_update(firewall)
+
post_apply_trap(firewall)
return None
diff --git a/src/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py
index 7f7a98b04..7750c1247 100755
--- a/src/conf_mode/flow_accounting_conf.py
+++ b/src/conf_mode/flow_accounting_conf.py
@@ -22,6 +22,7 @@ import ipaddress
from ipaddress import ip_address
+from vyos.base import Warning
from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.ifconfig import Section
@@ -109,6 +110,9 @@ def _nftables_config(configured_ifaces, direction, length=None):
iface_prefix = "o" if direction == "egress" else "i"
rule_definition = f'{iface_prefix}ifname "{iface}" counter log group 2 snaplen {length} queue-threshold 100 comment "FLOW_ACCOUNTING_RULE"'
nftable_commands.append(f'nft insert rule {nftables_table} {nftables_chain} {rule_definition}')
+ # Also add IPv6 ingres logging
+ if nftables_table == nftables_nflog_table:
+ nftable_commands.append(f'nft insert rule ip6 {nftables_table} {nftables_chain} {rule_definition}')
# change nftables
for command in nftable_commands:
@@ -172,7 +176,7 @@ def verify(flow_config):
if interface not in Section.interfaces():
# Changed from error to warning to allow adding dynamic interfaces
# and interface templates
- print(f'Warning: Interface "{interface}" is not presented in the system')
+ Warning(f'Interface "{interface}" is not presented in the system')
# check sFlow configuration
if 'sflow' in flow_config:
@@ -200,7 +204,13 @@ def verify(flow_config):
if 'agent_address' in flow_config['sflow']:
tmp = flow_config['sflow']['agent_address']
if not is_addr_assigned(tmp):
- print(f'Warning: Configured "sflow agent-address {tmp}" does not exist in the system!')
+ raise ConfigError(f'Configured "sflow agent-address {tmp}" does not exist in the system!')
+
+ # Check if configured netflow source-address exist in the system
+ if 'source_address' in flow_config['sflow']:
+ if not is_addr_assigned(flow_config['sflow']['source_address']):
+ tmp = flow_config['sflow']['source_address']
+ raise ConfigError(f'Configured "sflow source-address {tmp}" does not exist on the system!')
# check NetFlow configuration
if 'netflow' in flow_config:
@@ -212,7 +222,7 @@ def verify(flow_config):
if 'source_address' in flow_config['netflow']:
if not is_addr_assigned(flow_config['netflow']['source_address']):
tmp = flow_config['netflow']['source_address']
- print(f'Warning: Configured "netflow source-address {tmp}" does not exist on the system!')
+ raise ConfigError(f'Configured "netflow source-address {tmp}" does not exist on the system!')
# Check if engine-id compatible with selected protocol version
if 'engine_id' in flow_config['netflow']:
diff --git a/src/conf_mode/high-availability.py b/src/conf_mode/high-availability.py
index f939f9469..e14050dd3 100755
--- a/src/conf_mode/high-availability.py
+++ b/src/conf_mode/high-availability.py
@@ -28,7 +28,6 @@ from vyos.template import render
from vyos.template import is_ipv4
from vyos.template import is_ipv6
from vyos.util import call
-from vyos.util import is_systemd_service_running
from vyos.xml import defaults
from vyos import ConfigError
from vyos import airbag
@@ -161,12 +160,7 @@ def apply(ha):
call(f'systemctl stop {service_name}')
return None
- # XXX: T3944 - reload keepalived configuration if service is already running
- # to not cause any service disruption when applying changes.
- if is_systemd_service_running(service_name):
- call(f'systemctl reload {service_name}')
- else:
- call(f'systemctl restart {service_name}')
+ call(f'systemctl reload-or-restart {service_name}')
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py
index 4167594e3..82ce1b41a 100755
--- a/src/conf_mode/interfaces-bonding.py
+++ b/src/conf_mode/interfaces-bonding.py
@@ -109,20 +109,22 @@ def get_config(config=None):
for interface, interface_config in bond['member']['interface'].items():
# Check if member interface is already member of another bridge
tmp = is_member(conf, interface, 'bridge')
- if tmp: interface_config.update({'is_bridge_member' : tmp})
+ if tmp: bond['member']['interface'][interface].update({'is_bridge_member' : tmp})
# Check if member interface is already member of a bond
tmp = is_member(conf, interface, 'bonding')
- if tmp and bond['ifname'] not in tmp:
- interface_config.update({'is_bond_member' : tmp})
+ for tmp in is_member(conf, interface, 'bonding'):
+ if bond['ifname'] == tmp:
+ continue
+ bond['member']['interface'][interface].update({'is_bond_member' : tmp})
# Check if member interface is used as source-interface on another interface
tmp = is_source_interface(conf, interface)
- if tmp: interface_config.update({'is_source_interface' : tmp})
+ if tmp: bond['member']['interface'][interface].update({'is_source_interface' : tmp})
# bond members must not have an assigned address
tmp = has_address_configured(conf, interface)
- if tmp: interface_config.update({'has_address' : ''})
+ if tmp: bond['member']['interface'][interface].update({'has_address' : {}})
return bond
@@ -167,11 +169,11 @@ def verify(bond):
raise ConfigError(error_msg + 'it does not exist!')
if 'is_bridge_member' in interface_config:
- tmp = next(iter(interface_config['is_bridge_member']))
+ tmp = interface_config['is_bridge_member']
raise ConfigError(error_msg + f'it is already a member of bridge "{tmp}"!')
if 'is_bond_member' in interface_config:
- tmp = next(iter(interface_config['is_bond_member']))
+ tmp = interface_config['is_bond_member']
raise ConfigError(error_msg + f'it is already a member of bond "{tmp}"!')
if 'is_source_interface' in interface_config:
diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py
index 38ae727c1..9f8eb31c6 100755
--- a/src/conf_mode/interfaces-bridge.py
+++ b/src/conf_mode/interfaces-bridge.py
@@ -118,11 +118,11 @@ def verify(bridge):
raise ConfigError('Loopback interface "lo" can not be added to a bridge')
if 'is_bridge_member' in interface_config:
- tmp = next(iter(interface_config['is_bridge_member']))
+ tmp = interface_config['is_bridge_member']
raise ConfigError(error_msg + f'it is already a member of bridge "{tmp}"!')
if 'is_bond_member' in interface_config:
- tmp = next(iter(interface_config['is_bond_member']))
+ tmp = interface_config['is_bond_member']
raise ConfigError(error_msg + f'it is already a member of bond "{tmp}"!')
if 'is_source_interface' in interface_config:
@@ -134,7 +134,7 @@ def verify(bridge):
if 'enable_vlan' in bridge:
if 'has_vlan' in interface_config:
- raise ConfigError(error_msg + 'it has an VLAN subinterface assigned!')
+ raise ConfigError(error_msg + 'it has VLAN subinterface(s) assigned!')
if 'wlan' in interface:
raise ConfigError(error_msg + 'VLAN aware cannot be set!')
diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index 4750ca3e8..280a62b9a 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -39,6 +39,8 @@ from vyos.configverify import verify_mirror_redirect
from vyos.ifconfig import VTunIf
from vyos.pki import load_dh_parameters
from vyos.pki import load_private_key
+from vyos.pki import sort_ca_chain
+from vyos.pki import verify_ca_chain
from vyos.pki import wrap_certificate
from vyos.pki import wrap_crl
from vyos.pki import wrap_dh_parameters
@@ -148,8 +150,14 @@ def verify_pki(openvpn):
if 'ca_certificate' not in tls:
raise ConfigError(f'Must specify "tls ca-certificate" on openvpn interface {interface}')
- if tls['ca_certificate'] not in pki['ca']:
- raise ConfigError(f'Invalid CA certificate on openvpn interface {interface}')
+ for ca_name in tls['ca_certificate']:
+ if ca_name not in pki['ca']:
+ raise ConfigError(f'Invalid CA certificate on openvpn interface {interface}')
+
+ if len(tls['ca_certificate']) > 1:
+ sorted_chain = sort_ca_chain(tls['ca_certificate'], pki['ca'])
+ if not verify_ca_chain(sorted_chain, pki['ca']):
+ raise ConfigError(f'CA certificates are not a valid chain')
if mode != 'client' and 'auth_key' not in tls:
if 'certificate' not in tls:
@@ -516,21 +524,28 @@ def generate_pki_files(openvpn):
if tls:
if 'ca_certificate' in tls:
- cert_name = tls['ca_certificate']
- pki_ca = pki['ca'][cert_name]
+ cert_path = os.path.join(cfg_dir, f'{interface}_ca.pem')
+ crl_path = os.path.join(cfg_dir, f'{interface}_crl.pem')
- if 'certificate' in pki_ca:
- cert_path = os.path.join(cfg_dir, f'{interface}_ca.pem')
- write_file(cert_path, wrap_certificate(pki_ca['certificate']),
- user=user, group=group, mode=0o600)
+ if os.path.exists(cert_path):
+ os.unlink(cert_path)
+
+ if os.path.exists(crl_path):
+ os.unlink(crl_path)
+
+ for cert_name in sort_ca_chain(tls['ca_certificate'], pki['ca']):
+ pki_ca = pki['ca'][cert_name]
+
+ if 'certificate' in pki_ca:
+ write_file(cert_path, wrap_certificate(pki_ca['certificate']) + "\n",
+ user=user, group=group, mode=0o600, append=True)
- if 'crl' in pki_ca:
- for crl in pki_ca['crl']:
- crl_path = os.path.join(cfg_dir, f'{interface}_crl.pem')
- write_file(crl_path, wrap_crl(crl), user=user, group=group,
- mode=0o600)
+ if 'crl' in pki_ca:
+ for crl in pki_ca['crl']:
+ write_file(crl_path, wrap_crl(crl) + "\n", user=user, group=group,
+ mode=0o600, append=True)
- openvpn['tls']['crl'] = True
+ openvpn['tls']['crl'] = True
if 'certificate' in tls:
cert_name = tls['certificate']
diff --git a/src/conf_mode/ntp.py b/src/conf_mode/ntp.py
index 0d6ec9ace..5490a794d 100755
--- a/src/conf_mode/ntp.py
+++ b/src/conf_mode/ntp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2021 VyOS maintainers and contributors
+# Copyright (C) 2018-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
@@ -18,9 +18,11 @@ import os
from vyos.config import Config
from vyos.configverify import verify_vrf
-from vyos import ConfigError
+from vyos.configverify import verify_interface_exists
from vyos.util import call
+from vyos.util import get_interface_config
from vyos.template import render
+from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -49,6 +51,20 @@ def verify(ntp):
raise ConfigError('NTP server not configured')
verify_vrf(ntp)
+
+ if 'interface' in ntp:
+ # If ntpd should listen on a given interface, ensure it exists
+ for interface in ntp['interface']:
+ verify_interface_exists(interface)
+
+ # If we run in a VRF, our interface must belong to this VRF, too
+ if 'vrf' in ntp:
+ tmp = get_interface_config(interface)
+ vrf_name = ntp['vrf']
+ if 'master' not in tmp or tmp['master'] != vrf_name:
+ raise ConfigError(f'NTP runs in VRF "{vrf_name}" - "{interface}" '\
+ f'does not belong to this VRF!')
+
return None
def generate(ntp):
diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py
index efa3578b4..29ed7b1b7 100755
--- a/src/conf_mode/pki.py
+++ b/src/conf_mode/pki.py
@@ -29,12 +29,60 @@ from vyos.pki import load_private_key
from vyos.pki import load_crl
from vyos.pki import load_dh_parameters
from vyos.util import ask_input
+from vyos.util import call
+from vyos.util import dict_search_args
from vyos.util import dict_search_recursive
from vyos.xml import defaults
from vyos import ConfigError
from vyos import airbag
airbag.enable()
+# keys to recursively search for under specified path, script to call if update required
+sync_search = [
+ {
+ 'keys': ['certificate'],
+ 'path': ['service', 'https'],
+ 'script': '/usr/libexec/vyos/conf_mode/https.py'
+ },
+ {
+ 'keys': ['certificate', 'ca_certificate'],
+ 'path': ['interfaces', 'ethernet'],
+ 'script': '/usr/libexec/vyos/conf_mode/interfaces-ethernet.py'
+ },
+ {
+ 'keys': ['certificate', 'ca_certificate', 'dh_params', 'shared_secret_key', 'auth_key', 'crypt_key'],
+ 'path': ['interfaces', 'openvpn'],
+ 'script': '/usr/libexec/vyos/conf_mode/interfaces-openvpn.py'
+ },
+ {
+ 'keys': ['certificate', 'ca_certificate', 'local_key', 'remote_key'],
+ 'path': ['vpn', 'ipsec'],
+ 'script': '/usr/libexec/vyos/conf_mode/vpn_ipsec.py'
+ },
+ {
+ 'keys': ['certificate', 'ca_certificate'],
+ 'path': ['vpn', 'openconnect'],
+ 'script': '/usr/libexec/vyos/conf_mode/vpn_openconnect.py'
+ },
+ {
+ 'keys': ['certificate', 'ca_certificate'],
+ 'path': ['vpn', 'sstp'],
+ 'script': '/usr/libexec/vyos/conf_mode/vpn_sstp.py'
+ }
+]
+
+# key from other config nodes -> key in pki['changed'] and pki
+sync_translate = {
+ 'certificate': 'certificate',
+ 'ca_certificate': 'ca',
+ 'dh_params': 'dh',
+ 'local_key': 'key_pair',
+ 'remote_key': 'key_pair',
+ 'shared_secret_key': 'openvpn',
+ 'auth_key': 'openvpn',
+ 'crypt_key': 'openvpn'
+}
+
def get_config(config=None):
if config:
conf = config
@@ -47,12 +95,21 @@ def get_config(config=None):
no_tag_node_value_mangle=True)
pki['changed'] = {}
- tmp = node_changed(conf, base + ['ca'], key_mangling=('-', '_'))
+ tmp = node_changed(conf, base + ['ca'], key_mangling=('-', '_'), recursive=True)
if tmp: pki['changed'].update({'ca' : tmp})
- tmp = node_changed(conf, base + ['certificate'], key_mangling=('-', '_'))
+ tmp = node_changed(conf, base + ['certificate'], key_mangling=('-', '_'), recursive=True)
if tmp: pki['changed'].update({'certificate' : tmp})
+ tmp = node_changed(conf, base + ['dh'], key_mangling=('-', '_'), recursive=True)
+ if tmp: pki['changed'].update({'dh' : tmp})
+
+ tmp = node_changed(conf, base + ['key-pair'], key_mangling=('-', '_'), recursive=True)
+ if tmp: pki['changed'].update({'key_pair' : tmp})
+
+ tmp = node_changed(conf, base + ['openvpn', 'shared-secret'], key_mangling=('-', '_'), recursive=True)
+ if tmp: pki['changed'].update({'openvpn' : tmp})
+
# We only merge on the defaults of there is a configuration at all
if conf.exists(base):
default_values = defaults(base)
@@ -164,17 +221,30 @@ def verify(pki):
if 'changed' in pki:
# if the list is getting longer, we can move to a dict() and also embed the
# search key as value from line 173 or 176
- for cert_type in ['ca', 'certificate']:
- if not cert_type in pki['changed']:
- continue
- for certificate in pki['changed'][cert_type]:
- if cert_type not in pki or certificate not in pki['changed'][cert_type]:
- if cert_type == 'ca':
- if certificate in dict_search_recursive(pki['system'], 'ca_certificate'):
- raise ConfigError(f'CA certificate "{certificate}" is still in use!')
- elif cert_type == 'certificate':
- if certificate in dict_search_recursive(pki['system'], 'certificate'):
- raise ConfigError(f'Certificate "{certificate}" is still in use!')
+ for search in sync_search:
+ for key in search['keys']:
+ changed_key = sync_translate[key]
+
+ if changed_key not in pki['changed']:
+ continue
+
+ for item_name in pki['changed'][changed_key]:
+ node_present = False
+ if changed_key == 'openvpn':
+ node_present = dict_search_args(pki, 'openvpn', 'shared_secret', item_name)
+ else:
+ node_present = dict_search_args(pki, changed_key, item_name)
+
+ if not node_present:
+ search_dict = dict_search_args(pki['system'], *search['path'])
+
+ if not search_dict:
+ continue
+
+ for found_name, found_path in dict_search_recursive(search_dict, key):
+ if found_name == item_name:
+ path_str = " ".join(search['path'] + found_path)
+ raise ConfigError(f'PKI object "{item_name}" still in use by "{path_str}"')
return None
@@ -188,7 +258,38 @@ def apply(pki):
if not pki:
return None
- # XXX: restart services if the content of a certificate changes
+ if 'changed' in pki:
+ for search in sync_search:
+ for key in search['keys']:
+ changed_key = sync_translate[key]
+
+ if changed_key not in pki['changed']:
+ continue
+
+ for item_name in pki['changed'][changed_key]:
+ node_present = False
+ if changed_key == 'openvpn':
+ node_present = dict_search_args(pki, 'openvpn', 'shared_secret', item_name)
+ else:
+ node_present = dict_search_args(pki, changed_key, item_name)
+
+ if node_present:
+ search_dict = dict_search_args(pki['system'], *search['path'])
+
+ if not search_dict:
+ continue
+
+ for found_name, found_path in dict_search_recursive(search_dict, key):
+ if found_name == item_name:
+ path_str = ' '.join(search['path'] + found_path)
+ print(f'pki: Updating config: {path_str} {found_name}')
+
+ script = search['script']
+ if found_path[0] == 'interfaces':
+ ifname = found_path[2]
+ call(f'VYOS_TAGNODE_VALUE={ifname} {script}')
+ else:
+ call(script)
return None
diff --git a/src/conf_mode/policy-route-interface.py b/src/conf_mode/policy-route-interface.py
index 1108aebe6..58c5fd93d 100755
--- a/src/conf_mode/policy-route-interface.py
+++ b/src/conf_mode/policy-route-interface.py
@@ -24,6 +24,7 @@ from vyos.config import Config
from vyos.ifconfig import Section
from vyos.template import render
from vyos.util import cmd
+from vyos.util import run
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -47,6 +48,11 @@ def get_config(config=None):
return if_policy
+def verify_chain(table, chain):
+ # Verify policy route applied
+ code = run(f'nft list chain {table} {chain}')
+ return code == 0
+
def verify(if_policy):
# bail out early - looks like removal from running config
if not if_policy:
@@ -62,6 +68,12 @@ def verify(if_policy):
if route_name not in if_policy['policy'][route]:
raise ConfigError(f'Invalid policy route name "{name}"')
+ nft_prefix = 'VYOS_PBR6_' if route == 'route6' else 'VYOS_PBR_'
+ nft_table = 'ip6 mangle' if route == 'route6' else 'ip mangle'
+
+ if not verify_chain(nft_table, nft_prefix + route_name):
+ raise ConfigError('Policy route did not apply')
+
return None
def generate(if_policy):
diff --git a/src/conf_mode/policy-route.py b/src/conf_mode/policy-route.py
index 5de341beb..9fddbd2c6 100755
--- a/src/conf_mode/policy-route.py
+++ b/src/conf_mode/policy-route.py
@@ -25,6 +25,7 @@ from vyos.config import Config
from vyos.template import render
from vyos.util import cmd
from vyos.util import dict_search_args
+from vyos.util import dict_search_recursive
from vyos.util import run
from vyos import ConfigError
from vyos import airbag
@@ -33,6 +34,9 @@ airbag.enable()
mark_offset = 0x7FFFFFFF
nftables_conf = '/run/nftables_policy.conf'
+ROUTE_PREFIX = 'VYOS_PBR_'
+ROUTE6_PREFIX = 'VYOS_PBR6_'
+
preserve_chains = [
'VYOS_PBR_PREROUTING',
'VYOS_PBR_POSTROUTING',
@@ -46,6 +50,16 @@ valid_groups = [
'port_group'
]
+group_set_prefix = {
+ 'A_': 'address_group',
+ 'A6_': 'ipv6_address_group',
+# 'D_': 'domain_group',
+ 'M_': 'mac_group',
+ 'N_': 'network_group',
+ 'N6_': 'ipv6_network_group',
+ 'P_': 'port_group'
+}
+
def get_policy_interfaces(conf):
out = {}
interfaces = conf.get_config_dict(['interfaces'], key_mangling=('-', '_'), get_first_key=True,
@@ -166,37 +180,55 @@ def verify(policy):
return None
-def cleanup_rule(table, jump_chain):
- commands = []
- results = cmd(f'nft -a list table {table}').split("\n")
- for line in results:
- if f'jump {jump_chain}' in line:
- handle_search = re.search('handle (\d+)', line)
- if handle_search:
- commands.append(f'delete rule {table} {chain} handle {handle_search[1]}')
- return commands
-
def cleanup_commands(policy):
commands = []
+ commands_chains = []
+ commands_sets = []
for table in ['ip mangle', 'ip6 mangle']:
- json_str = cmd(f'nft -j list table {table}')
+ route_node = 'route' if table == 'ip mangle' else 'route6'
+ chain_prefix = ROUTE_PREFIX if table == 'ip mangle' else ROUTE6_PREFIX
+
+ json_str = cmd(f'nft -t -j list table {table}')
obj = loads(json_str)
if 'nftables' not in obj:
continue
for item in obj['nftables']:
if 'chain' in item:
chain = item['chain']['name']
- if not chain.startswith("VYOS_PBR"):
+ if chain in preserve_chains or not chain.startswith("VYOS_PBR"):
continue
+
+ if dict_search_args(policy, route_node, chain.replace(chain_prefix, "", 1)) != None:
+ commands.append(f'flush chain {table} {chain}')
+ else:
+ commands_chains.append(f'delete chain {table} {chain}')
+
+ if 'rule' in item:
+ rule = item['rule']
+ chain = rule['chain']
+ handle = rule['handle']
+
if chain not in preserve_chains:
- if table == 'ip mangle' and dict_search_args(policy, 'route', chain.replace("VYOS_PBR_", "", 1)):
- commands.append(f'flush chain {table} {chain}')
- elif table == 'ip6 mangle' and dict_search_args(policy, 'route6', chain.replace("VYOS_PBR6_", "", 1)):
- commands.append(f'flush chain {table} {chain}')
- else:
- commands += cleanup_rule(table, chain)
- commands.append(f'delete chain {table} {chain}')
- return commands
+ continue
+
+ target, _ = next(dict_search_recursive(rule['expr'], 'target'))
+
+ if target.startswith(chain_prefix):
+ if dict_search_args(policy, route_node, target.replace(chain_prefix, "", 1)) == None:
+ commands.append(f'delete rule {table} {chain} handle {handle}')
+
+ if 'set' in item:
+ set_name = item['set']['name']
+
+ for prefix, group_type in group_set_prefix.items():
+ if set_name.startswith(prefix):
+ group_name = set_name.replace(prefix, "", 1)
+ if dict_search_args(policy, 'firewall_group', group_type, group_name) != None:
+ commands_sets.append(f'flush set {table} {set_name}')
+ else:
+ commands_sets.append(f'delete set {table} {set_name}')
+
+ return commands + commands_chains + commands_sets
def generate(policy):
if not os.path.exists(nftables_conf):
diff --git a/src/conf_mode/policy.py b/src/conf_mode/policy.py
index ef6008140..3008a20e0 100755
--- a/src/conf_mode/policy.py
+++ b/src/conf_mode/policy.py
@@ -150,6 +150,16 @@ def verify(policy):
tmp = dict_search('match.ipv6.address.prefix_list', rule_config)
if tmp and tmp not in policy.get('prefix_list6', []):
raise ConfigError(f'prefix-list6 {tmp} does not exist!')
+
+ # Specified access_list6 in nexthop must exist
+ tmp = dict_search('match.ipv6.nexthop.access_list', rule_config)
+ if tmp and tmp not in policy.get('access_list6', []):
+ raise ConfigError(f'access_list6 {tmp} does not exist!')
+
+ # Specified prefix-list6 in nexthop must exist
+ tmp = dict_search('match.ipv6.nexthop.prefix_list', rule_config)
+ if tmp and tmp not in policy.get('prefix_list6', []):
+ raise ConfigError(f'prefix-list6 {tmp} does not exist!')
# When routing protocols are active some use prefix-lists, route-maps etc.
# to apply the systems routing policy to the learned or redistributed routes.
diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py
index a9173ab87..01f14df61 100755
--- a/src/conf_mode/protocols_bgp.py
+++ b/src/conf_mode/protocols_bgp.py
@@ -19,6 +19,7 @@ import os
from sys import exit
from sys import argv
+from vyos.base import Warning
from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.configverify import verify_prefix_list
@@ -169,6 +170,16 @@ def verify(bgp):
peer_group = peer_config['peer_group']
if 'remote_as' in peer_config and 'remote_as' in bgp['peer_group'][peer_group]:
raise ConfigError(f'Peer-group member "{peer}" cannot override remote-as of peer-group "{peer_group}"!')
+ if 'interface' in peer_config:
+ if 'peer_group' in peer_config['interface']:
+ peer_group = peer_config['interface']['peer_group']
+ if 'remote_as' in peer_config['interface'] and 'remote_as' in bgp['peer_group'][peer_group]:
+ raise ConfigError(f'Peer-group member "{peer}" cannot override remote-as of peer-group "{peer_group}"!')
+ if 'v6only' in peer_config['interface']:
+ if 'peer_group' in peer_config['interface']['v6only']:
+ peer_group = peer_config['interface']['v6only']['peer_group']
+ if 'remote_as' in peer_config['interface']['v6only'] and 'remote_as' in bgp['peer_group'][peer_group]:
+ raise ConfigError(f'Peer-group member "{peer}" cannot override remote-as of peer-group "{peer_group}"!')
# Only checks for ipv4 and ipv6 neighbors
# Check if neighbor address is assigned as system interface address
@@ -188,6 +199,9 @@ def verify(bgp):
if 'source_interface' in peer_config['interface']:
raise ConfigError(f'"source-interface" option not allowed for neighbor "{peer}"')
+ if 'address_family' not in peer_config:
+ Warning(f'BGP neighbor "{peer}" requires address-family!')
+
for afi in ['ipv4_unicast', 'ipv4_multicast', 'ipv4_labeled_unicast', 'ipv4_flowspec',
'ipv6_unicast', 'ipv6_multicast', 'ipv6_labeled_unicast', 'ipv6_flowspec',
'l2vpn_evpn']:
diff --git a/src/conf_mode/protocols_eigrp.py b/src/conf_mode/protocols_eigrp.py
new file mode 100755
index 000000000..c1a1a45e1
--- /dev/null
+++ b/src/conf_mode/protocols_eigrp.py
@@ -0,0 +1,123 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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/>.
+
+from sys import exit
+from sys import argv
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.template import render_to_string
+from vyos import ConfigError
+from vyos import frr
+from vyos import airbag
+airbag.enable()
+
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ vrf = None
+ if len(argv) > 1:
+ vrf = argv[1]
+
+ base_path = ['protocols', 'eigrp']
+
+ # eqivalent of the C foo ? 'a' : 'b' statement
+ base = vrf and ['vrf', 'name', vrf, 'protocols', 'eigrp'] or base_path
+ eigrp = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+
+ # Assign the name of our VRF context. This MUST be done before the return
+ # statement below, else on deletion we will delete the default instance
+ # instead of the VRF instance.
+ if vrf: eigrp.update({'vrf' : vrf})
+
+ if not conf.exists(base):
+ eigrp.update({'deleted' : ''})
+ if not vrf:
+ # We are running in the default VRF context, thus we can not delete
+ # our main EIGRP instance if there are dependent EIGRP VRF instances.
+ eigrp['dependent_vrfs'] = conf.get_config_dict(['vrf', 'name'],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ return eigrp
+
+ # We also need some additional information from the config, prefix-lists
+ # and route-maps for instance. They will be used in verify().
+ #
+ # XXX: one MUST always call this without the key_mangling() option! See
+ # vyos.configverify.verify_common_route_maps() for more information.
+ tmp = conf.get_config_dict(['policy'])
+ # Merge policy dict into "regular" config dict
+ eigrp = dict_merge(tmp, eigrp)
+
+ import pprint
+ pprint.pprint(eigrp)
+ return eigrp
+
+def verify(eigrp):
+ pass
+
+def generate(eigrp):
+ if not eigrp or 'deleted' in eigrp:
+ return None
+
+ eigrp['protocol'] = 'eigrp' # required for frr/vrf.route-map.frr.j2
+ eigrp['frr_zebra_config'] = render_to_string('frr/vrf.route-map.frr.j2', eigrp)
+ eigrp['frr_eigrpd_config'] = render_to_string('frr/eigrpd.frr.j2', eigrp)
+
+def apply(eigrp):
+ eigrp_daemon = 'eigrpd'
+ zebra_daemon = 'zebra'
+
+ # Save original configuration prior to starting any commit actions
+ frr_cfg = frr.FRRConfig()
+
+ # The route-map used for the FIB (zebra) is part of the zebra daemon
+ frr_cfg.load_configuration(zebra_daemon)
+ frr_cfg.modify_section(r'(\s+)?ip protocol eigrp route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)')
+ if 'frr_zebra_config' in eigrp:
+ frr_cfg.add_before(frr.default_add_before, eigrp['frr_zebra_config'])
+ frr_cfg.commit_configuration(zebra_daemon)
+
+ # Generate empty helper string which can be ammended to FRR commands, it
+ # will be either empty (default VRF) or contain the "vrf <name" statement
+ vrf = ''
+ if 'vrf' in eigrp:
+ vrf = ' vrf ' + eigrp['vrf']
+
+ frr_cfg.load_configuration(eigrp_daemon)
+ frr_cfg.modify_section(f'^router eigrp \d+{vrf}', stop_pattern='^exit', remove_stop_mark=True)
+ if 'frr_eigrpd_config' in eigrp:
+ frr_cfg.add_before(frr.default_add_before, eigrp['frr_eigrpd_config'])
+ frr_cfg.commit_configuration(eigrp_daemon)
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/protocols_nhrp.py b/src/conf_mode/protocols_nhrp.py
index 92b335085..b247ce2ab 100755
--- a/src/conf_mode/protocols_nhrp.py
+++ b/src/conf_mode/protocols_nhrp.py
@@ -104,8 +104,8 @@ def apply(nhrp):
if rule_handle:
remove_nftables_rule('ip filter', 'VYOS_FW_OUTPUT', rule_handle)
- action = 'reload-or-restart' if nhrp and 'tunnel' in nhrp else 'stop'
- run(f'systemctl {action} opennhrp')
+ action = 'restart' if nhrp and 'tunnel' in nhrp else 'stop'
+ run(f'systemctl {action} opennhrp.service')
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/protocols_rip.py b/src/conf_mode/protocols_rip.py
index a76c1ce76..c78d90396 100755
--- a/src/conf_mode/protocols_rip.py
+++ b/src/conf_mode/protocols_rip.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021 VyOS maintainers and contributors
+# 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
diff --git a/src/conf_mode/service_event_handler.py b/src/conf_mode/service_event_handler.py
new file mode 100755
index 000000000..5440d1056
--- /dev/null
+++ b/src/conf_mode/service_event_handler.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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 json
+from pathlib import Path
+
+from vyos.config import Config
+from vyos.util import call, dict_search
+from vyos import ConfigError
+from vyos import airbag
+
+airbag.enable()
+
+service_name = 'vyos-event-handler'
+service_conf = Path(f'/run/{service_name}.conf')
+
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['service', 'event-handler', 'event']
+ config = conf.get_config_dict(base,
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ return config
+
+
+def verify(config):
+ # bail out early - looks like removal from running config
+ if not config:
+ return None
+
+ for name, event_config in config.items():
+ if not dict_search('filter.pattern', event_config) or not dict_search(
+ 'script.path', event_config):
+ raise ConfigError(
+ 'Event-handler: both pattern and script path items are mandatory'
+ )
+
+ if dict_search('script.environment.message', event_config):
+ raise ConfigError(
+ 'Event-handler: "message" environment variable is reserved for log message text'
+ )
+
+
+def generate(config):
+ if not config:
+ # Remove old config and return
+ service_conf.unlink(missing_ok=True)
+ return None
+
+ # Write configuration file
+ conf_json = json.dumps(config, indent=4)
+ service_conf.write_text(conf_json)
+
+ return None
+
+
+def apply(config):
+ if config:
+ call(f'systemctl restart {service_name}.service')
+ else:
+ call(f'systemctl stop {service_name}.service')
+
+
+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/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py
index 559d1bcd5..61f484129 100755
--- a/src/conf_mode/service_ipoe-server.py
+++ b/src/conf_mode/service_ipoe-server.py
@@ -53,6 +53,8 @@ default_config_data = {
'radius_nas_ip': '',
'radius_source_address': '',
'radius_shaper_attr': '',
+ 'radius_shaper_enable': False,
+ 'radius_shaper_multiplier': '',
'radius_shaper_vendor': '',
'radius_dynamic_author': '',
'thread_cnt': get_half_cpus()
@@ -196,6 +198,18 @@ def get_config(config=None):
if conf.exists(['nas-ip-address']):
ipoe['radius_nas_ip'] = conf.return_value(['nas-ip-address'])
+ if conf.exists(['rate-limit', 'attribute']):
+ ipoe['radius_shaper_attr'] = conf.return_value(['rate-limit', 'attribute'])
+
+ if conf.exists(['rate-limit', 'enable']):
+ ipoe['radius_shaper_enable'] = True
+
+ if conf.exists(['rate-limit', 'multiplier']):
+ ipoe['radius_shaper_multiplier'] = conf.return_value(['rate-limit', 'multiplier'])
+
+ if conf.exists(['rate-limit', 'vendor']):
+ ipoe['radius_shaper_vendor'] = conf.return_value(['rate-limit', 'vendor'])
+
if conf.exists(['source-address']):
ipoe['radius_source_address'] = conf.return_value(['source-address'])
diff --git a/src/conf_mode/service_monitoring_telegraf.py b/src/conf_mode/service_monitoring_telegraf.py
index 102a87318..62f5e1ddf 100755
--- a/src/conf_mode/service_monitoring_telegraf.py
+++ b/src/conf_mode/service_monitoring_telegraf.py
@@ -99,15 +99,31 @@ def get_config(config=None):
monitoring['interfaces_ethernet'] = get_interfaces('ethernet', vlan=False)
monitoring['nft_chains'] = get_nft_filter_chains()
- if 'authentication' in monitoring or \
- 'url' in monitoring:
- monitoring['influxdb_configured'] = True
+ # Redefine azure group-metrics 'single-table' and 'table-per-metric'
+ if 'azure_data_explorer' in monitoring:
+ if 'single-table' in monitoring['azure_data_explorer']['group_metrics']:
+ monitoring['azure_data_explorer']['group_metrics'] = 'SingleTable'
+ else:
+ monitoring['azure_data_explorer']['group_metrics'] = 'TablePerMetric'
+ # Set azure env
+ if 'authentication' in monitoring['azure_data_explorer']:
+ auth_config = monitoring['azure_data_explorer']['authentication']
+ if {'client_id', 'client_secret', 'tenant_id'} <= set(auth_config):
+ os.environ['AZURE_CLIENT_ID'] = auth_config['client_id']
+ os.environ['AZURE_CLIENT_SECRET'] = auth_config['client_secret']
+ os.environ['AZURE_TENANT_ID'] = auth_config['tenant_id']
# Ignore default XML values if config doesn't exists
# Delete key from dict
+ if not conf.exists(base + ['influxdb']):
+ del monitoring['influxdb']
+
if not conf.exists(base + ['prometheus-client']):
del monitoring['prometheus_client']
+ if not conf.exists(base + ['azure-data-explorer']):
+ del monitoring['azure_data_explorer']
+
return monitoring
def verify(monitoring):
@@ -115,15 +131,34 @@ def verify(monitoring):
if not monitoring:
return None
- if 'influxdb_configured' in monitoring:
- 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!')
+ # Verify influxdb
+ if 'influxdb' in monitoring:
+ if 'authentication' not in monitoring['influxdb'] or \
+ 'organization' not in monitoring['influxdb']['authentication'] or \
+ 'token' not in monitoring['influxdb']['authentication']:
+ raise ConfigError(f'influxdb authentication "organization and token" are mandatory!')
+
+ if 'url' not in monitoring['influxdb']:
+ raise ConfigError(f'Monitoring influxdb "url" is mandatory!')
- if 'url' not in monitoring:
+ # Verify azure-data-explorer
+ if 'azure_data_explorer' in monitoring:
+ if 'authentication' not in monitoring['azure_data_explorer'] or \
+ 'client_id' not in monitoring['azure_data_explorer']['authentication'] or \
+ 'client_secret' not in monitoring['azure_data_explorer']['authentication'] or \
+ 'tenant_id' not in monitoring['azure_data_explorer']['authentication']:
+ raise ConfigError(f'Authentication "client-id, client-secret and tenant-id" are mandatory!')
+
+ if 'database' not in monitoring['azure_data_explorer']:
+ raise ConfigError(f'Monitoring "database" is mandatory!')
+
+ if 'url' not in monitoring['azure_data_explorer']:
raise ConfigError(f'Monitoring "url" is mandatory!')
+ if monitoring['azure_data_explorer']['group_metrics'] == 'SingleTable' and \
+ 'table' not in monitoring['azure_data_explorer']:
+ raise ConfigError(f'Monitoring "table" name for single-table mode is mandatory!')
+
# Verify Splunk
if 'splunk' in monitoring:
if 'authentication' not in monitoring['splunk'] or \
diff --git a/src/conf_mode/service_router-advert.py b/src/conf_mode/service_router-advert.py
index 71b758399..ff7caaa84 100755
--- a/src/conf_mode/service_router-advert.py
+++ b/src/conf_mode/service_router-advert.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2021 VyOS maintainers and contributors
+# Copyright (C) 2018-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
@@ -17,7 +17,7 @@
import os
from sys import exit
-
+from vyos.base import Warning
from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.template import render
@@ -79,22 +79,35 @@ def verify(rtradv):
if 'interface' not in rtradv:
return None
- for interface in rtradv['interface']:
- interface = rtradv['interface'][interface]
+ for interface, interface_config in rtradv['interface'].items():
if 'prefix' in interface:
- for prefix in interface['prefix']:
- prefix = interface['prefix'][prefix]
- valid_lifetime = prefix['valid_lifetime']
+ for prefix, prefix_config in interface_config['prefix'].items():
+ valid_lifetime = prefix_config['valid_lifetime']
if valid_lifetime == 'infinity':
valid_lifetime = 4294967295
- preferred_lifetime = prefix['preferred_lifetime']
+ preferred_lifetime = prefix_config['preferred_lifetime']
if preferred_lifetime == 'infinity':
preferred_lifetime = 4294967295
if not (int(valid_lifetime) > int(preferred_lifetime)):
raise ConfigError('Prefix valid-lifetime must be greater then preferred-lifetime')
+ if 'name_server_lifetime' in interface_config:
+ # man page states:
+ # The maximum duration how long the RDNSS entries are used for name
+ # resolution. A value of 0 means the nameserver must no longer be
+ # used. The value, if not 0, must be at least MaxRtrAdvInterval. To
+ # ensure stale RDNSS info gets removed in a timely fashion, this
+ # should not be greater than 2*MaxRtrAdvInterval.
+ lifetime = int(interface_config['name_server_lifetime'])
+ interval_max = int(interface_config['interval']['max'])
+ if lifetime > 0:
+ if lifetime < int(interval_max):
+ raise ConfigError(f'RDNSS lifetime must be at least "{interval_max}" seconds!')
+ if lifetime > 2* interval_max:
+ Warning(f'RDNSS lifetime should not exceed "{2 * interval_max}" which is two times "interval max"!')
+
return None
def generate(rtradv):
@@ -105,15 +118,16 @@ def generate(rtradv):
return None
def apply(rtradv):
+ systemd_service = 'radvd.service'
if not rtradv:
# bail out early - looks like removal from running config
- call('systemctl stop radvd.service')
+ call(f'systemctl stop {systemd_service}')
if os.path.exists(config_file):
os.unlink(config_file)
return None
- call('systemctl restart radvd.service')
+ call(f'systemctl reload-or-restart {systemd_service}')
return None
diff --git a/src/conf_mode/service_sla.py b/src/conf_mode/service_sla.py
new file mode 100755
index 000000000..e7c3ca59c
--- /dev/null
+++ b/src/conf_mode/service_sla.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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
+
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.template import render
+from vyos.util import call
+from vyos.xml import defaults
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+
+owamp_config_dir = '/etc/owamp-server'
+owamp_config_file = f'{owamp_config_dir}/owamp-server.conf'
+systemd_override_owamp = r'/etc/systemd/system/owamp-server.d/20-override.conf'
+
+twamp_config_dir = '/etc/twamp-server'
+twamp_config_file = f'{twamp_config_dir}/twamp-server.conf'
+systemd_override_twamp = r'/etc/systemd/system/twamp-server.d/20-override.conf'
+
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['service', 'sla']
+ if not conf.exists(base):
+ return None
+
+ sla = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=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)
+ sla = dict_merge(default_values, sla)
+
+ # Ignore default XML values if config doesn't exists
+ # Delete key from dict
+ if not conf.exists(base + ['owamp-server']):
+ del sla['owamp_server']
+ if not conf.exists(base + ['twamp-server']):
+ del sla['twamp_server']
+
+ return sla
+
+def verify(sla):
+ if not sla:
+ return None
+
+def generate(sla):
+ if not sla:
+ return None
+
+ render(owamp_config_file, 'sla/owamp-server.conf.j2', sla)
+ render(systemd_override_owamp, 'sla/owamp-override.conf.j2', sla)
+
+ render(twamp_config_file, 'sla/twamp-server.conf.j2', sla)
+ render(systemd_override_twamp, 'sla/twamp-override.conf.j2', sla)
+
+ return None
+
+def apply(sla):
+ owamp_service = 'owamp-server.service'
+ twamp_service = 'twamp-server.service'
+
+ call('systemctl daemon-reload')
+
+ if not sla or 'owamp_server' not in sla:
+ call(f'systemctl stop {owamp_service}')
+
+ if os.path.exists(owamp_config_file):
+ os.unlink(owamp_config_file)
+
+ if not sla or 'twamp_server' not in sla:
+ call(f'systemctl stop {twamp_service}')
+ if os.path.exists(twamp_config_file):
+ os.unlink(twamp_config_file)
+
+ if sla and 'owamp_server' in sla:
+ call(f'systemctl reload-or-restart {owamp_service}')
+
+ if sla and 'twamp_server' in sla:
+ call(f'systemctl reload-or-restart {twamp_service}')
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py
index ae060580d..5cd24db32 100755
--- a/src/conf_mode/snmp.py
+++ b/src/conf_mode/snmp.py
@@ -293,7 +293,15 @@ def apply(snmp):
call(f'systemctl restart {systemd_service}')
# Enable AgentX in FRR
- call('vtysh -c "configure terminal" -c "agentx" >/dev/null')
+ # This should be done for each daemon individually because common command
+ # works only if all the daemons started with SNMP support
+ frr_daemons_list = [
+ 'bgpd', 'ospf6d', 'ospfd', 'ripd', 'ripngd', 'isisd', 'ldpd', 'zebra'
+ ]
+ for frr_daemon in frr_daemons_list:
+ call(
+ f'vtysh -c "configure terminal" -d {frr_daemon} -c "agentx" >/dev/null'
+ )
return None
diff --git a/src/conf_mode/ssh.py b/src/conf_mode/ssh.py
index 487e8c229..28669694b 100755
--- a/src/conf_mode/ssh.py
+++ b/src/conf_mode/ssh.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2021 VyOS maintainers and contributors
+# Copyright (C) 2018-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
@@ -33,6 +33,9 @@ airbag.enable()
config_file = r'/run/sshd/sshd_config'
systemd_override = r'/etc/systemd/system/ssh.service.d/override.conf'
+sshguard_config_file = '/etc/sshguard/sshguard.conf'
+sshguard_whitelist = '/etc/sshguard/whitelist'
+
key_rsa = '/etc/ssh/ssh_host_rsa_key'
key_dsa = '/etc/ssh/ssh_host_dsa_key'
key_ed25519 = '/etc/ssh/ssh_host_ed25519_key'
@@ -54,6 +57,11 @@ def get_config(config=None):
# pass config file path - used in override template
ssh['config_file'] = config_file
+ # Ignore default XML values if config doesn't exists
+ # Delete key from dict
+ if not conf.exists(base + ['dynamic-protection']):
+ del ssh['dynamic_protection']
+
return ssh
def verify(ssh):
@@ -86,6 +94,10 @@ def generate(ssh):
render(config_file, 'ssh/sshd_config.j2', ssh)
render(systemd_override, 'ssh/override.conf.j2', ssh)
+
+ if 'dynamic_protection' in ssh:
+ render(sshguard_config_file, 'ssh/sshguard_config.j2', ssh)
+ render(sshguard_whitelist, 'ssh/sshguard_whitelist.j2', ssh)
# Reload systemd manager configuration
call('systemctl daemon-reload')
@@ -95,7 +107,12 @@ def apply(ssh):
if not ssh:
# SSH access is removed in the commit
call('systemctl stop ssh.service')
+ call('systemctl stop sshguard.service')
return None
+ if 'dynamic_protection' not in ssh:
+ call('systemctl stop sshguard.service')
+ else:
+ call('systemctl restart sshguard.service')
call('systemctl restart ssh.service')
return None
diff --git a/src/conf_mode/system-ip.py b/src/conf_mode/system-ip.py
index 05fc3a97a..0c5063ed3 100755
--- a/src/conf_mode/system-ip.py
+++ b/src/conf_mode/system-ip.py
@@ -64,6 +64,11 @@ def apply(opt):
value = '0' if (tmp != None) else '1'
write_file('/proc/sys/net/ipv4/conf/all/forwarding', value)
+ # enable/disable IPv4 directed broadcast forwarding
+ tmp = dict_search('disable_directed_broadcast', opt)
+ value = '0' if (tmp != None) else '1'
+ write_file('/proc/sys/net/ipv4/conf/all/bc_forwarding', value)
+
# configure multipath
tmp = dict_search('multipath.ignore_unreachable_nexthops', opt)
value = '1' if (tmp != None) else '0'
diff --git a/src/conf_mode/system-syslog.py b/src/conf_mode/system-syslog.py
index a9d3bbe31..20132456c 100755
--- a/src/conf_mode/system-syslog.py
+++ b/src/conf_mode/system-syslog.py
@@ -52,8 +52,6 @@ def get_config(config=None):
{
'global': {
'log-file': '/var/log/messages',
- 'max-size': 262144,
- 'action-on-max-size': '/usr/sbin/logrotate /etc/logrotate.d/vyos-rsyslog',
'selectors': '*.notice;local7.debug',
'max-files': '5',
'preserver_fqdn': False
diff --git a/src/conf_mode/system_frr.py b/src/conf_mode/system_frr.py
new file mode 100755
index 000000000..1af0055f6
--- /dev/null
+++ b/src/conf_mode/system_frr.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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
+# 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/>.
+
+from pathlib import Path
+from sys import exit
+
+from vyos import ConfigError
+from vyos import airbag
+from vyos.config import Config
+from vyos.logger import syslog
+from vyos.template import render_to_string
+from vyos.util import read_file, write_file, run
+airbag.enable()
+
+# path to daemons config and config status files
+config_file = '/etc/frr/daemons'
+vyos_status_file = '/tmp/vyos-config-status'
+# path to watchfrr for FRR control
+watchfrr = '/usr/lib/frr/watchfrr.sh'
+
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['system', 'frr']
+ frr_config = conf.get_config_dict(base, get_first_key=True)
+
+ return frr_config
+
+
+def verify(frr_config):
+ # Nothing to verify here
+ pass
+
+
+def generate(frr_config):
+ # read daemons config file
+ daemons_config_current = read_file(config_file)
+ # generate new config file
+ daemons_config_new = render_to_string('frr/daemons.frr.tmpl', frr_config)
+ # update configuration file if this is necessary
+ if daemons_config_new != daemons_config_current:
+ syslog.warning('FRR daemons configuration file need to be changed')
+ write_file(config_file, daemons_config_new)
+ frr_config['config_file_changed'] = True
+
+
+def apply(frr_config):
+ # check if this is initial commit during boot or intiated by CLI
+ # if the file exists, this must be CLI commit
+ commit_type_cli = Path(vyos_status_file).exists()
+ # display warning to user
+ if commit_type_cli and frr_config.get('config_file_changed'):
+ # Since FRR restart is not safe thing, better to give
+ # control over this to users
+ print('''
+ You need to reboot a router (preferred) or restart FRR
+ to apply changes in modules settings
+ ''')
+ # restart FRR automatically. DUring the initial boot this should be
+ # safe in most cases
+ if not commit_type_cli and frr_config.get('config_file_changed'):
+ syslog.warning('Restarting FRR to apply changes in modules')
+ run(f'{watchfrr} restart')
+
+
+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/vpn_sstp.py b/src/conf_mode/vpn_sstp.py
index db53463cf..23e5162ba 100755
--- a/src/conf_mode/vpn_sstp.py
+++ b/src/conf_mode/vpn_sstp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2020 VyOS maintainers and contributors
+# Copyright (C) 2018-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
@@ -20,6 +20,7 @@ from sys import exit
from vyos.config import Config
from vyos.configdict import get_accel_dict
+from vyos.configdict import dict_merge
from vyos.configverify import verify_accel_ppp_base_service
from vyos.pki import wrap_certificate
from vyos.pki import wrap_private_key
@@ -50,10 +51,10 @@ def get_config(config=None):
# retrieve common dictionary keys
sstp = get_accel_dict(conf, base, sstp_chap_secrets)
-
if sstp:
sstp['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
- get_first_key=True, no_tag_node_value_mangle=True)
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
return sstp
@@ -121,7 +122,6 @@ def generate(sstp):
ca_cert_name = sstp['ssl']['ca_certificate']
pki_ca = sstp['pki']['ca'][ca_cert_name]
-
write_file(cert_file_path, wrap_certificate(pki_cert['certificate']))
write_file(cert_key_path, wrap_private_key(pki_cert['private']['key']))
write_file(ca_cert_file_path, wrap_certificate(pki_ca['certificate']))
diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py
index f2d041083..1b4156895 100755
--- a/src/conf_mode/vrf.py
+++ b/src/conf_mode/vrf.py
@@ -83,7 +83,8 @@ def get_config(config=None):
conf = Config()
base = ['vrf']
- vrf = conf.get_config_dict(base, get_first_key=True)
+ vrf = conf.get_config_dict(base, key_mangling=('-', '_'),
+ no_tag_node_value_mangle=True, get_first_key=True)
# determine which VRF has been removed
for name in node_changed(conf, base + ['name']):
@@ -112,8 +113,14 @@ def verify(vrf):
f'static routes installed!')
if 'name' in vrf:
+ reserved_names = ["add", "all", "broadcast", "default", "delete", "dev", "get", "inet", "mtu", "link", "type",
+ "vrf"]
table_ids = []
for name, config in vrf['name'].items():
+ # Reserved VRF names
+ if name in reserved_names:
+ raise ConfigError(f'VRF name "{name}" is reserved and connot be used!')
+
# table id is mandatory
if 'table' not in config:
raise ConfigError(f'VRF "{name}" table id is mandatory!')
@@ -152,7 +159,7 @@ def apply(vrf):
# set the default VRF global behaviour
bind_all = '0'
- if 'bind-to-all' in vrf:
+ if 'bind_to_all' in vrf:
bind_all = '1'
sysctl_write('net.ipv4.tcp_l3mdev_accept', bind_all)
sysctl_write('net.ipv4.udp_l3mdev_accept', bind_all)
@@ -222,6 +229,15 @@ def apply(vrf):
# add VRF description if available
vrf_if.set_alias(config.get('description', ''))
+ # Enable/Disable IPv4 forwarding
+ tmp = dict_search('ip.disable_forwarding', config)
+ value = '0' if (tmp != None) else '1'
+ vrf_if.set_ipv4_forwarding(value)
+ # Enable/Disable IPv6 forwarding
+ tmp = dict_search('ipv6.disable_forwarding', config)
+ value = '0' if (tmp != None) else '1'
+ vrf_if.set_ipv6_forwarding(value)
+
# Enable/Disable of an interface must always be done at the end of the
# derived class to make use of the ref-counting set_admin_state()
# function. We will only enable the interface if 'up' was called as
diff --git a/src/conf_mode/zone_policy.py b/src/conf_mode/zone_policy.py
index 070a4deea..a52c52706 100755
--- a/src/conf_mode/zone_policy.py
+++ b/src/conf_mode/zone_policy.py
@@ -155,7 +155,7 @@ def get_local_from(zone_policy, local_zone_name):
def cleanup_commands():
commands = []
for table in ['ip filter', 'ip6 filter']:
- json_str = cmd(f'nft -j list table {table}')
+ json_str = cmd(f'nft -t -j list table {table}')
obj = loads(json_str)
if 'nftables' not in obj:
continue