summaryrefslogtreecommitdiff
path: root/src/conf_mode
diff options
context:
space:
mode:
Diffstat (limited to 'src/conf_mode')
-rwxr-xr-xsrc/conf_mode/firewall-interface.py186
-rwxr-xr-xsrc/conf_mode/firewall.py296
-rwxr-xr-xsrc/conf_mode/high-availability.py21
-rwxr-xr-xsrc/conf_mode/https.py29
-rwxr-xr-xsrc/conf_mode/interfaces-bonding.py56
-rwxr-xr-xsrc/conf_mode/interfaces-ethernet.py23
-rwxr-xr-xsrc/conf_mode/interfaces-macsec.py8
-rwxr-xr-xsrc/conf_mode/interfaces-openvpn.py3
-rwxr-xr-xsrc/conf_mode/interfaces-pseudo-ethernet.py35
-rwxr-xr-xsrc/conf_mode/nat.py24
-rwxr-xr-xsrc/conf_mode/ntp.py21
-rwxr-xr-xsrc/conf_mode/protocols_nhrp.py32
-rwxr-xr-xsrc/conf_mode/service_monitoring_telegraf.py82
-rwxr-xr-xsrc/conf_mode/ssh.py23
-rwxr-xr-xsrc/conf_mode/system_console.py27
-rwxr-xr-xsrc/conf_mode/vpn_ipsec.py8
-rwxr-xr-xsrc/conf_mode/vpn_openconnect.py5
-rwxr-xr-xsrc/conf_mode/vpn_sstp.py8
-rwxr-xr-xsrc/conf_mode/zone_policy.py213
19 files changed, 358 insertions, 742 deletions
diff --git a/src/conf_mode/firewall-interface.py b/src/conf_mode/firewall-interface.py
deleted file mode 100755
index ab1c69259..000000000
--- a/src/conf_mode/firewall-interface.py
+++ /dev/null
@@ -1,186 +0,0 @@
-#!/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/>.
-
-import os
-import re
-
-from sys import argv
-from sys import exit
-
-from vyos.config import Config
-from vyos.configdict import leaf_node_changed
-from vyos.ifconfig import Section
-from vyos.template import render
-from vyos.util import cmd
-from vyos.util import dict_search_args
-from vyos.util import run
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-NAME_PREFIX = 'NAME_'
-NAME6_PREFIX = 'NAME6_'
-
-NFT_CHAINS = {
- 'in': 'VYOS_FW_FORWARD',
- 'out': 'VYOS_FW_FORWARD',
- 'local': 'VYOS_FW_LOCAL'
-}
-NFT6_CHAINS = {
- 'in': 'VYOS_FW6_FORWARD',
- 'out': 'VYOS_FW6_FORWARD',
- 'local': 'VYOS_FW6_LOCAL'
-}
-
-def get_config(config=None):
- if config:
- conf = config
- else:
- conf = Config()
-
- ifname = argv[1]
- ifpath = Section.get_config_path(ifname)
- if_firewall_path = f'interfaces {ifpath} firewall'
-
- if_firewall = conf.get_config_dict(if_firewall_path, key_mangling=('-', '_'), get_first_key=True,
- no_tag_node_value_mangle=True)
-
- if_firewall['ifname'] = ifname
- if_firewall['firewall'] = conf.get_config_dict(['firewall'], key_mangling=('-', '_'), get_first_key=True,
- no_tag_node_value_mangle=True)
-
- 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:
- return None
-
- for direction in ['in', 'out', 'local']:
- if direction in if_firewall:
- if 'name' in if_firewall[direction]:
- name = if_firewall[direction]['name']
-
- if 'name' not in if_firewall['firewall']:
- raise ConfigError('Firewall name not configured')
-
- 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']
-
- if 'ipv6_name' not in if_firewall['firewall']:
- raise ConfigError('Firewall ipv6-name not configured')
-
- 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):
- return None
-
-def cleanup_rule(table, chain, prefix, ifname, new_name=None):
- results = cmd(f'nft -a list chain {table} {chain}').split("\n")
- retval = None
- for line in results:
- if f'{prefix}ifname "{ifname}"' in line:
- if new_name and f'jump {new_name}' in line:
- # new_name is used to clear rules for any previously referenced chains
- # returns true when rule exists and doesn't need to be created
- retval = True
- continue
-
- handle_search = re.search('handle (\d+)', line)
- if handle_search:
- run(f'nft delete rule {table} {chain} handle {handle_search[1]}')
- return retval
-
-def state_policy_handle(table, chain):
- # Find any state-policy rule to ensure interface rules are only inserted afterwards
- results = cmd(f'nft -a list chain {table} {chain}').split("\n")
- for line in results:
- if 'jump VYOS_STATE_POLICY' in line:
- handle_search = re.search('handle (\d+)', line)
- if handle_search:
- return handle_search[1]
- return None
-
-def apply(if_firewall):
- ifname = if_firewall['ifname']
-
- for direction in ['in', 'out', 'local']:
- chain = NFT_CHAINS[direction]
- ipv6_chain = NFT6_CHAINS[direction]
- if_prefix = 'i' if direction in ['in', 'local'] else 'o'
-
- name = dict_search_args(if_firewall, direction, 'name')
- if name:
- rule_exists = cleanup_rule('ip filter', chain, if_prefix, ifname, f'{NAME_PREFIX}{name}')
-
- if not rule_exists:
- rule_action = 'insert'
- rule_prefix = ''
-
- handle = state_policy_handle('ip filter', chain)
- if handle:
- rule_action = 'add'
- rule_prefix = f'position {handle}'
-
- run(f'nft {rule_action} rule ip filter {chain} {rule_prefix} {if_prefix}ifname {ifname} counter jump {NAME_PREFIX}{name}')
- else:
- cleanup_rule('ip filter', chain, if_prefix, ifname)
-
- ipv6_name = dict_search_args(if_firewall, direction, 'ipv6_name')
- if ipv6_name:
- rule_exists = cleanup_rule('ip6 filter', ipv6_chain, if_prefix, ifname, f'{NAME6_PREFIX}{ipv6_name}')
-
- if not rule_exists:
- rule_action = 'insert'
- rule_prefix = ''
-
- handle = state_policy_handle('ip6 filter', ipv6_chain)
- if handle:
- rule_action = 'add'
- rule_prefix = f'position {handle}'
-
- run(f'nft {rule_action} rule ip6 filter {ipv6_chain} {rule_prefix} {if_prefix}ifname {ifname} counter jump {NAME6_PREFIX}{ipv6_name}')
- else:
- cleanup_rule('ip6 filter', ipv6_chain, if_prefix, ifname)
-
- 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/firewall.py b/src/conf_mode/firewall.py
index 07eca722f..eeb57bd30 100755
--- a/src/conf_mode/firewall.py
+++ b/src/conf_mode/firewall.py
@@ -26,6 +26,7 @@ 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.configverify import verify_interface_exists
from vyos.firewall import geoip_update
from vyos.firewall import get_ips_domains_dict
from vyos.firewall import nft_add_set_elements
@@ -38,7 +39,7 @@ 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.util import rc_cmd
from vyos.xml import defaults
from vyos import ConfigError
from vyos import airbag
@@ -47,7 +48,6 @@ airbag.enable()
policy_route_conf_script = '/usr/libexec/vyos/conf_mode/policy-route.py'
nftables_conf = '/run/nftables.conf'
-nftables_defines_conf = '/run/nftables_defines.conf'
sysfs_config = {
'all_ping': {'sysfs': '/proc/sys/net/ipv4/icmp_echo_ignore_all', 'enable': '0', 'disable': '1'},
@@ -63,28 +63,6 @@ sysfs_config = {
'twa_hazards_protection': {'sysfs': '/proc/sys/net/ipv4/tcp_rfc1337'}
}
-NAME_PREFIX = 'NAME_'
-NAME6_PREFIX = 'NAME6_'
-
-preserve_chains = [
- 'INPUT',
- 'FORWARD',
- 'OUTPUT',
- 'VYOS_FW_FORWARD',
- 'VYOS_FW_LOCAL',
- 'VYOS_FW_OUTPUT',
- 'VYOS_POST_FW',
- 'VYOS_FRAG_MARK',
- 'VYOS_FW6_FORWARD',
- 'VYOS_FW6_LOCAL',
- 'VYOS_FW6_OUTPUT',
- 'VYOS_POST_FW6',
- 'VYOS_FRAG6_MARK'
-]
-
-nft_iface_chains = ['VYOS_FW_FORWARD', 'VYOS_FW_OUTPUT', 'VYOS_FW_LOCAL']
-nft6_iface_chains = ['VYOS_FW6_FORWARD', 'VYOS_FW6_OUTPUT', 'VYOS_FW6_LOCAL']
-
valid_groups = [
'address_group',
'domain_group',
@@ -97,16 +75,6 @@ nested_group_types = [
'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,
@@ -117,51 +85,6 @@ snmp_event_source = 1
snmp_trap_mib = 'VYATTA-TRAP-MIB'
snmp_trap_name = 'mgmtEventTrap'
-def get_firewall_interfaces(conf):
- out = {}
- interfaces = conf.get_config_dict(['interfaces'], key_mangling=('-', '_'), get_first_key=True,
- no_tag_node_value_mangle=True)
- def find_interfaces(iftype_conf, output={}, prefix=''):
- for ifname, if_conf in iftype_conf.items():
- if 'firewall' in if_conf:
- output[prefix + ifname] = if_conf['firewall']
- for vif in ['vif', 'vif_s', 'vif_c']:
- if vif in if_conf:
- output.update(find_interfaces(if_conf[vif], output, f'{prefix}{ifname}.'))
- return output
- for iftype, iftype_conf in interfaces.items():
- out.update(find_interfaces(iftype_conf))
- return out
-
-def get_firewall_zones(conf):
- used_v4 = []
- used_v6 = []
- zone_policy = conf.get_config_dict(['zone-policy'], key_mangling=('-', '_'), get_first_key=True,
- no_tag_node_value_mangle=True)
-
- if 'zone' in zone_policy:
- for zone, zone_conf in zone_policy['zone'].items():
- if 'from' in zone_conf:
- for from_zone, from_conf in zone_conf['from'].items():
- name = dict_search_args(from_conf, 'firewall', 'name')
- if name:
- used_v4.append(name)
-
- ipv6_name = dict_search_args(from_conf, 'firewall', 'ipv6_name')
- if ipv6_name:
- used_v6.append(ipv6_name)
-
- if 'intra_zone_filtering' in zone_conf:
- name = dict_search_args(zone_conf, 'intra_zone_filtering', 'firewall', 'name')
- if name:
- used_v4.append(name)
-
- ipv6_name = dict_search_args(zone_conf, 'intra_zone_filtering', 'firewall', 'ipv6_name')
- if ipv6_name:
- used_v6.append(ipv6_name)
-
- 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)
@@ -206,12 +129,40 @@ def get_config(config=None):
firewall = 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.
+ # XXX: T2665: we currently have no nice way for defaults under tag
+ # nodes, thus we load the defaults "by hand"
default_values = defaults(base)
+ for tmp in ['name', 'ipv6_name']:
+ if tmp in default_values:
+ del default_values[tmp]
+
+ if 'zone' in default_values:
+ del default_values['zone']
+
firewall = dict_merge(default_values, firewall)
+ # Merge in defaults for IPv4 ruleset
+ if 'name' in firewall:
+ default_values = defaults(base + ['name'])
+ for name in firewall['name']:
+ firewall['name'][name] = dict_merge(default_values,
+ firewall['name'][name])
+
+ # Merge in defaults for IPv6 ruleset
+ if 'ipv6_name' in firewall:
+ default_values = defaults(base + ['ipv6-name'])
+ for ipv6_name in firewall['ipv6_name']:
+ firewall['ipv6_name'][ipv6_name] = dict_merge(default_values,
+ firewall['ipv6_name'][ipv6_name])
+
+ if 'zone' in firewall:
+ default_values = defaults(base + ['zone'])
+ for zone in firewall['zone']:
+ firewall['zone'][zone] = dict_merge(default_values, firewall['zone'][zone])
+
firewall['policy_resync'] = bool('group' in firewall or node_changed(conf, base + ['group']))
- firewall['interfaces'] = get_firewall_interfaces(conf)
- firewall['zone_policy'] = get_firewall_zones(conf)
if 'config_trap' in firewall and firewall['config_trap'] == 'enable':
diff = get_config_diff(conf)
@@ -315,7 +266,7 @@ def verify_nested_group(group_name, group, groups, seen):
if g in seen:
raise ConfigError(f'Group "{group_name}" has a circular reference')
-
+
seen.append(g)
if 'include' in groups[g]:
@@ -336,109 +287,99 @@ def verify(firewall):
for name in ['name', 'ipv6_name']:
if name in firewall:
for name_id, name_conf in firewall[name].items():
- if name_id in preserve_chains:
- raise ConfigError(f'Firewall name "{name_id}" is reserved for VyOS')
-
- if name_id.startswith("VZONE"):
- raise ConfigError(f'Firewall name "{name_id}" uses reserved prefix')
-
if 'rule' in name_conf:
for rule_id, rule_conf in name_conf['rule'].items():
verify_rule(firewall, rule_conf, name == 'ipv6_name')
- for ifname, if_firewall in firewall['interfaces'].items():
- for direction in ['in', 'out', 'local']:
- name = dict_search_args(if_firewall, direction, 'name')
- ipv6_name = dict_search_args(if_firewall, direction, 'ipv6_name')
+ if 'interface' in firewall:
+ for ifname, if_firewall in firewall['interface'].items():
+ # verify ifname needs to be disabled, dynamic devices come up later
+ # verify_interface_exists(ifname)
- if name and dict_search_args(firewall, 'name', name) == None:
- raise ConfigError(f'Firewall name "{name}" is still referenced on interface {ifname}')
+ for direction in ['in', 'out', 'local']:
+ name = dict_search_args(if_firewall, direction, 'name')
+ ipv6_name = dict_search_args(if_firewall, direction, 'ipv6_name')
- if ipv6_name and dict_search_args(firewall, 'ipv6_name', ipv6_name) == None:
- raise ConfigError(f'Firewall ipv6-name "{ipv6_name}" is still referenced on interface {ifname}')
+ if name and dict_search_args(firewall, 'name', name) == None:
+ raise ConfigError(f'Invalid firewall name "{name}" referenced on interface {ifname}')
- for fw_name, used_names in firewall['zone_policy'].items():
- for name in used_names:
- if dict_search_args(firewall, fw_name, name) == None:
- raise ConfigError(f'Firewall {fw_name.replace("_", "-")} "{name}" is still referenced in zone-policy')
+ if ipv6_name and dict_search_args(firewall, 'ipv6_name', ipv6_name) == None:
+ raise ConfigError(f'Invalid firewall ipv6-name "{ipv6_name}" referenced on interface {ifname}')
- return None
+ local_zone = False
+ zone_interfaces = []
-def cleanup_commands(firewall):
- commands = []
- 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'
- 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 preserve_chains or chain.startswith("VZONE"):
- continue
+ if 'zone' in firewall:
+ for zone, zone_conf in firewall['zone'].items():
+ if 'local_zone' not in zone_conf and 'interface' not in zone_conf:
+ raise ConfigError(f'Zone "{zone}" has no interfaces and is not the local zone')
- 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 'local_zone' in zone_conf:
+ if local_zone:
+ raise ConfigError('There cannot be multiple local zones')
+ if 'interface' in zone_conf:
+ raise ConfigError('Local zone cannot have interfaces assigned')
+ if 'intra_zone_filtering' in zone_conf:
+ raise ConfigError('Local zone cannot use intra-zone-filtering')
+ local_zone = True
- if 'rule' in item:
- rule = item['rule']
- chain = rule['chain']
- handle = rule['handle']
+ if 'interface' in zone_conf:
+ found_duplicates = [intf for intf in zone_conf['interface'] if intf in zone_interfaces]
- if chain in iface_chains:
- target, _ = next(dict_search_recursive(rule['expr'], 'target'))
+ if found_duplicates:
+ raise ConfigError(f'Interfaces cannot be assigned to multiple zones')
- if target == state_chain and 'state_policy' not in firewall:
- commands.append(f'delete rule {table} {chain} handle {handle}')
+ zone_interfaces += zone_conf['interface']
- 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}')
+ if 'intra_zone_filtering' in zone_conf:
+ intra_zone = zone_conf['intra_zone_filtering']
- if 'set' in item:
- set_name = item['set']['name']
+ if len(intra_zone) > 1:
+ raise ConfigError('Only one intra-zone-filtering action must be specified')
- 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
+ if 'firewall' in intra_zone:
+ v4_name = dict_search_args(intra_zone, 'firewall', 'name')
+ if v4_name and not dict_search_args(firewall, 'name', v4_name):
+ raise ConfigError(f'Firewall name "{v4_name}" does not exist')
+
+ v6_name = dict_search_args(intra_zone, 'firewall', 'ipv6_name')
+ if v6_name and not dict_search_args(firewall, 'ipv6_name', v6_name):
+ raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist')
+
+ if not v4_name and not v6_name:
+ raise ConfigError('No firewall names specified for intra-zone-filtering')
+
+ if 'from' in zone_conf:
+ for from_zone, from_conf in zone_conf['from'].items():
+ if from_zone not in firewall['zone']:
+ raise ConfigError(f'Zone "{zone}" refers to a non-existent or deleted zone "{from_zone}"')
+
+ v4_name = dict_search_args(from_conf, 'firewall', 'name')
+ if v4_name and not dict_search_args(firewall, 'name', v4_name):
+ raise ConfigError(f'Firewall name "{v4_name}" does not exist')
+
+ v6_name = dict_search_args(from_conf, 'firewall', 'ipv6_name')
+ if v6_name and not dict_search_args(firewall, 'ipv6_name', v6_name):
+ raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist')
- 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
+ return None
def generate(firewall):
if not os.path.exists(nftables_conf):
firewall['first_install'] = True
- else:
- firewall['cleanup_commands'] = cleanup_commands(firewall)
+
+ if 'zone' in firewall:
+ for local_zone, local_zone_conf in firewall['zone'].items():
+ if 'local_zone' not in local_zone_conf:
+ continue
+
+ local_zone_conf['from_local'] = {}
+
+ for zone, zone_conf in firewall['zone'].items():
+ if zone == local_zone or 'from' not in zone_conf:
+ continue
+ if local_zone in zone_conf['from']:
+ local_zone_conf['from_local'][zone] = zone_conf['from'][local_zone]
render(nftables_conf, 'firewall/nftables.j2', firewall)
return None
@@ -499,28 +440,18 @@ def post_apply_trap(firewall):
cmd(base_cmd + ' '.join(objects))
-def state_policy_rule_exists():
- # Determine if state policy rules already exist in nft
- search_str = cmd(f'nft list chain ip filter VYOS_FW_FORWARD')
- return 'VYOS_STATE_POLICY' in search_str
-
def resync_policy_route():
# Update policy route as firewall groups were updated
- tmp = run(policy_route_conf_script)
+ tmp, out = rc_cmd(policy_route_conf_script)
if tmp > 0:
- Warning('Failed to re-apply policy route configuration!')
+ Warning(f'Failed to re-apply policy route configuration! {out}')
def apply(firewall):
- if 'first_install' in firewall:
- run('nfct helper add rpc inet tcp')
- run('nfct helper add rpc inet udp')
- run('nfct helper add tns inet tcp')
-
- install_result = run(f'nft -f {nftables_conf}')
+ install_result, output = rc_cmd(f'nft -f {nftables_conf}')
if install_result == 1:
- raise ConfigError('Failed to apply firewall')
+ raise ConfigError(f'Failed to apply firewall: {output}')
- # set fireall group domain-group xxx
+ # set firewall group domain-group xxx
if 'group' in firewall:
if 'domain_group' in firewall['group']:
# T970 Enable a resolver (systemd daemon) that checks
@@ -541,13 +472,6 @@ def apply(firewall):
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')
-
- for chain in ['VYOS_FW6_FORWARD', 'VYOS_FW6_OUTPUT', 'VYOS_FW6_LOCAL']:
- cmd(f'nft insert rule ip6 filter {chain} jump VYOS_STATE_POLICY6')
-
apply_sysfs(firewall)
if firewall['policy_resync']:
diff --git a/src/conf_mode/high-availability.py b/src/conf_mode/high-availability.py
index e14050dd3..8a959dc79 100755
--- a/src/conf_mode/high-availability.py
+++ b/src/conf_mode/high-availability.py
@@ -88,15 +88,12 @@ def verify(ha):
if not {'password', 'type'} <= set(group_config['authentication']):
raise ConfigError(f'Authentication requires both type and passwortd to be set in VRRP group "{group}"')
- # We can not use a VRID once per interface
+ # Keepalived doesn't allow mixing IPv4 and IPv6 in one group, so we mirror that restriction
+ # We also need to make sure VRID is not used twice on the same interface with the
+ # same address family.
+
interface = group_config['interface']
vrid = group_config['vrid']
- tmp = {'interface': interface, 'vrid': vrid}
- if tmp in used_vrid_if:
- raise ConfigError(f'VRID "{vrid}" can only be used once on interface "{interface}"!')
- used_vrid_if.append(tmp)
-
- # Keepalived doesn't allow mixing IPv4 and IPv6 in one group, so we mirror that restriction
# XXX: filter on map object is destructive, so we force it to list.
# Additionally, filter objects always evaluate to True, empty or not,
@@ -109,6 +106,11 @@ def verify(ha):
raise ConfigError(f'VRRP group "{group}" mixes IPv4 and IPv6 virtual addresses, this is not allowed.\n' \
'Create individual groups for IPv4 and IPv6!')
if vaddrs4:
+ tmp = {'interface': interface, 'vrid': vrid, 'ipver': 'IPv4'}
+ if tmp in used_vrid_if:
+ raise ConfigError(f'VRID "{vrid}" can only be used once on interface "{interface} with address family IPv4"!')
+ used_vrid_if.append(tmp)
+
if 'hello_source_address' in group_config:
if is_ipv6(group_config['hello_source_address']):
raise ConfigError(f'VRRP group "{group}" uses IPv4 but hello-source-address is IPv6!')
@@ -118,6 +120,11 @@ def verify(ha):
raise ConfigError(f'VRRP group "{group}" uses IPv4 but peer-address is IPv6!')
if vaddrs6:
+ tmp = {'interface': interface, 'vrid': vrid, 'ipver': 'IPv6'}
+ if tmp in used_vrid_if:
+ raise ConfigError(f'VRID "{vrid}" can only be used once on interface "{interface} with address family IPv6"!')
+ used_vrid_if.append(tmp)
+
if 'hello_source_address' in group_config:
if is_ipv4(group_config['hello_source_address']):
raise ConfigError(f'VRRP group "{group}" uses IPv6 but hello-source-address is IPv4!')
diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py
index 3057357fc..7cd7ea42e 100755
--- a/src/conf_mode/https.py
+++ b/src/conf_mode/https.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,8 @@ from vyos.pki import wrap_certificate
from vyos.pki import wrap_private_key
from vyos.template import render
from vyos.util import call
+from vyos.util import check_port_availability
+from vyos.util import is_listen_port_bind_service
from vyos.util import write_file
from vyos import airbag
@@ -107,6 +109,31 @@ def verify(https):
raise ConfigError("At least one 'virtual-host <id> server-name' "
"matching the 'certbot domain-name' is required.")
+ server_block_list = []
+
+ # organize by vhosts
+ vhost_dict = https.get('virtual-host', {})
+
+ if not vhost_dict:
+ # no specified virtual hosts (server blocks); use default
+ server_block_list.append(default_server_block)
+ else:
+ for vhost in list(vhost_dict):
+ server_block = deepcopy(default_server_block)
+ data = vhost_dict.get(vhost, {})
+ server_block['address'] = data.get('listen-address', '*')
+ server_block['port'] = data.get('listen-port', '443')
+ server_block_list.append(server_block)
+
+ for entry in server_block_list:
+ _address = entry.get('address')
+ _address = '0.0.0.0' if _address == '*' else _address
+ _port = entry.get('port')
+ proto = 'tcp'
+ if check_port_availability(_address, int(_port), proto) is not True and \
+ not is_listen_port_bind_service(int(_port), 'nginx'):
+ raise ConfigError(f'"{proto}" port "{_port}" is used by another service')
+
verify_vrf(https)
return None
diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py
index 7e146f446..21cf204fc 100755
--- a/src/conf_mode/interfaces-bonding.py
+++ b/src/conf_mode/interfaces-bonding.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
@@ -73,63 +73,83 @@ def get_config(config=None):
# To make our own life easier transfor the list of member interfaces
# into a dictionary - we will use this to add additional information
- # later on for wach member
+ # later on for each member
if 'member' in bond and 'interface' in bond['member']:
- # convert list if member interfaces to a dictionary
- bond['member']['interface'] = dict.fromkeys(
- bond['member']['interface'], {})
+ # convert list of member interfaces to a dictionary
+ bond['member']['interface'] = {k: {} for k in bond['member']['interface']}
if 'mode' in bond:
bond['mode'] = get_bond_mode(bond['mode'])
tmp = leaf_node_changed(conf, base + [ifname, 'mode'])
- if tmp: bond.update({'shutdown_required': {}})
+ if tmp: bond['shutdown_required'] = {}
tmp = leaf_node_changed(conf, base + [ifname, 'lacp-rate'])
- if tmp: bond.update({'shutdown_required': {}})
+ if tmp: bond['shutdown_required'] = {}
# determine which members have been removed
interfaces_removed = leaf_node_changed(conf, base + [ifname, 'member', 'interface'])
+
+ # Reset config level to interfaces
+ old_level = conf.get_level()
+ conf.set_level(['interfaces'])
+
if interfaces_removed:
- bond.update({'shutdown_required': {}})
+ bond['shutdown_required'] = {}
if 'member' not in bond:
- bond.update({'member': {}})
+ bond['member'] = {}
tmp = {}
for interface in interfaces_removed:
section = Section.section(interface) # this will be 'ethernet' for 'eth0'
- if conf.exists(['insterfaces', section, interface, 'disable']):
- tmp.update({interface : {'disable': ''}})
+ if conf.exists([section, interface, 'disable']):
+ tmp[interface] = {'disable': ''}
else:
- tmp.update({interface : {}})
+ tmp[interface] = {}
# also present the interfaces to be removed from the bond as dictionary
- bond['member'].update({'interface_remove': tmp})
+ bond['member']['interface_remove'] = tmp
+
+ # Restore existing config level
+ conf.set_level(old_level)
if dict_search('member.interface', bond):
for interface, interface_config in bond['member']['interface'].items():
+ # Check if member interface is a new member
+ if not conf.exists_effective(['member', 'interface', interface]):
+ bond['shutdown_required'] = {}
+
+ # Check if member interface is disabled
+ conf.set_level(['interfaces'])
+
+ section = Section.section(interface) # this will be 'ethernet' for 'eth0'
+ if conf.exists([section, interface, 'disable']):
+ interface_config['disable'] = ''
+
+ conf.set_level(old_level)
+
# Check if member interface is already member of another bridge
tmp = is_member(conf, interface, 'bridge')
- if tmp: bond['member']['interface'][interface].update({'is_bridge_member' : tmp})
+ if tmp: interface_config['is_bridge_member'] = tmp
# Check if member interface is already member of a bond
tmp = is_member(conf, interface, 'bonding')
for tmp in is_member(conf, interface, 'bonding'):
if bond['ifname'] == tmp:
continue
- bond['member']['interface'][interface].update({'is_bond_member' : tmp})
+ interface_config['is_bond_member'] = tmp
# Check if member interface is used as source-interface on another interface
tmp = is_source_interface(conf, interface)
- if tmp: bond['member']['interface'][interface].update({'is_source_interface' : tmp})
+ if tmp: interface_config['is_source_interface'] = tmp
# bond members must not have an assigned address
tmp = has_address_configured(conf, interface)
- if tmp: bond['member']['interface'][interface].update({'has_address' : {}})
+ if tmp: interface_config['has_address'] = {}
# bond members must not have a VRF attached
tmp = has_vrf_configured(conf, interface)
- if tmp: bond['member']['interface'][interface].update({'has_vrf' : {}})
+ if tmp: interface_config['has_vrf'] = {}
return bond
diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py
index 30e7a2af7..e02841831 100755
--- a/src/conf_mode/interfaces-ethernet.py
+++ b/src/conf_mode/interfaces-ethernet.py
@@ -153,11 +153,20 @@ def verify(ethernet):
return None
def generate(ethernet):
- if 'eapol' in ethernet:
- render(wpa_suppl_conf.format(**ethernet),
- 'ethernet/wpa_supplicant.conf.j2', ethernet)
+ # render real configuration file once
+ wpa_supplicant_conf = wpa_suppl_conf.format(**ethernet)
+
+ if 'deleted' in ethernet:
+ # delete configuration on interface removal
+ if os.path.isfile(wpa_supplicant_conf):
+ os.unlink(wpa_supplicant_conf)
+ return None
+ if 'eapol' in ethernet:
ifname = ethernet['ifname']
+
+ render(wpa_supplicant_conf, 'ethernet/wpa_supplicant.conf.j2', ethernet)
+
cert_file_path = os.path.join(cfg_dir, f'{ifname}_cert.pem')
cert_key_path = os.path.join(cfg_dir, f'{ifname}_cert.key')
@@ -184,10 +193,6 @@ def generate(ethernet):
write_file(ca_cert_file_path,
'\n'.join(encode_certificate(c) for c in ca_full_chain))
- else:
- # delete configuration on interface removal
- if os.path.isfile(wpa_suppl_conf.format(**ethernet)):
- os.unlink(wpa_suppl_conf.format(**ethernet))
return None
@@ -203,9 +208,9 @@ def apply(ethernet):
else:
e.update(ethernet)
if 'eapol' in ethernet:
- eapol_action='restart'
+ eapol_action='reload-or-restart'
- call(f'systemctl {eapol_action} wpa_supplicant-macsec@{ifname}')
+ call(f'systemctl {eapol_action} wpa_supplicant-wired@{ifname}')
if __name__ == '__main__':
try:
diff --git a/src/conf_mode/interfaces-macsec.py b/src/conf_mode/interfaces-macsec.py
index 870049a88..649ea8d50 100755
--- a/src/conf_mode/interfaces-macsec.py
+++ b/src/conf_mode/interfaces-macsec.py
@@ -67,7 +67,7 @@ def get_config(config=None):
macsec.update({'shutdown_required': {}})
if 'source_interface' in macsec:
- tmp = is_source_interface(conf, macsec['source_interface'], 'macsec')
+ tmp = is_source_interface(conf, macsec['source_interface'], ['macsec', 'pseudo-ethernet'])
if tmp and tmp != ifname: macsec.update({'is_source_interface' : tmp})
return macsec
@@ -102,12 +102,6 @@ def verify(macsec):
# gcm-aes-128 requires a 128bit long key - 64 characters (string) = 32byte = 256bit
raise ConfigError('gcm-aes-128 requires a 256bit long key!')
- if 'is_source_interface' in macsec:
- tmp = macsec['is_source_interface']
- src_ifname = macsec['source_interface']
- raise ConfigError(f'Can not use source-interface "{src_ifname}", it already ' \
- f'belongs to interface "{tmp}"!')
-
if 'source_interface' in macsec:
# MACsec adds a 40 byte overhead (32 byte MACsec + 8 bytes VLAN 802.1ad
# and 802.1q) - we need to check the underlaying MTU if our configured
diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index ef745d737..8155f36c2 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -55,6 +55,7 @@ from vyos.util import chown
from vyos.util import cmd
from vyos.util import dict_search
from vyos.util import dict_search_args
+from vyos.util import is_list_equal
from vyos.util import makedir
from vyos.util import read_file
from vyos.util import write_file
@@ -274,7 +275,7 @@ def verify(openvpn):
elif v6remAddr and not v6loAddr:
raise ConfigError('IPv6 "remote-address" requires IPv6 "local-address"')
- if (v4loAddr == v4remAddr) or (v6remAddr == v4remAddr):
+ if is_list_equal(v4loAddr, v4remAddr) or is_list_equal(v6loAddr, v6remAddr):
raise ConfigError('"local-address" and "remote-address" cannot be the same')
if dict_search('local_host', openvpn) in dict_search('local_address', openvpn):
diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py
index f26a50a0e..4c65bc0b6 100755
--- a/src/conf_mode/interfaces-pseudo-ethernet.py
+++ b/src/conf_mode/interfaces-pseudo-ethernet.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
@@ -15,10 +15,13 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from sys import exit
+from netifaces import interfaces
from vyos.config import Config
from vyos.configdict import get_interface_dict
from vyos.configdict import is_node_changed
+from vyos.configdict import is_source_interface
+from vyos.configdict import leaf_node_changed
from vyos.configverify import verify_vrf
from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
@@ -48,9 +51,16 @@ def get_config(config=None):
mode = is_node_changed(conf, ['mode'])
if mode: peth.update({'shutdown_required' : {}})
+ if leaf_node_changed(conf, base + [ifname, 'mode']):
+ peth.update({'rebuild_required': {}})
+
if 'source_interface' in peth:
_, peth['parent'] = get_interface_dict(conf, ['interfaces', 'ethernet'],
peth['source_interface'])
+ # test if source-interface is maybe already used by another interface
+ tmp = is_source_interface(conf, peth['source_interface'], ['macsec'])
+ if tmp and tmp != ifname: peth.update({'is_source_interface' : tmp})
+
return peth
def verify(peth):
@@ -72,21 +82,18 @@ def generate(peth):
return None
def apply(peth):
- if 'deleted' in peth:
- # delete interface
- MACVLANIf(peth['ifname']).remove()
- return None
+ # Check if the MACVLAN interface already exists
+ if 'rebuild_required' in peth or 'deleted' in peth:
+ if peth['ifname'] in interfaces():
+ p = MACVLANIf(peth['ifname'])
+ # MACVLAN is always needs to be recreated,
+ # thus we can simply always delete it first.
+ p.remove()
- # Check if MACVLAN interface already exists. Parameters like the underlaying
- # source-interface device or mode can not be changed on the fly and the
- # interface needs to be recreated from the bottom.
- if 'mode_old' in peth:
- MACVLANIf(peth['ifname']).remove()
+ if 'deleted' not in peth:
+ p = MACVLANIf(**peth)
+ p.update(peth)
- # It is safe to "re-create" the interface always, there is a sanity check
- # that the interface will only be create if its non existent
- p = MACVLANIf(**peth)
- p.update(peth)
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py
index 85819a77e..e75418ba5 100755
--- a/src/conf_mode/nat.py
+++ b/src/conf_mode/nat.py
@@ -44,7 +44,8 @@ if LooseVersion(kernel_version()) > LooseVersion('5.1'):
else:
k_mod = ['nft_nat', 'nft_chain_nat_ipv4']
-nftables_nat_config = '/tmp/vyos-nat-rules.nft'
+nftables_nat_config = '/run/nftables_nat.conf'
+nftables_static_nat_conf = '/run/nftables_static-nat-rules.nft'
def get_handler(json, chain, target):
""" Get nftable rule handler number of given chain/target combination.
@@ -88,7 +89,7 @@ def get_config(config=None):
# T2665: we must add the tagNode defaults individually until this is
# moved to the base class
- for direction in ['source', 'destination']:
+ for direction in ['source', 'destination', 'static']:
if direction in nat:
default_values = defaults(base + [direction, 'rule'])
for rule in dict_search(f'{direction}.rule', nat) or []:
@@ -178,24 +179,35 @@ def verify(nat):
# common rule verification
verify_rule(config, err_msg)
+ if dict_search('static.rule', nat):
+ for rule, config in dict_search('static.rule', nat).items():
+ err_msg = f'Static NAT configuration error in rule {rule}:'
+
+ if 'inbound_interface' not in config:
+ raise ConfigError(f'{err_msg}\n' \
+ 'inbound-interface not specified')
+
+ # common rule verification
+ verify_rule(config, err_msg)
+
return None
def generate(nat):
render(nftables_nat_config, 'firewall/nftables-nat.j2', nat)
+ render(nftables_static_nat_conf, 'firewall/nftables-static-nat.j2', nat)
# dry-run newly generated configuration
tmp = run(f'nft -c -f {nftables_nat_config}')
if tmp > 0:
- if os.path.exists(nftables_nat_config):
- os.unlink(nftables_nat_config)
raise ConfigError('Configuration file errors encountered!')
+ tmp = run(f'nft -c -f {nftables_nat_config}')
+
return None
def apply(nat):
cmd(f'nft -f {nftables_nat_config}')
- if os.path.isfile(nftables_nat_config):
- os.unlink(nftables_nat_config)
+ cmd(f'nft -f {nftables_static_nat_conf}')
return None
diff --git a/src/conf_mode/ntp.py b/src/conf_mode/ntp.py
index 5490a794d..0ecb4d736 100755
--- a/src/conf_mode/ntp.py
+++ b/src/conf_mode/ntp.py
@@ -17,6 +17,7 @@
import os
from vyos.config import Config
+from vyos.configdict import is_node_changed
from vyos.configverify import verify_vrf
from vyos.configverify import verify_interface_exists
from vyos.util import call
@@ -40,6 +41,10 @@ def get_config(config=None):
ntp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
ntp['config_file'] = config_file
+
+ tmp = is_node_changed(conf, base + ['vrf'])
+ if tmp: ntp.update({'restart_required': {}})
+
return ntp
def verify(ntp):
@@ -78,19 +83,25 @@ def generate(ntp):
return None
def apply(ntp):
+ systemd_service = 'ntp.service'
+ # Reload systemd manager configuration
+ call('systemctl daemon-reload')
+
if not ntp:
# NTP support is removed in the commit
- call('systemctl stop ntp.service')
+ call(f'systemctl stop {systemd_service}')
if os.path.exists(config_file):
os.unlink(config_file)
if os.path.isfile(systemd_override):
os.unlink(systemd_override)
+ return
- # Reload systemd manager configuration
- call('systemctl daemon-reload')
- if ntp:
- call('systemctl restart ntp.service')
+ # we need to restart the service if e.g. the VRF name changed
+ systemd_action = 'reload-or-restart'
+ if 'restart_required' in ntp:
+ systemd_action = 'restart'
+ call(f'systemctl {systemd_action} {systemd_service}')
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/protocols_nhrp.py b/src/conf_mode/protocols_nhrp.py
index b247ce2ab..d28ced4fd 100755
--- a/src/conf_mode/protocols_nhrp.py
+++ b/src/conf_mode/protocols_nhrp.py
@@ -14,10 +14,10 @@
# 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 vyos.config import Config
from vyos.configdict import node_changed
-from vyos.firewall import find_nftables_rule
-from vyos.firewall import remove_nftables_rule
from vyos.template import render
from vyos.util import process_named_running
from vyos.util import run
@@ -26,6 +26,7 @@ from vyos import airbag
airbag.enable()
opennhrp_conf = '/run/opennhrp/opennhrp.conf'
+nhrp_nftables_conf = '/run/nftables_nhrp.conf'
def get_config(config=None):
if config:
@@ -84,28 +85,23 @@ def verify(nhrp):
return None
def generate(nhrp):
+ if not os.path.exists(nhrp_nftables_conf):
+ nhrp['first_install'] = True
+
render(opennhrp_conf, 'nhrp/opennhrp.conf.j2', nhrp)
+ render(nhrp_nftables_conf, 'nhrp/nftables.conf.j2', nhrp)
return None
def apply(nhrp):
- if 'tunnel' in nhrp:
- for tunnel, tunnel_conf in nhrp['tunnel'].items():
- if 'source_address' in nhrp['if_tunnel'][tunnel]:
- comment = f'VYOS_NHRP_{tunnel}'
- source_address = nhrp['if_tunnel'][tunnel]['source_address']
-
- rule_handle = find_nftables_rule('ip filter', 'VYOS_FW_OUTPUT', ['ip protocol gre', f'ip saddr {source_address}', 'ip daddr 224.0.0.0/4'])
- if not rule_handle:
- run(f'sudo nft insert rule ip filter VYOS_FW_OUTPUT ip protocol gre ip saddr {source_address} ip daddr 224.0.0.0/4 counter drop comment "{comment}"')
-
- for tunnel in nhrp['del_tunnels']:
- comment = f'VYOS_NHRP_{tunnel}'
- rule_handle = find_nftables_rule('ip filter', 'VYOS_FW_OUTPUT', [f'comment "{comment}"'])
- if rule_handle:
- remove_nftables_rule('ip filter', 'VYOS_FW_OUTPUT', rule_handle)
+ nft_rc = run(f'nft -f {nhrp_nftables_conf}')
+ if nft_rc != 0:
+ raise ConfigError('Failed to apply NHRP tunnel firewall rules')
action = 'restart' if nhrp and 'tunnel' in nhrp else 'stop'
- run(f'systemctl {action} opennhrp.service')
+ service_rc = run(f'systemctl {action} opennhrp.service')
+ if service_rc != 0:
+ raise ConfigError(f'Failed to {action} the NHRP service')
+
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/service_monitoring_telegraf.py b/src/conf_mode/service_monitoring_telegraf.py
index 62f5e1ddf..427cb6911 100755
--- a/src/conf_mode/service_monitoring_telegraf.py
+++ b/src/conf_mode/service_monitoring_telegraf.py
@@ -22,6 +22,8 @@ from shutil import rmtree
from vyos.config import Config
from vyos.configdict import dict_merge
+from vyos.configdict import is_node_changed
+from vyos.configverify import verify_vrf
from vyos.ifconfig import Section
from vyos.template import render
from vyos.util import call
@@ -32,40 +34,15 @@ 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'
+config_telegraf = f'/run/telegraf/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
+systemd_override = '/etc/systemd/system/telegraf.service.d/10-override.conf'
def get_nft_filter_chains():
- """
- Get nft chains for table filter
- """
- nft = cmd('nft --json list table ip filter')
+ """ Get nft chains for table filter """
+ nft = cmd('nft --json list table ip vyos_filter')
nft = json.loads(nft)
chain_list = []
@@ -76,9 +53,7 @@ def get_nft_filter_chains():
return chain_list
-
def get_config(config=None):
-
if config:
conf = config
else:
@@ -87,8 +62,12 @@ def get_config(config=None):
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)
+ monitoring = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ tmp = is_node_changed(conf, base + ['vrf'])
+ if tmp: monitoring.update({'restart_required': {}})
# We have gathered the dict representation of the CLI, but there are default
# options which we need to update into the dictionary retrived.
@@ -96,7 +75,7 @@ def get_config(config=None):
monitoring = dict_merge(default_values, monitoring)
monitoring['custom_scripts_dir'] = custom_scripts_dir
- monitoring['interfaces_ethernet'] = get_interfaces('ethernet', vlan=False)
+ monitoring['interfaces_ethernet'] = Section.interfaces('ethernet', vlan=False)
monitoring['nft_chains'] = get_nft_filter_chains()
# Redefine azure group-metrics 'single-table' and 'table-per-metric'
@@ -131,6 +110,8 @@ def verify(monitoring):
if not monitoring:
return None
+ verify_vrf(monitoring)
+
# Verify influxdb
if 'influxdb' in monitoring:
if 'authentication' not in monitoring['influxdb'] or \
@@ -173,7 +154,7 @@ def verify(monitoring):
def generate(monitoring):
if not monitoring:
# Delete config and systemd files
- config_files = [config_telegraf, systemd_telegraf_service, systemd_override, syslog_telegraf]
+ config_files = [config_telegraf, systemd_override, syslog_telegraf]
for file in config_files:
if os.path.isfile(file):
os.unlink(file)
@@ -190,33 +171,34 @@ def generate(monitoring):
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.j2', monitoring)
- render(systemd_telegraf_service, 'monitoring/systemd_vyos_telegraf_service.j2', monitoring)
- render(systemd_override, 'monitoring/override.conf.j2', monitoring, permission=0o640)
- render(syslog_telegraf, 'monitoring/syslog_telegraf.j2', monitoring)
-
- chown(base_dir, 'telegraf', 'telegraf')
+ render(config_telegraf, 'telegraf/telegraf.j2', monitoring, user='telegraf', group='telegraf')
+ render(systemd_override, 'telegraf/override.conf.j2', monitoring)
+ render(syslog_telegraf, 'telegraf/syslog_telegraf.j2', monitoring)
return None
def apply(monitoring):
# Reload systemd manager configuration
+ systemd_service = 'telegraf.service'
call('systemctl daemon-reload')
- if monitoring:
- call('systemctl restart vyos-telegraf.service')
- else:
- call('systemctl stop vyos-telegraf.service')
+ if not monitoring:
+ call(f'systemctl stop {systemd_service}')
+ return
+
+ # we need to restart the service if e.g. the VRF name changed
+ systemd_action = 'reload-or-restart'
+ if 'restart_required' in monitoring:
+ systemd_action = 'restart'
+
+ call(f'systemctl {systemd_action} {systemd_service}')
+
# Telegraf include custom rsyslog config changes
- call('systemctl restart rsyslog')
+ call('systemctl reload-or-restart rsyslog')
if __name__ == '__main__':
try:
diff --git a/src/conf_mode/ssh.py b/src/conf_mode/ssh.py
index 28669694b..2bbd7142a 100755
--- a/src/conf_mode/ssh.py
+++ b/src/conf_mode/ssh.py
@@ -22,6 +22,7 @@ from syslog import LOG_INFO
from vyos.config import Config
from vyos.configdict import dict_merge
+from vyos.configdict import is_node_changed
from vyos.configverify import verify_vrf
from vyos.util import call
from vyos.template import render
@@ -50,6 +51,10 @@ def get_config(config=None):
return None
ssh = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+
+ tmp = is_node_changed(conf, base + ['vrf'])
+ if tmp: ssh.update({'restart_required': {}})
+
# 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)
@@ -104,17 +109,25 @@ def generate(ssh):
return None
def apply(ssh):
+ systemd_service_ssh = 'ssh.service'
+ systemd_service_sshguard = 'sshguard.service'
if not ssh:
# SSH access is removed in the commit
- call('systemctl stop ssh.service')
- call('systemctl stop sshguard.service')
+ call(f'systemctl stop {systemd_service_ssh}')
+ call(f'systemctl stop {systemd_service_sshguard}')
return None
+
if 'dynamic_protection' not in ssh:
- call('systemctl stop sshguard.service')
+ call(f'systemctl stop {systemd_service_sshguard}')
else:
- call('systemctl restart sshguard.service')
+ call(f'systemctl reload-or-restart {systemd_service_sshguard}')
+
+ # we need to restart the service if e.g. the VRF name changed
+ systemd_action = 'reload-or-restart'
+ if 'restart_required' in ssh:
+ systemd_action = 'restart'
- call('systemctl restart ssh.service')
+ call(f'systemctl {systemd_action} {systemd_service_ssh}')
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/system_console.py b/src/conf_mode/system_console.py
index 86985d765..e922edc4e 100755
--- a/src/conf_mode/system_console.py
+++ b/src/conf_mode/system_console.py
@@ -16,6 +16,7 @@
import os
import re
+from pathlib import Path
from vyos.config import Config
from vyos.configdict import dict_merge
@@ -68,18 +69,15 @@ def verify(console):
# amount of connected devices. We will resolve the fixed device name
# to its dynamic device file - and create a new dict entry for it.
by_bus_device = f'{by_bus_dir}/{device}'
- if os.path.isdir(by_bus_dir) and os.path.exists(by_bus_device):
- device = os.path.basename(os.readlink(by_bus_device))
-
- # If the device name still starts with usbXXX no matching tty was found
- # and it can not be used as a serial interface
- if device.startswith('usb'):
- raise ConfigError(f'Device {device} does not support beeing used as tty')
+ # If the device name still starts with usbXXX no matching tty was found
+ # and it can not be used as a serial interface
+ if not os.path.isdir(by_bus_dir) or not os.path.exists(by_bus_device):
+ raise ConfigError(f'Device {device} does not support beeing used as tty')
return None
def generate(console):
- base_dir = '/etc/systemd/system'
+ base_dir = '/run/systemd/system'
# Remove all serial-getty configuration files in advance
for root, dirs, files in os.walk(base_dir):
for basename in files:
@@ -90,7 +88,8 @@ def generate(console):
if not console or 'device' not in console:
return None
- for device, device_config in console['device'].items():
+ # replace keys in the config for ttyUSB items to use them in `apply()` later
+ for device in console['device'].copy():
if device.startswith('usb'):
# It is much easiert to work with the native ttyUSBn name when using
# getty, but that name may change across reboots - depending on the
@@ -98,9 +97,17 @@ def generate(console):
# to its dynamic device file - and create a new dict entry for it.
by_bus_device = f'{by_bus_dir}/{device}'
if os.path.isdir(by_bus_dir) and os.path.exists(by_bus_device):
- device = os.path.basename(os.readlink(by_bus_device))
+ device_updated = os.path.basename(os.readlink(by_bus_device))
+
+ # replace keys in the config to use them in `apply()` later
+ console['device'][device_updated] = console['device'][device]
+ del console['device'][device]
+ else:
+ raise ConfigError(f'Device {device} does not support beeing used as tty')
+ for device, device_config in console['device'].items():
config_file = base_dir + f'/serial-getty@{device}.service'
+ Path(f'{base_dir}/getty.target.wants').mkdir(exist_ok=True)
getty_wants_symlink = base_dir + f'/getty.target.wants/serial-getty@{device}.service'
render(config_file, 'getty/serial-getty.service.j2', device_config)
diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
index bad9cfbd8..5ca32d23e 100755
--- a/src/conf_mode/vpn_ipsec.py
+++ b/src/conf_mode/vpn_ipsec.py
@@ -595,13 +595,11 @@ def wait_for_vici_socket(timeout=5, sleep_interval=0.1):
sleep(sleep_interval)
def apply(ipsec):
+ systemd_service = 'strongswan-starter.service'
if not ipsec:
- call('sudo ipsec stop')
+ call(f'systemctl stop {systemd_service}')
else:
- call('sudo ipsec restart')
- call('sudo ipsec rereadall')
- call('sudo ipsec reload')
-
+ call(f'systemctl reload-or-restart {systemd_service}')
if wait_for_vici_socket():
call('sudo swanctl -q')
diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py
index 4e2a3f58b..23b1baf4d 100755
--- a/src/conf_mode/vpn_openconnect.py
+++ b/src/conf_mode/vpn_openconnect.py
@@ -25,6 +25,7 @@ from vyos.template import render
from vyos.util import call
from vyos.util import check_port_availability
from vyos.util import is_systemd_service_running
+from vyos.util import is_listen_port_bind_service
from vyos.util import dict_search
from vyos.xml import defaults
from vyos import ConfigError
@@ -78,8 +79,10 @@ def verify(ocserv):
if ocserv is None:
return None
# Check if listen-ports not binded other services
+ # It can be only listen by 'ocserv-main'
for proto, port in ocserv.get('listen_ports').items():
- if check_port_availability('0.0.0.0', int(port), proto) is not True:
+ if check_port_availability('0.0.0.0', int(port), proto) is not True and \
+ not is_listen_port_bind_service(int(port), 'ocserv-main'):
raise ConfigError(f'"{proto}" port "{port}" is used by another service')
# Check authentication
if "authentication" in ocserv:
diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py
index 23e5162ba..2949ab290 100755
--- a/src/conf_mode/vpn_sstp.py
+++ b/src/conf_mode/vpn_sstp.py
@@ -26,7 +26,9 @@ from vyos.pki import wrap_certificate
from vyos.pki import wrap_private_key
from vyos.template import render
from vyos.util import call
+from vyos.util import check_port_availability
from vyos.util import dict_search
+from vyos.util import is_listen_port_bind_service
from vyos.util import write_file
from vyos import ConfigError
from vyos import airbag
@@ -62,6 +64,12 @@ def verify(sstp):
if not sstp:
return None
+ port = sstp.get('port')
+ proto = 'tcp'
+ if check_port_availability('0.0.0.0', int(port), proto) is not True and \
+ not is_listen_port_bind_service(int(port), 'accel-pppd'):
+ raise ConfigError(f'"{proto}" port "{port}" is used by another service')
+
verify_accel_ppp_base_service(sstp)
if 'client_ip_pool' not in sstp and 'client_ipv6_pool' not in sstp:
diff --git a/src/conf_mode/zone_policy.py b/src/conf_mode/zone_policy.py
deleted file mode 100755
index a52c52706..000000000
--- a/src/conf_mode/zone_policy.py
+++ /dev/null
@@ -1,213 +0,0 @@
-#!/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
-
-from json import loads
-from sys import exit
-
-from vyos.config import Config
-from vyos.configdict import dict_merge
-from vyos.template import render
-from vyos.util import cmd
-from vyos.util import dict_search_args
-from vyos.util import run
-from vyos.xml import defaults
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-nftables_conf = '/run/nftables_zone.conf'
-
-def get_config(config=None):
- if config:
- conf = config
- else:
- conf = Config()
- base = ['zone-policy']
- zone_policy = conf.get_config_dict(base, key_mangling=('-', '_'),
- get_first_key=True,
- no_tag_node_value_mangle=True)
-
- zone_policy['firewall'] = conf.get_config_dict(['firewall'],
- key_mangling=('-', '_'),
- get_first_key=True,
- no_tag_node_value_mangle=True)
-
- if 'zone' in zone_policy:
- # 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 + ['zone'])
- for zone in zone_policy['zone']:
- zone_policy['zone'][zone] = dict_merge(default_values,
- zone_policy['zone'][zone])
-
- return zone_policy
-
-def verify(zone_policy):
- # bail out early - looks like removal from running config
- if not zone_policy:
- return None
-
- local_zone = False
- interfaces = []
-
- if 'zone' in zone_policy:
- for zone, zone_conf in zone_policy['zone'].items():
- if 'local_zone' not in zone_conf and 'interface' not in zone_conf:
- raise ConfigError(f'Zone "{zone}" has no interfaces and is not the local zone')
-
- if 'local_zone' in zone_conf:
- if local_zone:
- raise ConfigError('There cannot be multiple local zones')
- if 'interface' in zone_conf:
- raise ConfigError('Local zone cannot have interfaces assigned')
- if 'intra_zone_filtering' in zone_conf:
- raise ConfigError('Local zone cannot use intra-zone-filtering')
- local_zone = True
-
- if 'interface' in zone_conf:
- found_duplicates = [intf for intf in zone_conf['interface'] if intf in interfaces]
-
- if found_duplicates:
- raise ConfigError(f'Interfaces cannot be assigned to multiple zones')
-
- interfaces += zone_conf['interface']
-
- if 'intra_zone_filtering' in zone_conf:
- intra_zone = zone_conf['intra_zone_filtering']
-
- if len(intra_zone) > 1:
- raise ConfigError('Only one intra-zone-filtering action must be specified')
-
- if 'firewall' in intra_zone:
- v4_name = dict_search_args(intra_zone, 'firewall', 'name')
- if v4_name and not dict_search_args(zone_policy, 'firewall', 'name', v4_name):
- raise ConfigError(f'Firewall name "{v4_name}" does not exist')
-
- v6_name = dict_search_args(intra_zone, 'firewall', 'ipv6-name')
- if v6_name and not dict_search_args(zone_policy, 'firewall', 'ipv6-name', v6_name):
- raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist')
-
- if not v4_name and not v6_name:
- raise ConfigError('No firewall names specified for intra-zone-filtering')
-
- if 'from' in zone_conf:
- for from_zone, from_conf in zone_conf['from'].items():
- if from_zone not in zone_policy['zone']:
- raise ConfigError(f'Zone "{zone}" refers to a non-existent or deleted zone "{from_zone}"')
-
- v4_name = dict_search_args(from_conf, 'firewall', 'name')
- if v4_name:
- if 'name' not in zone_policy['firewall']:
- raise ConfigError(f'Firewall name "{v4_name}" does not exist')
-
- if not dict_search_args(zone_policy, 'firewall', 'name', v4_name):
- raise ConfigError(f'Firewall name "{v4_name}" does not exist')
-
- v6_name = dict_search_args(from_conf, 'firewall', 'v6_name')
- if v6_name:
- if 'ipv6_name' not in zone_policy['firewall']:
- raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist')
-
- if not dict_search_args(zone_policy, 'firewall', 'ipv6_name', v6_name):
- raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist')
-
- return None
-
-def has_ipv4_fw(zone_conf):
- if 'from' not in zone_conf:
- return False
- zone_from = zone_conf['from']
- return any([True for fz in zone_from if dict_search_args(zone_from, fz, 'firewall', 'name')])
-
-def has_ipv6_fw(zone_conf):
- if 'from' not in zone_conf:
- return False
- zone_from = zone_conf['from']
- return any([True for fz in zone_from if dict_search_args(zone_from, fz, 'firewall', 'ipv6_name')])
-
-def get_local_from(zone_policy, local_zone_name):
- # Get all zone firewall names from the local zone
- out = {}
- for zone, zone_conf in zone_policy['zone'].items():
- if zone == local_zone_name:
- continue
- if 'from' not in zone_conf:
- continue
- if local_zone_name in zone_conf['from']:
- out[zone] = zone_conf['from'][local_zone_name]
- return out
-
-def cleanup_commands():
- commands = []
- for table in ['ip filter', 'ip6 filter']:
- 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 'rule' in item:
- chain = item['rule']['chain']
- handle = item['rule']['handle']
- if 'expr' not in item['rule']:
- continue
- for expr in item['rule']['expr']:
- target = dict_search_args(expr, 'jump', 'target')
- if not target:
- continue
- if target.startswith("VZONE") or target.startswith("VYOS_STATE_POLICY"):
- commands.append(f'delete rule {table} {chain} handle {handle}')
- for item in obj['nftables']:
- if 'chain' in item:
- if item['chain']['name'].startswith("VZONE"):
- chain = item['chain']['name']
- commands.append(f'delete chain {table} {chain}')
- return commands
-
-def generate(zone_policy):
- data = zone_policy or {}
-
- if os.path.exists(nftables_conf): # Check to see if we've run before
- data['cleanup_commands'] = cleanup_commands()
-
- if 'zone' in data:
- for zone, zone_conf in data['zone'].items():
- zone_conf['ipv4'] = has_ipv4_fw(zone_conf)
- zone_conf['ipv6'] = has_ipv6_fw(zone_conf)
-
- if 'local_zone' in zone_conf:
- zone_conf['from_local'] = get_local_from(data, zone)
-
- render(nftables_conf, 'zone_policy/nftables.j2', data)
- return None
-
-def apply(zone_policy):
- install_result = run(f'nft -f {nftables_conf}')
- if install_result != 0:
- raise ConfigError('Failed to apply zone-policy')
-
- return None
-
-if __name__ == '__main__':
- try:
- c = get_config()
- verify(c)
- generate(c)
- apply(c)
- except ConfigError as e:
- print(e)
- exit(1)