summaryrefslogtreecommitdiff
path: root/src/conf_mode
diff options
context:
space:
mode:
authorGeorg <georg@lysergic.dev>2022-04-08 14:52:37 +0000
committerGitHub <noreply@github.com>2022-04-08 14:52:37 +0000
commit630945291c9a389ad62fd32caea3749f4c5e9d72 (patch)
treea85f72880269bfb43740b7a0bc790dcaca6de1e7 /src/conf_mode
parent15461be0cd7b51e0e290d66bae0bb112f6b2c3ea (diff)
parent654dbc9aa3b0d27ec4f3faefff6cbd85fc3e1d1a (diff)
downloadvyos-1x-630945291c9a389ad62fd32caea3749f4c5e9d72.tar.gz
vyos-1x-630945291c9a389ad62fd32caea3749f4c5e9d72.zip
Merge branch 'current' into dhcpd
Diffstat (limited to 'src/conf_mode')
-rwxr-xr-xsrc/conf_mode/conntrack.py23
-rwxr-xr-xsrc/conf_mode/conntrack_sync.py6
-rwxr-xr-xsrc/conf_mode/containers.py50
-rwxr-xr-xsrc/conf_mode/dns_forwarding.py25
-rwxr-xr-xsrc/conf_mode/firewall-interface.py13
-rwxr-xr-xsrc/conf_mode/firewall.py130
-rwxr-xr-xsrc/conf_mode/flow_accounting_conf.py14
-rwxr-xr-xsrc/conf_mode/http-api.py9
-rwxr-xr-xsrc/conf_mode/interfaces-bonding.py8
-rwxr-xr-xsrc/conf_mode/interfaces-bridge.py3
-rwxr-xr-xsrc/conf_mode/interfaces-dummy.py2
-rwxr-xr-xsrc/conf_mode/interfaces-ethernet.py24
-rwxr-xr-xsrc/conf_mode/interfaces-geneve.py2
-rwxr-xr-xsrc/conf_mode/interfaces-l2tpv3.py2
-rwxr-xr-xsrc/conf_mode/interfaces-loopback.py2
-rwxr-xr-xsrc/conf_mode/interfaces-macsec.py2
-rwxr-xr-xsrc/conf_mode/interfaces-openvpn.py36
-rwxr-xr-xsrc/conf_mode/interfaces-pppoe.py2
-rwxr-xr-xsrc/conf_mode/interfaces-pseudo-ethernet.py2
-rwxr-xr-xsrc/conf_mode/interfaces-tunnel.py91
-rwxr-xr-xsrc/conf_mode/interfaces-vti.py2
-rwxr-xr-xsrc/conf_mode/interfaces-vxlan.py56
-rwxr-xr-xsrc/conf_mode/interfaces-wireguard.py2
-rwxr-xr-xsrc/conf_mode/interfaces-wireless.py2
-rwxr-xr-xsrc/conf_mode/interfaces-wwan.py97
-rwxr-xr-xsrc/conf_mode/lldp.py235
-rwxr-xr-xsrc/conf_mode/nat.py16
-rwxr-xr-xsrc/conf_mode/policy-local-route.py205
-rwxr-xr-xsrc/conf_mode/policy-route-interface.py8
-rwxr-xr-xsrc/conf_mode/policy-route.py227
-rwxr-xr-xsrc/conf_mode/policy.py7
-rwxr-xr-xsrc/conf_mode/protocols_bgp.py19
-rwxr-xr-xsrc/conf_mode/protocols_isis.py44
-rwxr-xr-xsrc/conf_mode/protocols_mpls.py21
-rwxr-xr-xsrc/conf_mode/protocols_ospf.py11
-rwxr-xr-xsrc/conf_mode/protocols_static.py4
-rwxr-xr-xsrc/conf_mode/qos.py87
-rwxr-xr-xsrc/conf_mode/service_ipoe-server.py23
-rwxr-xr-xsrc/conf_mode/service_monitoring_telegraf.py25
-rwxr-xr-xsrc/conf_mode/service_upnp.py157
-rwxr-xr-xsrc/conf_mode/system-ip.py43
-rwxr-xr-xsrc/conf_mode/system-ipv6.py99
-rwxr-xr-xsrc/conf_mode/system-login.py21
-rwxr-xr-xsrc/conf_mode/system-syslog.py14
-rwxr-xr-xsrc/conf_mode/vrf.py108
-rwxr-xr-xsrc/conf_mode/zone_policy.py24
46 files changed, 1363 insertions, 640 deletions
diff --git a/src/conf_mode/conntrack.py b/src/conf_mode/conntrack.py
index c65ef9540..aabf2bdf5 100755
--- a/src/conf_mode/conntrack.py
+++ b/src/conf_mode/conntrack.py
@@ -35,6 +35,7 @@ airbag.enable()
conntrack_config = r'/etc/modprobe.d/vyatta_nf_conntrack.conf'
sysctl_file = r'/run/sysctl/10-vyos-conntrack.conf'
+nftables_ct_file = r'/run/nftables-ct.conf'
# Every ALG (Application Layer Gateway) consists of either a Kernel Object
# also called a Kernel Module/Driver or some rules present in iptables
@@ -81,16 +82,35 @@ def get_config(config=None):
# 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)
+ # XXX: T2665: we can not safely rely on the defaults() when there are
+ # tagNodes in place, it is better to blend in the defaults manually.
+ if 'timeout' in default_values and 'custom' in default_values['timeout']:
+ del default_values['timeout']['custom']
conntrack = dict_merge(default_values, conntrack)
return conntrack
def verify(conntrack):
+ if dict_search('ignore.rule', conntrack) != None:
+ for rule, rule_config in conntrack['ignore']['rule'].items():
+ if dict_search('destination.port', rule_config) or \
+ dict_search('source.port', rule_config):
+ if 'protocol' not in rule_config or rule_config['protocol'] not in ['tcp', 'udp']:
+ raise ConfigError(f'Port requires tcp or udp as protocol in rule {rule}')
+
return None
def generate(conntrack):
render(conntrack_config, 'conntrack/vyos_nf_conntrack.conf.tmpl', conntrack)
render(sysctl_file, 'conntrack/sysctl.conf.tmpl', conntrack)
+ render(nftables_ct_file, 'conntrack/nftables-ct.tmpl', conntrack)
+
+ # dry-run newly generated configuration
+ tmp = run(f'nft -c -f {nftables_ct_file}')
+ if tmp > 0:
+ if os.path.exists(nftables_ct_file):
+ os.unlink(nftables_ct_file)
+ raise ConfigError('Configuration file errors encountered!')
return None
@@ -127,6 +147,9 @@ def apply(conntrack):
if not find_nftables_ct_rule(rule):
cmd(f'nft insert rule ip raw VYOS_CT_HELPER {rule}')
+ # Load new nftables ruleset
+ cmd(f'nft -f {nftables_ct_file}')
+
if process_named_running('conntrackd'):
# Reload conntrack-sync daemon to fetch new sysctl values
resync_conntrackd()
diff --git a/src/conf_mode/conntrack_sync.py b/src/conf_mode/conntrack_sync.py
index 8f9837c2b..34d1f7398 100755
--- a/src/conf_mode/conntrack_sync.py
+++ b/src/conf_mode/conntrack_sync.py
@@ -93,9 +93,9 @@ def verify(conntrack):
raise ConfigError('Can not configure expect-sync "all" with other protocols!')
if 'listen_address' in conntrack:
- address = conntrack['listen_address']
- if not is_addr_assigned(address):
- raise ConfigError(f'Specified listen-address {address} not assigned to any interface!')
+ for address in conntrack['listen_address']:
+ if not is_addr_assigned(address):
+ raise ConfigError(f'Specified listen-address {address} not assigned to any interface!')
vrrp_group = dict_search('failover_mechanism.vrrp.sync_group', conntrack)
if vrrp_group == None:
diff --git a/src/conf_mode/containers.py b/src/conf_mode/containers.py
index 2e14e0b25..516671844 100755
--- a/src/conf_mode/containers.py
+++ b/src/conf_mode/containers.py
@@ -122,6 +122,18 @@ def verify(container):
raise ConfigError(f'IP address "{address}" can not be used for a container, '\
'reserved for the container engine!')
+ if 'device' in container_config:
+ for dev, dev_config in container_config['device'].items():
+ if 'source' not in dev_config:
+ raise ConfigError(f'Device "{dev}" has no source path configured!')
+
+ if 'destination' not in dev_config:
+ raise ConfigError(f'Device "{dev}" has no destination path configured!')
+
+ source = dev_config['source']
+ if not os.path.exists(source):
+ raise ConfigError(f'Device "{dev}" source path "{source}" does not exist!')
+
if 'environment' in container_config:
for var, cfg in container_config['environment'].items():
if 'value' not in cfg:
@@ -266,6 +278,14 @@ def apply(container):
c = c.replace('-', '_')
cap_add += f' --cap-add={c}'
+ # Add a host device to the container /dev/x:/dev/x
+ device = ''
+ if 'device' in container_config:
+ for dev, dev_config in container_config['device'].items():
+ source_dev = dev_config['source']
+ dest_dev = dev_config['destination']
+ device += f' --device={source_dev}:{dest_dev}'
+
# Check/set environment options "-e foo=bar"
env_opt = ''
if 'environment' in container_config:
@@ -296,9 +316,9 @@ def apply(container):
container_base_cmd = f'podman run --detach --interactive --tty --replace {cap_add} ' \
f'--memory {memory}m --memory-swap 0 --restart {restart} ' \
- f'--name {name} {port} {volume} {env_opt}'
+ f'--name {name} {device} {port} {volume} {env_opt}'
if 'allow_host_networks' in container_config:
- _cmd(f'{container_base_cmd} --net host {image}')
+ run(f'{container_base_cmd} --net host {image}')
else:
for network in container_config['network']:
ipparam = ''
@@ -306,19 +326,25 @@ def apply(container):
address = container_config['network'][network]['address']
ipparam = f'--ip {address}'
- counter = 0
- while True:
- if counter >= 10:
- break
- try:
- _cmd(f'{container_base_cmd} --net {network} {ipparam} {image}')
- break
- except:
- counter = counter +1
- sleep(0.5)
+ run(f'{container_base_cmd} --net {network} {ipparam} {image}')
return None
+def run(container_cmd):
+ counter = 0
+ while True:
+ if counter >= 10:
+ break
+ try:
+ _cmd(container_cmd)
+ break
+ except:
+ counter = counter +1
+ sleep(0.5)
+
+ return None
+
+
if __name__ == '__main__':
try:
c = get_config()
diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py
index 23a16df63..fa9b21f20 100755
--- a/src/conf_mode/dns_forwarding.py
+++ b/src/conf_mode/dns_forwarding.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
@@ -16,6 +16,7 @@
import os
+from netifaces import interfaces
from sys import exit
from glob import glob
@@ -65,10 +66,6 @@ def get_config(config=None):
if conf.exists(base_nameservers):
dns.update({'system_name_server': conf.return_values(base_nameservers)})
- base_nameservers_dhcp = ['system', 'name-servers-dhcp']
- if conf.exists(base_nameservers_dhcp):
- dns.update({'system_name_server_dhcp': conf.return_values(base_nameservers_dhcp)})
-
if 'authoritative_domain' in dns:
dns['authoritative_zones'] = []
dns['authoritative_zone_errors'] = []
@@ -272,9 +269,8 @@ def verify(dns):
raise ConfigError('Invalid authoritative records have been defined')
if 'system' in dns:
- if not ('system_name_server' in dns or 'system_name_server_dhcp' in dns):
- print("Warning: No 'system name-server' or 'system " \
- "name-servers-dhcp' configured")
+ if not 'system_name_server' in dns:
+ print('Warning: No "system name-server" configured')
return None
@@ -339,10 +335,15 @@ def apply(dns):
hc.delete_name_server_tags_recursor(['system'])
# add dhcp nameserver tags for configured interfaces
- if 'system_name_server_dhcp' in dns:
- for interface in dns['system_name_server_dhcp']:
- hc.add_name_server_tags_recursor(['dhcp-' + interface,
- 'dhcpv6-' + interface ])
+ if 'system_name_server' in dns:
+ for interface in dns['system_name_server']:
+ # system_name_server key contains both IP addresses and interface
+ # names (DHCP) to use DNS servers. We need to check if the
+ # value is an interface name - only if this is the case, add the
+ # interface based DNS forwarder.
+ if interface in interfaces():
+ hc.add_name_server_tags_recursor(['dhcp-' + interface,
+ 'dhcpv6-' + interface ])
# hostsd will generate the forward-zones file
# the list and keys() are required as get returns a dict, not list
diff --git a/src/conf_mode/firewall-interface.py b/src/conf_mode/firewall-interface.py
index b0df9dff4..9a5d278e9 100755
--- a/src/conf_mode/firewall-interface.py
+++ b/src/conf_mode/firewall-interface.py
@@ -31,6 +31,9 @@ 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',
@@ -127,7 +130,7 @@ def apply(if_firewall):
name = dict_search_args(if_firewall, direction, 'name')
if name:
- rule_exists = cleanup_rule('ip filter', chain, if_prefix, ifname, name)
+ rule_exists = cleanup_rule('ip filter', chain, if_prefix, ifname, f'{NAME_PREFIX}{name}')
if not rule_exists:
rule_action = 'insert'
@@ -138,24 +141,24 @@ def apply(if_firewall):
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}')
+ 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, 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('ip filter', chain)
+ 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 {ipv6_name}')
+ 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)
diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py
index 75382034f..f33198a49 100755
--- a/src/conf_mode/firewall.py
+++ b/src/conf_mode/firewall.py
@@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
+import re
from glob import glob
from json import loads
@@ -22,6 +23,7 @@ from sys import exit
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.template import render
from vyos.util import cmd
@@ -33,7 +35,10 @@ from vyos import ConfigError
from vyos import airbag
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'},
@@ -49,6 +54,9 @@ sysfs_config = {
'twa_hazards_protection': {'sysfs': '/proc/sys/net/ipv4/tcp_rfc1337'}
}
+NAME_PREFIX = 'NAME_'
+NAME6_PREFIX = 'NAME6_'
+
preserve_chains = [
'INPUT',
'FORWARD',
@@ -65,6 +73,9 @@ preserve_chains = [
'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',
'network_group',
@@ -97,6 +108,35 @@ def get_firewall_interfaces(conf):
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 get_config(config=None):
if config:
conf = config
@@ -104,16 +144,15 @@ def get_config(config=None):
conf = Config()
base = ['firewall']
- if not conf.exists(base):
- return {}
-
firewall = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True,
no_tag_node_value_mangle=True)
default_values = defaults(base)
firewall = dict_merge(default_values, firewall)
+ 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)
@@ -121,6 +160,7 @@ def get_config(config=None):
firewall['trap_targets'] = conf.get_config_dict(['service', 'snmp', 'trap-target'],
key_mangling=('-', '_'), get_first_key=True,
no_tag_node_value_mangle=True)
+
return firewall
def verify_rule(firewall, rule_conf, ipv6):
@@ -131,6 +171,12 @@ def verify_rule(firewall, rule_conf, ipv6):
if {'match_frag', 'match_non_frag'} <= set(rule_conf['fragment']):
raise ConfigError('Cannot specify both "match-frag" and "match-non-frag"')
+ if 'limit' in rule_conf:
+ if 'rate' in rule_conf['limit']:
+ rate_int = re.sub(r'\D', '', rule_conf['limit']['rate'])
+ if int(rate_int) < 1:
+ raise ConfigError('Limit rate integer cannot be less than 1')
+
if 'ipsec' in rule_conf:
if {'match_ipsec', 'match_non_ipsec'} <= set(rule_conf['ipsec']):
raise ConfigError('Cannot specify both "match-ipsec" and "match-non-ipsec"')
@@ -139,6 +185,23 @@ def verify_rule(firewall, rule_conf, ipv6):
if not {'count', 'time'} <= set(rule_conf['recent']):
raise ConfigError('Recent "count" and "time" values must be defined')
+ tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags')
+ if tcp_flags:
+ if dict_search_args(rule_conf, 'protocol') != 'tcp':
+ raise ConfigError('Protocol must be tcp when specifying tcp flags')
+
+ not_flags = dict_search_args(rule_conf, 'tcp', 'flags', 'not')
+ if not_flags:
+ duplicates = [flag for flag in tcp_flags if flag in not_flags]
+ if duplicates:
+ raise ConfigError(f'Cannot match a tcp flag as set and not set')
+
+ if 'protocol' in rule_conf:
+ if rule_conf['protocol'] == 'icmp' and ipv6:
+ raise ConfigError(f'Cannot match IPv4 ICMP protocol on IPv6, use ipv6-icmp')
+ if rule_conf['protocol'] == 'ipv6-icmp' and not ipv6:
+ raise ConfigError(f'Cannot match IPv6 ICMP protocol on IPv4, use icmp')
+
for side in ['destination', 'source']:
if side in rule_conf:
side_conf = rule_conf[side]
@@ -151,16 +214,19 @@ def verify_rule(firewall, rule_conf, ipv6):
if group in side_conf['group']:
group_name = side_conf['group'][group]
- fw_group = f'ipv6_{group}' if ipv6 and group in ['address_group', 'network_group'] else group
+ if group_name and group_name[0] == '!':
+ group_name = group_name[1:]
- if not dict_search_args(firewall, 'group', fw_group):
- error_group = fw_group.replace("_", "-")
- raise ConfigError(f'Group defined in rule but {error_group} is not configured')
+ fw_group = f'ipv6_{group}' if ipv6 and group in ['address_group', 'network_group'] else group
+ error_group = fw_group.replace("_", "-")
+ group_obj = dict_search_args(firewall, 'group', fw_group, group_name)
- if group_name not in firewall['group'][fw_group]:
- error_group = group.replace("_", "-")
+ if group_obj is None:
raise ConfigError(f'Invalid {error_group} "{group_name}" on firewall rule')
+ if not group_obj:
+ print(f'WARNING: {error_group} "{group_name}" has no members')
+
if 'port' in side_conf or dict_search_args(side_conf, 'group', 'port_group'):
if 'protocol' not in rule_conf:
raise ConfigError('Protocol must be defined if specifying a port or port-group')
@@ -169,10 +235,6 @@ def verify_rule(firewall, rule_conf, ipv6):
raise ConfigError('Protocol must be tcp, udp, or tcp_udp when specifying a port or port-group')
def verify(firewall):
- # bail out early - looks like removal from running config
- if not firewall:
- return None
-
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')
@@ -195,16 +257,34 @@ def verify(firewall):
name = dict_search_args(if_firewall, direction, 'name')
ipv6_name = dict_search_args(if_firewall, direction, 'ipv6_name')
- if name and not dict_search_args(firewall, 'name', name):
+ if name and dict_search_args(firewall, 'name', name) == None:
raise ConfigError(f'Firewall name "{name}" is still referenced on interface {ifname}')
- if ipv6_name and not dict_search_args(firewall, 'ipv6_name', 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}')
+ 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')
+
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 = []
for table in ['ip filter', 'ip6 filter']:
state_chain = 'VYOS_STATE_POLICY' if table == 'ip filter' else 'VYOS_STATE_POLICY6'
json_str = cmd(f'nft -j list table {table}')
@@ -220,11 +300,12 @@ def cleanup_commands(firewall):
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):
+ 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):
+ 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:
rule = item['rule']
@@ -234,7 +315,10 @@ def cleanup_commands(firewall):
chain = rule['chain']
handle = rule['handle']
commands.append(f'delete rule {table} {chain} handle {handle}')
- return commands
+ elif 'set' in item:
+ set_name = item['set']['name']
+ commands_end.append(f'delete set {table} {set_name}')
+ return commands + commands_end
def generate(firewall):
if not os.path.exists(nftables_conf):
@@ -243,6 +327,7 @@ def generate(firewall):
firewall['cleanup_commands'] = cleanup_commands(firewall)
render(nftables_conf, 'firewall/nftables.tmpl', firewall)
+ render(nftables_defines_conf, 'firewall/nftables-defines.tmpl', firewall)
return None
def apply_sysfs(firewall):
@@ -306,6 +391,12 @@ def state_policy_rule_exists():
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)
+ if tmp > 0:
+ print('Warning: Failed to re-apply policy route configuration')
+
def apply(firewall):
if 'first_install' in firewall:
run('nfct helper add rpc inet tcp')
@@ -325,6 +416,9 @@ def apply(firewall):
apply_sysfs(firewall)
+ if firewall['policy_resync']:
+ resync_policy_route()
+
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 975f19acf..25bf54790 100755
--- a/src/conf_mode/flow_accounting_conf.py
+++ b/src/conf_mode/flow_accounting_conf.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
@@ -27,6 +27,7 @@ from vyos.configdict import dict_merge
from vyos.ifconfig import Section
from vyos.ifconfig import Interface
from vyos.template import render
+from vyos.util import call
from vyos.util import cmd
from vyos.validate import is_addr_assigned
from vyos.xml import defaults
@@ -35,6 +36,8 @@ from vyos import airbag
airbag.enable()
uacctd_conf_path = '/run/pmacct/uacctd.conf'
+systemd_service = 'uacctd.service'
+systemd_override = f'/etc/systemd/system/{systemd_service}.d/override.conf'
nftables_nflog_table = 'raw'
nftables_nflog_chain = 'VYOS_CT_PREROUTING_HOOK'
egress_nftables_nflog_table = 'inet mangle'
@@ -236,7 +239,10 @@ def generate(flow_config):
if not flow_config:
return None
- render(uacctd_conf_path, 'netflow/uacctd.conf.tmpl', flow_config)
+ render(uacctd_conf_path, 'pmacct/uacctd.conf.tmpl', flow_config)
+ render(systemd_override, 'pmacct/override.conf.tmpl', flow_config)
+ # Reload systemd manager configuration
+ call('systemctl daemon-reload')
def apply(flow_config):
action = 'restart'
@@ -246,13 +252,13 @@ def apply(flow_config):
_nftables_config([], 'egress')
# Stop flow-accounting daemon and remove configuration file
- cmd('systemctl stop uacctd.service')
+ call(f'systemctl stop {systemd_service}')
if os.path.exists(uacctd_conf_path):
os.unlink(uacctd_conf_path)
return
# Start/reload flow-accounting daemon
- cmd(f'systemctl restart uacctd.service')
+ call(f'systemctl restart {systemd_service}')
# configure nftables rules for defined interfaces
if 'interface' in flow_config:
diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py
index b5f5e919f..00f3d4f7f 100755
--- a/src/conf_mode/http-api.py
+++ b/src/conf_mode/http-api.py
@@ -66,6 +66,15 @@ def get_config(config=None):
if conf.exists('debug'):
http_api['debug'] = True
+ # this node is not available by CLI by default, and is reserved for
+ # the graphql tools. One can enable it for testing, with the warning
+ # that this will open an unauthenticated server. To do so
+ # mkdir /opt/vyatta/share/vyatta-cfg/templates/service/https/api/gql
+ # touch /opt/vyatta/share/vyatta-cfg/templates/service/https/api/gql/node.def
+ # and configure; editing the config alone is insufficient.
+ if conf.exists('gql'):
+ http_api['gql'] = True
+
if conf.exists('socket'):
http_api['socket'] = True
diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py
index 431d65f1f..ad5a0f499 100755
--- a/src/conf_mode/interfaces-bonding.py
+++ b/src/conf_mode/interfaces-bonding.py
@@ -27,8 +27,9 @@ from vyos.configdict import is_source_interface
from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_dhcpv6
-from vyos.configverify import verify_source_interface
+from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_source_interface
from vyos.configverify import verify_vlan_config
from vyos.configverify import verify_vrf
from vyos.ifconfig import BondIf
@@ -132,10 +133,10 @@ def verify(bond):
return None
if 'arp_monitor' in bond:
- if 'target' in bond['arp_monitor'] and len(int(bond['arp_monitor']['target'])) > 16:
+ if 'target' in bond['arp_monitor'] and len(bond['arp_monitor']['target']) > 16:
raise ConfigError('The maximum number of arp-monitor targets is 16')
- if 'interval' in bond['arp_monitor'] and len(int(bond['arp_monitor']['interval'])) > 0:
+ if 'interval' in bond['arp_monitor'] and int(bond['arp_monitor']['interval']) > 0:
if bond['mode'] in ['802.3ad', 'balance-tlb', 'balance-alb']:
raise ConfigError('ARP link monitoring does not work for mode 802.3ad, ' \
'transmit-load-balance or adaptive-load-balance')
@@ -149,6 +150,7 @@ def verify(bond):
verify_address(bond)
verify_dhcpv6(bond)
verify_vrf(bond)
+ verify_mirror_redirect(bond)
# use common function to verify VLAN configuration
verify_vlan_config(bond)
diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py
index 4d3ebc587..b1f7e6d7c 100755
--- a/src/conf_mode/interfaces-bridge.py
+++ b/src/conf_mode/interfaces-bridge.py
@@ -22,12 +22,12 @@ from netifaces import interfaces
from vyos.config import Config
from vyos.configdict import get_interface_dict
from vyos.configdict import node_changed
-from vyos.configdict import leaf_node_changed
from vyos.configdict import is_member
from vyos.configdict import is_source_interface
from vyos.configdict import has_vlan_subinterface_configured
from vyos.configdict import dict_merge
from vyos.configverify import verify_dhcpv6
+from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_vrf
from vyos.ifconfig import BridgeIf
from vyos.validate import has_address_configured
@@ -106,6 +106,7 @@ def verify(bridge):
verify_dhcpv6(bridge)
verify_vrf(bridge)
+ verify_mirror_redirect(bridge)
ifname = bridge['ifname']
diff --git a/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py
index 55c783f38..4a1eb7b93 100755
--- a/src/conf_mode/interfaces-dummy.py
+++ b/src/conf_mode/interfaces-dummy.py
@@ -21,6 +21,7 @@ from vyos.configdict import get_interface_dict
from vyos.configverify import verify_vrf
from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
+from vyos.configverify import verify_mirror_redirect
from vyos.ifconfig import DummyIf
from vyos import ConfigError
from vyos import airbag
@@ -46,6 +47,7 @@ def verify(dummy):
verify_vrf(dummy)
verify_address(dummy)
+ verify_mirror_redirect(dummy)
return None
diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py
index e7250fb49..6aea7a80e 100755
--- a/src/conf_mode/interfaces-ethernet.py
+++ b/src/conf_mode/interfaces-ethernet.py
@@ -25,14 +25,16 @@ from vyos.configverify import verify_address
from vyos.configverify import verify_dhcpv6
from vyos.configverify import verify_eapol
from vyos.configverify import verify_interface_exists
-from vyos.configverify import verify_mirror
+from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_mtu
from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_vlan_config
from vyos.configverify import verify_vrf
from vyos.ethtool import Ethtool
from vyos.ifconfig import EthernetIf
-from vyos.pki import wrap_certificate
+from vyos.pki import find_chain
+from vyos.pki import encode_certificate
+from vyos.pki import load_certificate
from vyos.pki import wrap_private_key
from vyos.template import render
from vyos.util import call
@@ -81,7 +83,7 @@ def verify(ethernet):
verify_address(ethernet)
verify_vrf(ethernet)
verify_eapol(ethernet)
- verify_mirror(ethernet)
+ verify_mirror_redirect(ethernet)
ethtool = Ethtool(ifname)
# No need to check speed and duplex keys as both have default values.
@@ -159,16 +161,26 @@ def generate(ethernet):
cert_name = ethernet['eapol']['certificate']
pki_cert = ethernet['pki']['certificate'][cert_name]
- write_file(cert_file_path, wrap_certificate(pki_cert['certificate']))
+ loaded_pki_cert = load_certificate(pki_cert['certificate'])
+ loaded_ca_certs = {load_certificate(c['certificate'])
+ for c in ethernet['pki']['ca'].values()}
+
+ cert_full_chain = find_chain(loaded_pki_cert, loaded_ca_certs)
+
+ write_file(cert_file_path,
+ '\n'.join(encode_certificate(c) for c in cert_full_chain))
write_file(cert_key_path, wrap_private_key(pki_cert['private']['key']))
if 'ca_certificate' in ethernet['eapol']:
ca_cert_file_path = os.path.join(cfg_dir, f'{ifname}_ca.pem')
ca_cert_name = ethernet['eapol']['ca_certificate']
- pki_ca_cert = ethernet['pki']['ca'][cert_name]
+ pki_ca_cert = ethernet['pki']['ca'][ca_cert_name]
+
+ loaded_ca_cert = load_certificate(pki_ca_cert['certificate'])
+ ca_full_chain = find_chain(loaded_ca_cert, loaded_ca_certs)
write_file(ca_cert_file_path,
- wrap_certificate(pki_ca_cert['certificate']))
+ '\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)):
diff --git a/src/conf_mode/interfaces-geneve.py b/src/conf_mode/interfaces-geneve.py
index 2a63b60aa..3a668226b 100755
--- a/src/conf_mode/interfaces-geneve.py
+++ b/src/conf_mode/interfaces-geneve.py
@@ -24,6 +24,7 @@ from vyos.configdict import get_interface_dict
from vyos.configverify import verify_address
from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_bridge_delete
+from vyos.configverify import verify_mirror_redirect
from vyos.ifconfig import GeneveIf
from vyos import ConfigError
@@ -50,6 +51,7 @@ def verify(geneve):
verify_mtu_ipv6(geneve)
verify_address(geneve)
+ verify_mirror_redirect(geneve)
if 'remote' not in geneve:
raise ConfigError('Remote side must be configured')
diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py
index 9b6ddd5aa..22256bf4f 100755
--- a/src/conf_mode/interfaces-l2tpv3.py
+++ b/src/conf_mode/interfaces-l2tpv3.py
@@ -25,6 +25,7 @@ from vyos.configdict import leaf_node_changed
from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_mirror_redirect
from vyos.ifconfig import L2TPv3If
from vyos.util import check_kmod
from vyos.validate import is_addr_assigned
@@ -76,6 +77,7 @@ def verify(l2tpv3):
verify_mtu_ipv6(l2tpv3)
verify_address(l2tpv3)
+ verify_mirror_redirect(l2tpv3)
return None
def generate(l2tpv3):
diff --git a/src/conf_mode/interfaces-loopback.py b/src/conf_mode/interfaces-loopback.py
index 193334443..e4bc15bb5 100755
--- a/src/conf_mode/interfaces-loopback.py
+++ b/src/conf_mode/interfaces-loopback.py
@@ -20,6 +20,7 @@ from sys import exit
from vyos.config import Config
from vyos.configdict import get_interface_dict
+from vyos.configverify import verify_mirror_redirect
from vyos.ifconfig import LoopbackIf
from vyos import ConfigError
from vyos import airbag
@@ -39,6 +40,7 @@ def get_config(config=None):
return loopback
def verify(loopback):
+ verify_mirror_redirect(loopback)
return None
def generate(loopback):
diff --git a/src/conf_mode/interfaces-macsec.py b/src/conf_mode/interfaces-macsec.py
index eab69f36e..96fc1c41c 100755
--- a/src/conf_mode/interfaces-macsec.py
+++ b/src/conf_mode/interfaces-macsec.py
@@ -29,6 +29,7 @@ from vyos.configverify import verify_vrf
from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_source_interface
from vyos import ConfigError
from vyos import airbag
@@ -66,6 +67,7 @@ def verify(macsec):
verify_vrf(macsec)
verify_mtu_ipv6(macsec)
verify_address(macsec)
+ verify_mirror_redirect(macsec)
if not (('security' in macsec) and
('cipher' in macsec['security'])):
diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index 3b8fae710..83d1c6d9b 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2021 VyOS maintainers and contributors
+# Copyright (C) 2019-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -32,8 +32,10 @@ from shutil import rmtree
from vyos.config import Config
from vyos.configdict import get_interface_dict
+from vyos.configdict import leaf_node_changed
from vyos.configverify import verify_vrf
from vyos.configverify import verify_bridge_delete
+from vyos.configverify import verify_mirror_redirect
from vyos.ifconfig import VTunIf
from vyos.pki import load_dh_parameters
from vyos.pki import load_private_key
@@ -47,6 +49,7 @@ from vyos.template import is_ipv4
from vyos.template import is_ipv6
from vyos.util import call
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 makedir
@@ -87,6 +90,9 @@ def get_config(config=None):
if 'deleted' not in openvpn:
openvpn['pki'] = tmp_pki
+ tmp = leaf_node_changed(conf, ['openvpn-option'])
+ if tmp: openvpn['restart_required'] = ''
+
# We have to get the dict using 'get_config_dict' instead of 'get_interface_dict'
# as 'get_interface_dict' merges the defaults in, so we can not check for defaults in there.
tmp = conf.get_config_dict(base + [openvpn['ifname']], get_first_key=True)
@@ -225,11 +231,12 @@ def verify(openvpn):
if 'local_address' not in openvpn and 'is_bridge_member' not in openvpn:
raise ConfigError('Must specify "local-address" or add interface to bridge')
- if len([addr for addr in openvpn['local_address'] if is_ipv4(addr)]) > 1:
- raise ConfigError('Only one IPv4 local-address can be specified')
+ if 'local_address' in openvpn:
+ if len([addr for addr in openvpn['local_address'] if is_ipv4(addr)]) > 1:
+ raise ConfigError('Only one IPv4 local-address can be specified')
- if len([addr for addr in openvpn['local_address'] if is_ipv6(addr)]) > 1:
- raise ConfigError('Only one IPv6 local-address can be specified')
+ if len([addr for addr in openvpn['local_address'] if is_ipv6(addr)]) > 1:
+ raise ConfigError('Only one IPv6 local-address can be specified')
if openvpn['device_type'] == 'tun':
if 'remote_address' not in openvpn:
@@ -268,7 +275,7 @@ def verify(openvpn):
if dict_search('remote_host', openvpn) in dict_search('remote_address', openvpn):
raise ConfigError('"remote-address" and "remote-host" can not be the same')
- if openvpn['device_type'] == 'tap':
+ if openvpn['device_type'] == 'tap' and 'local_address' in openvpn:
# we can only have one local_address, this is ensured above
v4addr = None
for laddr in openvpn['local_address']:
@@ -423,8 +430,8 @@ def verify(openvpn):
# verify specified IP address is present on any interface on this system
if 'local_host' in openvpn:
if not is_addr_assigned(openvpn['local_host']):
- raise ConfigError('local-host IP address "{local_host}" not assigned' \
- ' to any interface'.format(**openvpn))
+ print('local-host IP address "{local_host}" not assigned' \
+ ' to any interface'.format(**openvpn))
# TCP active
if openvpn['protocol'] == 'tcp-active':
@@ -489,6 +496,7 @@ def verify(openvpn):
raise ConfigError('Username for authentication is missing')
verify_vrf(openvpn)
+ verify_mirror_redirect(openvpn)
return None
@@ -647,9 +655,19 @@ def apply(openvpn):
return None
+ # verify specified IP address is present on any interface on this system
+ # Allow to bind service to nonlocal address, if it virtaual-vrrp address
+ # or if address will be assign later
+ if 'local_host' in openvpn:
+ if not is_addr_assigned(openvpn['local_host']):
+ cmd('sysctl -w net.ipv4.ip_nonlocal_bind=1')
+
# No matching OpenVPN process running - maybe it got killed or none
# existed - nevertheless, spawn new OpenVPN process
- call(f'systemctl reload-or-restart openvpn@{interface}.service')
+ action = 'reload-or-restart'
+ if 'restart_required' in openvpn:
+ action = 'restart'
+ call(f'systemctl {action} openvpn@{interface}.service')
o = VTunIf(**openvpn)
o.update(openvpn)
diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py
index 584adc75e..bfb1fadd5 100755
--- a/src/conf_mode/interfaces-pppoe.py
+++ b/src/conf_mode/interfaces-pppoe.py
@@ -28,6 +28,7 @@ from vyos.configverify import verify_source_interface
from vyos.configverify import verify_interface_exists
from vyos.configverify import verify_vrf
from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_mirror_redirect
from vyos.ifconfig import PPPoEIf
from vyos.template import render
from vyos.util import call
@@ -85,6 +86,7 @@ def verify(pppoe):
verify_authentication(pppoe)
verify_vrf(pppoe)
verify_mtu_ipv6(pppoe)
+ verify_mirror_redirect(pppoe)
if {'connect_on_demand', 'vrf'} <= set(pppoe):
raise ConfigError('On-demand dialing and VRF can not be used at the same time')
diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py
index 945a2ea9c..f2c85554f 100755
--- a/src/conf_mode/interfaces-pseudo-ethernet.py
+++ b/src/conf_mode/interfaces-pseudo-ethernet.py
@@ -25,6 +25,7 @@ from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_source_interface
from vyos.configverify import verify_vlan_config
from vyos.configverify import verify_mtu_parent
+from vyos.configverify import verify_mirror_redirect
from vyos.ifconfig import MACVLANIf
from vyos import ConfigError
@@ -60,6 +61,7 @@ def verify(peth):
verify_vrf(peth)
verify_address(peth)
verify_mtu_parent(peth, peth['parent'])
+ verify_mirror_redirect(peth)
# use common function to verify VLAN configuration
verify_vlan_config(peth)
diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py
index 30f57ec0c..f4668d976 100755
--- a/src/conf_mode/interfaces-tunnel.py
+++ b/src/conf_mode/interfaces-tunnel.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2021 VyOS maintainers and contributors
+# Copyright (C) 2018-2022 yOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -18,24 +18,20 @@ import os
from sys import exit
from netifaces import interfaces
-from ipaddress import IPv4Address
from vyos.config import Config
-from vyos.configdict import dict_merge
from vyos.configdict import get_interface_dict
-from vyos.configdict import node_changed
from vyos.configdict import leaf_node_changed
from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_interface_exists
from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_vrf
from vyos.configverify import verify_tunnel
from vyos.ifconfig import Interface
from vyos.ifconfig import Section
from vyos.ifconfig import TunnelIf
-from vyos.template import is_ipv4
-from vyos.template import is_ipv6
from vyos.util import get_interface_config
from vyos.util import dict_search
from vyos import ConfigError
@@ -54,8 +50,24 @@ def get_config(config=None):
base = ['interfaces', 'tunnel']
tunnel = get_interface_dict(conf, base)
- tmp = leaf_node_changed(conf, ['encapsulation'])
- if tmp: tunnel.update({'encapsulation_changed': {}})
+ if 'deleted' not in tunnel:
+ tmp = leaf_node_changed(conf, ['encapsulation'])
+ if tmp: tunnel.update({'encapsulation_changed': {}})
+
+ # We also need to inspect other configured tunnels as there are Kernel
+ # restrictions where we need to comply. E.g. GRE tunnel key can't be used
+ # twice, or with multiple GRE tunnels to the same location we must specify
+ # a GRE key
+ conf.set_level(base)
+ tunnel['other_tunnels'] = conf.get_config_dict([], key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
+ # delete our own instance from this dict
+ ifname = tunnel['ifname']
+ del tunnel['other_tunnels'][ifname]
+ # if only one tunnel is present on the system, no need to keep this key
+ if len(tunnel['other_tunnels']) == 0:
+ del tunnel['other_tunnels']
# We must check if our interface is configured to be a DMVPN member
nhrp_base = ['protocols', 'nhrp', 'tunnel']
@@ -96,35 +108,47 @@ def verify(tunnel):
if 'direction' not in tunnel['parameters']['erspan']:
raise ConfigError('ERSPAN version 2 requires direction to be set!')
- # If tunnel source address any and key not set
+ # If tunnel source is any and gre key is not set
+ interface = tunnel['ifname']
if tunnel['encapsulation'] in ['gre'] and \
dict_search('source_address', tunnel) == '0.0.0.0' and \
dict_search('parameters.ip.key', tunnel) == None:
- raise ConfigError('Tunnel parameters ip key must be set!')
-
- if tunnel['encapsulation'] in ['gre', 'gretap']:
- if dict_search('parameters.ip.key', tunnel) != None:
- # Check pairs tunnel source-address/encapsulation/key with exists tunnels.
- # Prevent the same key for 2 tunnels with same source-address/encap. T2920
- for tunnel_if in Section.interfaces('tunnel'):
- # It makes no sense to run the test for re-used GRE keys on our
- # own interface we are currently working on
- if tunnel['ifname'] == tunnel_if:
- continue
- tunnel_cfg = get_interface_config(tunnel_if)
- # no match on encapsulation - bail out
- if dict_search('linkinfo.info_kind', tunnel_cfg) != tunnel['encapsulation']:
- continue
- new_source_address = dict_search('source_address', tunnel)
- # Convert tunnel key to ip key, format "ip -j link show"
- # 1 => 0.0.0.1, 999 => 0.0.3.231
- orig_new_key = dict_search('parameters.ip.key', tunnel)
- new_key = IPv4Address(int(orig_new_key))
- new_key = str(new_key)
- if dict_search('address', tunnel_cfg) == new_source_address and \
- dict_search('linkinfo.info_data.ikey', tunnel_cfg) == new_key:
- raise ConfigError(f'Key "{orig_new_key}" for source-address "{new_source_address}" ' \
+ raise ConfigError(f'"parameters ip key" must be set for {interface} when '\
+ 'encapsulation is GRE!')
+
+ gre_encapsulations = ['gre', 'gretap']
+ if tunnel['encapsulation'] in gre_encapsulations and 'other_tunnels' in tunnel:
+ # Check pairs tunnel source-address/encapsulation/key with exists tunnels.
+ # Prevent the same key for 2 tunnels with same source-address/encap. T2920
+ for o_tunnel, o_tunnel_conf in tunnel['other_tunnels'].items():
+ # no match on encapsulation - bail out
+ our_encapsulation = tunnel['encapsulation']
+ their_encapsulation = o_tunnel_conf['encapsulation']
+ if our_encapsulation in gre_encapsulations and their_encapsulation \
+ not in gre_encapsulations:
+ continue
+
+ our_address = dict_search('source_address', tunnel)
+ our_key = dict_search('parameters.ip.key', tunnel)
+ their_address = dict_search('source_address', o_tunnel_conf)
+ their_key = dict_search('parameters.ip.key', o_tunnel_conf)
+ if our_key != None:
+ if their_address == our_address and their_key == our_key:
+ raise ConfigError(f'Key "{our_key}" for source-address "{our_address}" ' \
f'is already used for tunnel "{tunnel_if}"!')
+ else:
+ our_source_if = dict_search('source_interface', tunnel)
+ their_source_if = dict_search('source_interface', o_tunnel_conf)
+ our_remote = dict_search('remote', tunnel)
+ their_remote = dict_search('remote', o_tunnel_conf)
+ # If no IP GRE key is defined we can not have more then one GRE tunnel
+ # bound to any one interface/IP address and the same remote. This will
+ # result in a OS PermissionError: add tunnel "gre0" failed: File exists
+ if (their_address == our_address or our_source_if == their_source_if) and \
+ our_remote == their_remote:
+ raise ConfigError(f'Missing required "ip key" parameter when '\
+ 'running more then one GRE based tunnel on the '\
+ 'same source-interface/source-address')
# Keys are not allowed with ipip and sit tunnels
if tunnel['encapsulation'] in ['ipip', 'sit']:
@@ -134,6 +158,7 @@ def verify(tunnel):
verify_mtu_ipv6(tunnel)
verify_address(tunnel)
verify_vrf(tunnel)
+ verify_mirror_redirect(tunnel)
if 'source_interface' in tunnel:
verify_interface_exists(tunnel['source_interface'])
diff --git a/src/conf_mode/interfaces-vti.py b/src/conf_mode/interfaces-vti.py
index 57950ffea..f06fdff1b 100755
--- a/src/conf_mode/interfaces-vti.py
+++ b/src/conf_mode/interfaces-vti.py
@@ -19,6 +19,7 @@ from sys import exit
from vyos.config import Config
from vyos.configdict import get_interface_dict
+from vyos.configverify import verify_mirror_redirect
from vyos.ifconfig import VTIIf
from vyos.util import dict_search
from vyos import ConfigError
@@ -39,6 +40,7 @@ def get_config(config=None):
return vti
def verify(vti):
+ verify_mirror_redirect(vti)
return None
def generate(vti):
diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py
index 1f097c4e3..0a9b51cac 100755
--- a/src/conf_mode/interfaces-vxlan.py
+++ b/src/conf_mode/interfaces-vxlan.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2020 VyOS maintainers and contributors
+# Copyright (C) 2019-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -21,9 +21,11 @@ from netifaces import interfaces
from vyos.config import Config
from vyos.configdict import get_interface_dict
+from vyos.configdict import leaf_node_changed
from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_source_interface
from vyos.ifconfig import Interface
from vyos.ifconfig import VXLANIf
@@ -34,8 +36,8 @@ airbag.enable()
def get_config(config=None):
"""
- Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
- interface name will be added or a deleted flag
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at least
+ the interface name will be added or a deleted flag
"""
if config:
conf = config
@@ -44,6 +46,16 @@ def get_config(config=None):
base = ['interfaces', 'vxlan']
vxlan = get_interface_dict(conf, base)
+ # VXLAN interfaces are picky and require recreation if certain parameters
+ # change. But a VXLAN interface should - of course - not be re-created if
+ # it's description or IP address is adjusted. Feels somehow logic doesn't it?
+ for cli_option in ['external', 'gpe', 'group', 'port', 'remote',
+ 'source-address', 'source-interface', 'vni',
+ 'parameters ip dont-fragment', 'parameters ip tos',
+ 'parameters ip ttl']:
+ if leaf_node_changed(conf, cli_option.split()):
+ vxlan.update({'rebuild_required': {}})
+
# We need to verify that no other VXLAN tunnel is configured when external
# mode is in use - Linux Kernel limitation
conf.set_level(base)
@@ -70,8 +82,7 @@ def verify(vxlan):
if 'group' in vxlan:
if 'source_interface' not in vxlan:
- raise ConfigError('Multicast VXLAN requires an underlaying interface ')
-
+ raise ConfigError('Multicast VXLAN requires an underlaying interface')
verify_source_interface(vxlan)
if not any(tmp in ['group', 'remote', 'source_address'] for tmp in vxlan):
@@ -108,22 +119,42 @@ def verify(vxlan):
raise ConfigError(f'Underlaying device MTU is to small ({lower_mtu} '\
f'bytes) for VXLAN overhead ({vxlan_overhead} bytes!)')
+ # Check for mixed IPv4 and IPv6 addresses
+ protocol = None
+ if 'source_address' in vxlan:
+ if is_ipv6(vxlan['source_address']):
+ protocol = 'ipv6'
+ else:
+ protocol = 'ipv4'
+
+ if 'remote' in vxlan:
+ error_msg = 'Can not mix both IPv4 and IPv6 for VXLAN underlay'
+ for remote in vxlan['remote']:
+ if is_ipv6(remote):
+ if protocol == 'ipv4':
+ raise ConfigError(error_msg)
+ protocol = 'ipv6'
+ else:
+ if protocol == 'ipv6':
+ raise ConfigError(error_msg)
+ protocol = 'ipv4'
+
verify_mtu_ipv6(vxlan)
verify_address(vxlan)
+ verify_mirror_redirect(vxlan)
return None
-
def generate(vxlan):
return None
-
def apply(vxlan):
# Check if the VXLAN interface already exists
- if vxlan['ifname'] in interfaces():
- v = VXLANIf(vxlan['ifname'])
- # VXLAN is super picky and the tunnel always needs to be recreated,
- # thus we can simply always delete it first.
- v.remove()
+ if 'rebuild_required' in vxlan or 'delete' in vxlan:
+ if vxlan['ifname'] in interfaces():
+ v = VXLANIf(vxlan['ifname'])
+ # VXLAN is super picky and the tunnel always needs to be recreated,
+ # thus we can simply always delete it first.
+ v.remove()
if 'deleted' not in vxlan:
# Finally create the new interface
@@ -132,7 +163,6 @@ def apply(vxlan):
return None
-
if __name__ == '__main__':
try:
c = get_config()
diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py
index da64dd076..b404375d6 100755
--- a/src/conf_mode/interfaces-wireguard.py
+++ b/src/conf_mode/interfaces-wireguard.py
@@ -28,6 +28,7 @@ from vyos.configverify import verify_vrf
from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_mirror_redirect
from vyos.ifconfig import WireGuardIf
from vyos.util import check_kmod
from vyos.util import check_port_availability
@@ -70,6 +71,7 @@ def verify(wireguard):
verify_mtu_ipv6(wireguard)
verify_address(wireguard)
verify_vrf(wireguard)
+ verify_mirror_redirect(wireguard)
if 'private_key' not in wireguard:
raise ConfigError('Wireguard private-key not defined')
diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py
index af35b5f03..500952df1 100755
--- a/src/conf_mode/interfaces-wireless.py
+++ b/src/conf_mode/interfaces-wireless.py
@@ -27,6 +27,7 @@ from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_dhcpv6
from vyos.configverify import verify_source_interface
+from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_vlan_config
from vyos.configverify import verify_vrf
from vyos.ifconfig import WiFiIf
@@ -189,6 +190,7 @@ def verify(wifi):
verify_address(wifi)
verify_vrf(wifi)
+ verify_mirror_redirect(wifi)
# use common function to verify VLAN configuration
verify_vlan_config(wifi)
diff --git a/src/conf_mode/interfaces-wwan.py b/src/conf_mode/interfaces-wwan.py
index a4b033374..9a33039a3 100755
--- a/src/conf_mode/interfaces-wwan.py
+++ b/src/conf_mode/interfaces-wwan.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-2021 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -21,8 +21,10 @@ from time import sleep
from vyos.config import Config
from vyos.configdict import get_interface_dict
+from vyos.configdict import leaf_node_changed
from vyos.configverify import verify_authentication
from vyos.configverify import verify_interface_exists
+from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_vrf
from vyos.ifconfig import WWANIf
from vyos.util import cmd
@@ -36,7 +38,7 @@ from vyos import airbag
airbag.enable()
service_name = 'ModemManager.service'
-cron_script = '/etc/cron.d/wwan'
+cron_script = '/etc/cron.d/vyos-wwan'
def get_config(config=None):
"""
@@ -50,6 +52,32 @@ def get_config(config=None):
base = ['interfaces', 'wwan']
wwan = get_interface_dict(conf, base)
+ # We should only terminate the WWAN session if critical parameters change.
+ # All parameters that can be changed on-the-fly (like interface description)
+ # should not lead to a reconnect!
+ tmp = leaf_node_changed(conf, ['address'])
+ if tmp: wwan.update({'shutdown_required': {}})
+
+ tmp = leaf_node_changed(conf, ['apn'])
+ if tmp: wwan.update({'shutdown_required': {}})
+
+ tmp = leaf_node_changed(conf, ['disable'])
+ if tmp: wwan.update({'shutdown_required': {}})
+
+ tmp = leaf_node_changed(conf, ['vrf'])
+ # leaf_node_changed() returns a list, as VRF is a non-multi node, there
+ # will be only one list element
+ if tmp: wwan.update({'vrf_old': tmp[0]})
+
+ tmp = leaf_node_changed(conf, ['authentication', 'user'])
+ if tmp: wwan.update({'shutdown_required': {}})
+
+ tmp = leaf_node_changed(conf, ['authentication', 'password'])
+ if tmp: wwan.update({'shutdown_required': {}})
+
+ tmp = leaf_node_changed(conf, ['ipv6', 'address', 'autoconf'])
+ if tmp: wwan.update({'shutdown_required': {}})
+
# We need to know the amount of other WWAN interfaces as ModemManager needs
# to be started or stopped.
conf.set_level(base)
@@ -57,8 +85,8 @@ def get_config(config=None):
get_first_key=True,
no_tag_node_value_mangle=True)
- # This if-clause is just to be sure - it will always evaluate to true
ifname = wwan['ifname']
+ # This if-clause is just to be sure - it will always evaluate to true
if ifname in wwan['other_interfaces']:
del wwan['other_interfaces'][ifname]
if len(wwan['other_interfaces']) == 0:
@@ -77,18 +105,31 @@ def verify(wwan):
verify_interface_exists(ifname)
verify_authentication(wwan)
verify_vrf(wwan)
+ verify_mirror_redirect(wwan)
return None
def generate(wwan):
if 'deleted' in wwan:
+ # We are the last WWAN interface - there are no other ones remaining
+ # thus the cronjob needs to go away, too
+ if 'other_interfaces' not in wwan:
+ if os.path.exists(cron_script):
+ os.unlink(cron_script)
return None
+ # Install cron triggered helper script to re-dial WWAN interfaces on
+ # disconnect - e.g. happens during RF signal loss. The script watches every
+ # WWAN interface - so there is only one instance.
if not os.path.exists(cron_script):
write_file(cron_script, '*/5 * * * * root /usr/libexec/vyos/vyos-check-wwan.py')
+
return None
def apply(wwan):
+ # ModemManager is required to dial WWAN connections - one instance is
+ # required to serve all modems. Activate ModemManager on first invocation
+ # of any WWAN interface.
if not is_systemd_service_active(service_name):
cmd(f'systemctl start {service_name}')
@@ -101,17 +142,19 @@ def apply(wwan):
break
sleep(0.250)
- # we only need the modem number. wwan0 -> 0, wwan1 -> 1
- modem = wwan['ifname'].lstrip('wwan')
- base_cmd = f'mmcli --modem {modem}'
- # Number of bearers is limited - always disconnect first
- cmd(f'{base_cmd} --simple-disconnect')
+ if 'shutdown_required' in wwan:
+ # we only need the modem number. wwan0 -> 0, wwan1 -> 1
+ modem = wwan['ifname'].lstrip('wwan')
+ base_cmd = f'mmcli --modem {modem}'
+ # Number of bearers is limited - always disconnect first
+ cmd(f'{base_cmd} --simple-disconnect')
w = WWANIf(wwan['ifname'])
if 'deleted' in wwan or 'disable' in wwan:
w.remove()
- # There are no other WWAN interfaces - stop the daemon
+ # We are the last WWAN interface - there are no other WWAN interfaces
+ # remaining, thus we can stop ModemManager and free resources.
if 'other_interfaces' not in wwan:
cmd(f'systemctl stop {service_name}')
# Clean CRON helper script which is used for to re-connect when
@@ -121,27 +164,25 @@ def apply(wwan):
return None
- ip_type = 'ipv4'
- slaac = dict_search('ipv6.address.autoconf', wwan) != None
- if 'address' in wwan:
- if 'dhcp' in wwan['address'] and ('dhcpv6' in wwan['address'] or slaac):
- ip_type = 'ipv4v6'
- elif 'dhcpv6' in wwan['address'] or slaac:
- ip_type = 'ipv6'
- elif 'dhcp' in wwan['address']:
- ip_type = 'ipv4'
-
- options = f'ip-type={ip_type},apn=' + wwan['apn']
- if 'authentication' in wwan:
- options += ',user={user},password={password}'.format(**wwan['authentication'])
-
- command = f'{base_cmd} --simple-connect="{options}"'
- call(command, stdout=DEVNULL)
- w.update(wwan)
+ if 'shutdown_required' in wwan:
+ ip_type = 'ipv4'
+ slaac = dict_search('ipv6.address.autoconf', wwan) != None
+ if 'address' in wwan:
+ if 'dhcp' in wwan['address'] and ('dhcpv6' in wwan['address'] or slaac):
+ ip_type = 'ipv4v6'
+ elif 'dhcpv6' in wwan['address'] or slaac:
+ ip_type = 'ipv6'
+ elif 'dhcp' in wwan['address']:
+ ip_type = 'ipv4'
- if 'other_interfaces' not in wwan and 'deleted' in wwan:
- cmd(f'systemctl start {service_name}')
+ options = f'ip-type={ip_type},apn=' + wwan['apn']
+ if 'authentication' in wwan:
+ options += ',user={user},password={password}'.format(**wwan['authentication'])
+ command = f'{base_cmd} --simple-connect="{options}"'
+ call(command, stdout=DEVNULL)
+
+ w.update(wwan)
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/lldp.py b/src/conf_mode/lldp.py
index 082c3e128..db8328259 100755
--- a/src/conf_mode/lldp.py
+++ b/src/conf_mode/lldp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2017-2020 VyOS maintainers and contributors
+# Copyright (C) 2017-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,19 +15,19 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
-import re
-from copy import deepcopy
from sys import exit
from vyos.config import Config
+from vyos.configdict import dict_merge
from vyos.validate import is_addr_assigned
from vyos.validate import is_loopback_addr
from vyos.version import get_version_data
-from vyos import ConfigError
from vyos.util import call
+from vyos.util import dict_search
+from vyos.xml import defaults
from vyos.template import render
-
+from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -35,178 +35,73 @@ config_file = "/etc/default/lldpd"
vyos_config_file = "/etc/lldpd.d/01-vyos.conf"
base = ['service', 'lldp']
-default_config_data = {
- "options": '',
- "interface_list": '',
- "location": ''
-}
-
-def get_options(config):
- options = {}
- config.set_level(base)
-
- options['listen_vlan'] = config.exists('listen-vlan')
- options['mgmt_addr'] = []
- for addr in config.return_values('management-address'):
- if is_addr_assigned(addr) and not is_loopback_addr(addr):
- options['mgmt_addr'].append(addr)
- else:
- message = 'WARNING: LLDP management address {0} invalid - '.format(addr)
- if is_loopback_addr(addr):
- message += '(loopback address).'
- else:
- message += 'address not found.'
- print(message)
-
- snmp = config.exists('snmp enable')
- options["snmp"] = snmp
- if snmp:
- config.set_level('')
- options["sys_snmp"] = config.exists('service snmp')
- config.set_level(base)
-
- config.set_level(base + ['legacy-protocols'])
- options['cdp'] = config.exists('cdp')
- options['edp'] = config.exists('edp')
- options['fdp'] = config.exists('fdp')
- options['sonmp'] = config.exists('sonmp')
-
- # start with an unknown version information
- version_data = get_version_data()
- options['description'] = version_data['version']
- options['listen_on'] = []
-
- return options
-
-def get_interface_list(config):
- config.set_level(base)
- intfs_names = config.list_nodes(['interface'])
- if len(intfs_names) < 0:
- return 0
-
- interface_list = []
- for name in intfs_names:
- config.set_level(base + ['interface', name])
- disable = config.exists(['disable'])
- intf = {
- 'name': name,
- 'disable': disable
- }
- interface_list.append(intf)
- return interface_list
-
-
-def get_location_intf(config, name):
- path = base + ['interface', name]
- config.set_level(path)
-
- config.set_level(path + ['location'])
- elin = ''
- coordinate_based = {}
-
- if config.exists('elin'):
- elin = config.return_value('elin')
-
- if config.exists('coordinate-based'):
- config.set_level(path + ['location', 'coordinate-based'])
-
- coordinate_based['latitude'] = config.return_value(['latitude'])
- coordinate_based['longitude'] = config.return_value(['longitude'])
-
- coordinate_based['altitude'] = '0'
- if config.exists(['altitude']):
- coordinate_based['altitude'] = config.return_value(['altitude'])
-
- coordinate_based['datum'] = 'WGS84'
- if config.exists(['datum']):
- coordinate_based['datum'] = config.return_value(['datum'])
-
- intf = {
- 'name': name,
- 'elin': elin,
- 'coordinate_based': coordinate_based
-
- }
- return intf
-
-
-def get_location(config):
- config.set_level(base)
- intfs_names = config.list_nodes(['interface'])
- if len(intfs_names) < 0:
- return 0
-
- if config.exists('disable'):
- return 0
-
- intfs_location = []
- for name in intfs_names:
- intf = get_location_intf(config, name)
- intfs_location.append(intf)
-
- return intfs_location
-
-
def get_config(config=None):
- lldp = deepcopy(default_config_data)
if config:
conf = config
else:
conf = Config()
+
if not conf.exists(base):
- return None
- else:
- lldp['options'] = get_options(conf)
- lldp['interface_list'] = get_interface_list(conf)
- lldp['location'] = get_location(conf)
+ return {}
- return lldp
+ lldp = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+ if conf.exists(['service', 'snmp']):
+ lldp['system_snmp_enabled'] = ''
+
+ version_data = get_version_data()
+ lldp['version'] = version_data['version']
+
+ # We have gathered the dict representation of the CLI, but there are default
+ # options which we need to update into the dictionary retrived.
+ # location coordinates have a default value
+ if 'interface' in lldp:
+ for interface, interface_config in lldp['interface'].items():
+ default_values = defaults(base + ['interface'])
+ if dict_search('location.coordinate_based', interface_config) == None:
+ # no location specified - no need to add defaults
+ del default_values['location']['coordinate_based']['datum']
+ del default_values['location']['coordinate_based']['altitude']
+
+ # cleanup default_values dictionary from inner to outer
+ # this might feel overkill here, but it does support easy extension
+ # in the future with additional default values
+ if len(default_values['location']['coordinate_based']) == 0:
+ del default_values['location']['coordinate_based']
+ if len(default_values['location']) == 0:
+ del default_values['location']
+
+ lldp['interface'][interface] = dict_merge(default_values,
+ lldp['interface'][interface])
+
+ return lldp
def verify(lldp):
# bail out early - looks like removal from running config
if lldp is None:
return
- # check location
- for location in lldp['location']:
- # check coordinate-based
- if len(location['coordinate_based']) > 0:
- # check longitude and latitude
- if not location['coordinate_based']['longitude']:
- raise ConfigError('Must define longitude for interface {0}'.format(location['name']))
-
- if not location['coordinate_based']['latitude']:
- raise ConfigError('Must define latitude for interface {0}'.format(location['name']))
-
- if not re.match(r'^(\d+)(\.\d+)?[nNsS]$', location['coordinate_based']['latitude']):
- raise ConfigError('Invalid location for interface {0}:\n' \
- 'latitude should be a number followed by S or N'.format(location['name']))
-
- if not re.match(r'^(\d+)(\.\d+)?[eEwW]$', location['coordinate_based']['longitude']):
- raise ConfigError('Invalid location for interface {0}:\n' \
- 'longitude should be a number followed by E or W'.format(location['name']))
-
- # check altitude and datum if exist
- if location['coordinate_based']['altitude']:
- if not re.match(r'^[-+0-9\.]+$', location['coordinate_based']['altitude']):
- raise ConfigError('Invalid location for interface {0}:\n' \
- 'altitude should be a positive or negative number'.format(location['name']))
-
- if location['coordinate_based']['datum']:
- if not re.match(r'^(WGS84|NAD83|MLLW)$', location['coordinate_based']['datum']):
- raise ConfigError("Invalid location for interface {0}:\n' \
- 'datum should be WGS84, NAD83, or MLLW".format(location['name']))
-
- # check elin
- elif location['elin']:
- if not re.match(r'^[0-9]{10,25}$', location['elin']):
- raise ConfigError('Invalid location for interface {0}:\n' \
- 'ELIN number must be between 10-25 numbers'.format(location['name']))
+ if 'management_address' in lldp:
+ for address in lldp['management_address']:
+ message = f'WARNING: LLDP management address "{address}" is invalid'
+ if is_loopback_addr(address):
+ print(f'{message} - loopback address')
+ elif not is_addr_assigned(address):
+ print(f'{message} - not assigned to any interface')
+
+ if 'interface' in lldp:
+ for interface, interface_config in lldp['interface'].items():
+ # bail out early if no location info present in interface config
+ if 'location' not in interface_config:
+ continue
+ if 'coordinate_based' in interface_config['location']:
+ if not {'latitude', 'latitude'} <= set(interface_config['location']['coordinate_based']):
+ raise ConfigError(f'Must define both longitude and latitude for "{interface}" location!')
# check options
- if lldp['options']['snmp']:
- if not lldp['options']['sys_snmp']:
+ if 'snmp' in lldp and 'enable' in lldp['snmp']:
+ if 'system_snmp_enabled' not in lldp:
raise ConfigError('SNMP must be configured to enable LLDP SNMP')
@@ -215,29 +110,17 @@ def generate(lldp):
if lldp is None:
return
- # generate listen on interfaces
- for intf in lldp['interface_list']:
- tmp = ''
- # add exclamation mark if interface is disabled
- if intf['disable']:
- tmp = '!'
-
- tmp += intf['name']
- lldp['options']['listen_on'].append(tmp)
-
- # generate /etc/default/lldpd
render(config_file, 'lldp/lldpd.tmpl', lldp)
- # generate /etc/lldpd.d/01-vyos.conf
render(vyos_config_file, 'lldp/vyos.conf.tmpl', lldp)
-
def apply(lldp):
+ systemd_service = 'lldpd.service'
if lldp:
# start/restart lldp service
- call('systemctl restart lldpd.service')
+ call(f'systemctl restart {systemd_service}')
else:
# LLDP service has been terminated
- call('systemctl stop lldpd.service')
+ call(f'systemctl stop {systemd_service}')
if os.path.isfile(config_file):
os.unlink(config_file)
if os.path.isfile(vyos_config_file):
diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py
index 96f8f6fb6..9f319fc8a 100755
--- a/src/conf_mode/nat.py
+++ b/src/conf_mode/nat.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-2021 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -28,6 +28,7 @@ from vyos.configdict import dict_merge
from vyos.template import render
from vyos.template import is_ip_network
from vyos.util import cmd
+from vyos.util import run
from vyos.util import check_kmod
from vyos.util import dict_search
from vyos.validate import is_addr_assigned
@@ -179,12 +180,19 @@ def verify(nat):
return None
def generate(nat):
- render(nftables_nat_config, 'firewall/nftables-nat.tmpl', nat,
- permission=0o755)
+ render(nftables_nat_config, 'firewall/nftables-nat.tmpl', nat)
+
+ # dry-run newly generated configuration
+ tmp = run(f'nft -c -f {nftables_nat_config}')
+ if tmp > 0:
+ if os.path.exists(nftables_ct_file):
+ os.unlink(nftables_ct_file)
+ raise ConfigError('Configuration file errors encountered!')
+
return None
def apply(nat):
- cmd(f'{nftables_nat_config}')
+ cmd(f'nft -f {nftables_nat_config}')
if os.path.isfile(nftables_nat_config):
os.unlink(nftables_nat_config)
diff --git a/src/conf_mode/policy-local-route.py b/src/conf_mode/policy-local-route.py
index 539189442..3f834f55c 100755
--- a/src/conf_mode/policy-local-route.py
+++ b/src/conf_mode/policy-local-route.py
@@ -18,6 +18,7 @@ import os
from sys import exit
+from netifaces import interfaces
from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.configdict import node_changed
@@ -35,35 +36,92 @@ def get_config(config=None):
conf = config
else:
conf = Config()
- base = ['policy', 'local-route']
+ base = ['policy']
+
pbr = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
- # delete policy local-route
- dict = {}
- tmp = node_changed(conf, ['policy', 'local-route', 'rule'], key_mangling=('-', '_'))
- if tmp:
- for rule in (tmp or []):
- src = leaf_node_changed(conf, ['policy', 'local-route', 'rule', rule, 'source'])
- fwmk = leaf_node_changed(conf, ['policy', 'local-route', 'rule', rule, 'fwmark'])
- if src:
- dict = dict_merge({'rule_remove' : {rule : {'source' : src}}}, dict)
- pbr.update(dict)
- if fwmk:
- dict = dict_merge({'rule_remove' : {rule : {'fwmark' : fwmk}}}, dict)
+ for route in ['local_route', 'local_route6']:
+ dict_id = 'rule_remove' if route == 'local_route' else 'rule6_remove'
+ route_key = 'local-route' if route == 'local_route' else 'local-route6'
+ base_rule = base + [route_key, 'rule']
+
+ # delete policy local-route
+ dict = {}
+ tmp = node_changed(conf, base_rule, key_mangling=('-', '_'))
+ if tmp:
+ for rule in (tmp or []):
+ src = leaf_node_changed(conf, base_rule + [rule, 'source'])
+ fwmk = leaf_node_changed(conf, base_rule + [rule, 'fwmark'])
+ iif = leaf_node_changed(conf, base_rule + [rule, 'inbound-interface'])
+ dst = leaf_node_changed(conf, base_rule + [rule, 'destination'])
+ rule_def = {}
+ if src:
+ rule_def = dict_merge({'source' : src}, rule_def)
+ if fwmk:
+ rule_def = dict_merge({'fwmark' : fwmk}, rule_def)
+ if iif:
+ rule_def = dict_merge({'inbound_interface' : iif}, rule_def)
+ if dst:
+ rule_def = dict_merge({'destination' : dst}, rule_def)
+ dict = dict_merge({dict_id : {rule : rule_def}}, dict)
pbr.update(dict)
- # delete policy local-route rule x source x.x.x.x
- # delete policy local-route rule x fwmark x
- if 'rule' in pbr:
- for rule in pbr['rule']:
- src = leaf_node_changed(conf, ['policy', 'local-route', 'rule', rule, 'source'])
- fwmk = leaf_node_changed(conf, ['policy', 'local-route', 'rule', rule, 'fwmark'])
- if src:
- dict = dict_merge({'rule_remove' : {rule : {'source' : src}}}, dict)
- pbr.update(dict)
- if fwmk:
- dict = dict_merge({'rule_remove' : {rule : {'fwmark' : fwmk}}}, dict)
- pbr.update(dict)
+ if not route in pbr:
+ continue
+
+ # delete policy local-route rule x source x.x.x.x
+ # delete policy local-route rule x fwmark x
+ # delete policy local-route rule x destination x.x.x.x
+ if 'rule' in pbr[route]:
+ for rule, rule_config in pbr[route]['rule'].items():
+ src = leaf_node_changed(conf, base_rule + [rule, 'source'])
+ fwmk = leaf_node_changed(conf, base_rule + [rule, 'fwmark'])
+ iif = leaf_node_changed(conf, base_rule + [rule, 'inbound-interface'])
+ dst = leaf_node_changed(conf, base_rule + [rule, 'destination'])
+ # keep track of changes in configuration
+ # otherwise we might remove an existing node although nothing else has changed
+ changed = False
+
+ rule_def = {}
+ # src is None if there are no changes to src
+ if src is None:
+ # if src hasn't changed, include it in the removal selector
+ # if a new selector is added, we have to remove all previous rules without this selector
+ # to make sure we remove all previous rules with this source(s), it will be included
+ if 'source' in rule_config:
+ rule_def = dict_merge({'source': rule_config['source']}, rule_def)
+ else:
+ # if src is not None, it's previous content will be returned
+ # this can be an empty array if it's just being set, or the previous value
+ # either way, something has to be changed and we only want to remove previous values
+ changed = True
+ # set the old value for removal if it's not empty
+ if len(src) > 0:
+ rule_def = dict_merge({'source' : src}, rule_def)
+ if fwmk is None:
+ if 'fwmark' in rule_config:
+ rule_def = dict_merge({'fwmark': rule_config['fwmark']}, rule_def)
+ else:
+ changed = True
+ if len(fwmk) > 0:
+ rule_def = dict_merge({'fwmark' : fwmk}, rule_def)
+ if iif is None:
+ if 'inbound_interface' in rule_config:
+ rule_def = dict_merge({'inbound_interface': rule_config['inbound_interface']}, rule_def)
+ else:
+ changed = True
+ if len(iif) > 0:
+ rule_def = dict_merge({'inbound_interface' : iif}, rule_def)
+ if dst is None:
+ if 'destination' in rule_config:
+ rule_def = dict_merge({'destination': rule_config['destination']}, rule_def)
+ else:
+ changed = True
+ if len(dst) > 0:
+ rule_def = dict_merge({'destination' : dst}, rule_def)
+ if changed:
+ dict = dict_merge({dict_id : {rule : rule_def}}, dict)
+ pbr.update(dict)
return pbr
@@ -72,13 +130,25 @@ def verify(pbr):
if not pbr:
return None
- if 'rule' in pbr:
- for rule in pbr['rule']:
- if 'source' not in pbr['rule'][rule] and 'fwmark' not in pbr['rule'][rule]:
- raise ConfigError('Source address or fwmark is required!')
- else:
- if 'set' not in pbr['rule'][rule] or 'table' not in pbr['rule'][rule]['set']:
- raise ConfigError('Table set is required!')
+ for route in ['local_route', 'local_route6']:
+ if not route in pbr:
+ continue
+
+ pbr_route = pbr[route]
+ if 'rule' in pbr_route:
+ for rule in pbr_route['rule']:
+ if 'source' not in pbr_route['rule'][rule] \
+ and 'destination' not in pbr_route['rule'][rule] \
+ and 'fwmark' not in pbr_route['rule'][rule] \
+ and 'inbound_interface' not in pbr_route['rule'][rule]:
+ raise ConfigError('Source or destination address or fwmark or inbound-interface is required!')
+ else:
+ if 'set' not in pbr_route['rule'][rule] or 'table' not in pbr_route['rule'][rule]['set']:
+ raise ConfigError('Table set is required!')
+ if 'inbound_interface' in pbr_route['rule'][rule]:
+ interface = pbr_route['rule'][rule]['inbound_interface']
+ if interface not in interfaces():
+ raise ConfigError(f'Interface "{interface}" does not exist')
return None
@@ -93,36 +163,51 @@ def apply(pbr):
return None
# Delete old rule if needed
- if 'rule_remove' in pbr:
- for rule in pbr['rule_remove']:
- if 'source' in pbr['rule_remove'][rule]:
- for src in pbr['rule_remove'][rule]['source']:
- call(f'ip rule del prio {rule} from {src}')
- if 'fwmark' in pbr['rule_remove'][rule]:
- for fwmk in pbr['rule_remove'][rule]['fwmark']:
- call(f'ip rule del prio {rule} from all fwmark {fwmk}')
+ for rule_rm in ['rule_remove', 'rule6_remove']:
+ if rule_rm in pbr:
+ v6 = " -6" if rule_rm == 'rule6_remove' else ""
+ for rule, rule_config in pbr[rule_rm].items():
+ rule_config['source'] = rule_config['source'] if 'source' in rule_config else ['']
+ for src in rule_config['source']:
+ f_src = '' if src == '' else f' from {src} '
+ rule_config['destination'] = rule_config['destination'] if 'destination' in rule_config else ['']
+ for dst in rule_config['destination']:
+ f_dst = '' if dst == '' else f' to {dst} '
+ rule_config['fwmark'] = rule_config['fwmark'] if 'fwmark' in rule_config else ['']
+ for fwmk in rule_config['fwmark']:
+ f_fwmk = '' if fwmk == '' else f' fwmark {fwmk} '
+ rule_config['inbound_interface'] = rule_config['inbound_interface'] if 'inbound_interface' in rule_config else ['']
+ for iif in rule_config['inbound_interface']:
+ f_iif = '' if iif == '' else f' iif {iif} '
+ call(f'ip{v6} rule del prio {rule} {f_src}{f_dst}{f_fwmk}{f_iif}')
# Generate new config
- if 'rule' in pbr:
- for rule in pbr['rule']:
- table = pbr['rule'][rule]['set']['table']
- # Only source in the rule
- # set policy local-route rule 100 source '203.0.113.1'
- if 'source' in pbr['rule'][rule] and not 'fwmark' in pbr['rule'][rule]:
- for src in pbr['rule'][rule]['source']:
- call(f'ip rule add prio {rule} from {src} lookup {table}')
- # Only fwmark in the rule
- # set policy local-route rule 101 fwmark '23'
- if 'fwmark' in pbr['rule'][rule] and not 'source' in pbr['rule'][rule]:
- fwmk = pbr['rule'][rule]['fwmark']
- call(f'ip rule add prio {rule} from all fwmark {fwmk} lookup {table}')
- # Source and fwmark in the rule
- # set policy local-route rule 100 source '203.0.113.1'
- # set policy local-route rule 100 fwmark '23'
- if 'source' in pbr['rule'][rule] and 'fwmark' in pbr['rule'][rule]:
- fwmk = pbr['rule'][rule]['fwmark']
- for src in pbr['rule'][rule]['source']:
- call(f'ip rule add prio {rule} from {src} fwmark {fwmk} lookup {table}')
+ for route in ['local_route', 'local_route6']:
+ if not route in pbr:
+ continue
+
+ v6 = " -6" if route == 'local_route6' else ""
+
+ pbr_route = pbr[route]
+ if 'rule' in pbr_route:
+ for rule, rule_config in pbr_route['rule'].items():
+ table = rule_config['set']['table']
+
+ rule_config['source'] = rule_config['source'] if 'source' in rule_config else ['all']
+ for src in rule_config['source'] or ['all']:
+ f_src = '' if src == '' else f' from {src} '
+ rule_config['destination'] = rule_config['destination'] if 'destination' in rule_config else ['all']
+ for dst in rule_config['destination']:
+ f_dst = '' if dst == '' else f' to {dst} '
+ f_fwmk = ''
+ if 'fwmark' in rule_config:
+ fwmk = rule_config['fwmark']
+ f_fwmk = f' fwmark {fwmk} '
+ f_iif = ''
+ if 'inbound_interface' in rule_config:
+ iif = rule_config['inbound_interface']
+ f_iif = f' iif {iif} '
+ call(f'ip{v6} rule add prio {rule} {f_src}{f_dst}{f_fwmk}{f_iif} lookup {table}')
return None
diff --git a/src/conf_mode/policy-route-interface.py b/src/conf_mode/policy-route-interface.py
index e81135a74..1108aebe6 100755
--- a/src/conf_mode/policy-route-interface.py
+++ b/src/conf_mode/policy-route-interface.py
@@ -52,7 +52,7 @@ def verify(if_policy):
if not if_policy:
return None
- for route in ['route', 'ipv6_route']:
+ for route in ['route', 'route6']:
if route in if_policy:
if route not in if_policy['policy']:
raise ConfigError('Policy route not configured')
@@ -71,7 +71,7 @@ def cleanup_rule(table, chain, ifname, new_name=None):
results = cmd(f'nft -a list chain {table} {chain}').split("\n")
retval = None
for line in results:
- if f'oifname "{ifname}"' in line:
+ if f'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
@@ -98,8 +98,8 @@ def apply(if_policy):
else:
cleanup_rule('ip mangle', route_chain, ifname)
- if 'ipv6_route' in if_policy:
- name = 'VYOS_PBR6_' + if_policy['ipv6_route']
+ if 'route6' in if_policy:
+ name = 'VYOS_PBR6_' + if_policy['route6']
rule_exists = cleanup_rule('ip6 mangle', ipv6_route_chain, ifname, name)
if not rule_exists:
diff --git a/src/conf_mode/policy-route.py b/src/conf_mode/policy-route.py
index d098be68d..3d1d7d8c5 100755
--- a/src/conf_mode/policy-route.py
+++ b/src/conf_mode/policy-route.py
@@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
+import re
from json import loads
from sys import exit
@@ -31,6 +32,35 @@ airbag.enable()
mark_offset = 0x7FFFFFFF
nftables_conf = '/run/nftables_policy.conf'
+preserve_chains = [
+ 'VYOS_PBR_PREROUTING',
+ 'VYOS_PBR_POSTROUTING',
+ 'VYOS_PBR6_PREROUTING',
+ 'VYOS_PBR6_POSTROUTING'
+]
+
+valid_groups = [
+ 'address_group',
+ 'network_group',
+ 'port_group'
+]
+
+def get_policy_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 'policy' in if_conf:
+ output[prefix + ifname] = if_conf['policy']
+ 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_config(config=None):
if config:
conf = config
@@ -38,68 +68,149 @@ def get_config(config=None):
conf = Config()
base = ['policy']
- if not conf.exists(base + ['route']) and not conf.exists(base + ['ipv6-route']):
- return None
-
policy = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True,
no_tag_node_value_mangle=True)
+ policy['firewall_group'] = conf.get_config_dict(['firewall', 'group'], key_mangling=('-', '_'), get_first_key=True,
+ no_tag_node_value_mangle=True)
+ policy['interfaces'] = get_policy_interfaces(conf)
+
return policy
-def verify(policy):
- # bail out early - looks like removal from running config
- if not policy:
- return None
+def verify_rule(policy, name, rule_conf, ipv6):
+ icmp = 'icmp' if not ipv6 else 'icmpv6'
+ if icmp in rule_conf:
+ icmp_defined = False
+ if 'type_name' in rule_conf[icmp]:
+ icmp_defined = True
+ if 'code' in rule_conf[icmp] or 'type' in rule_conf[icmp]:
+ raise ConfigError(f'{name} rule {rule_id}: Cannot use ICMP type/code with ICMP type-name')
+ if 'code' in rule_conf[icmp]:
+ icmp_defined = True
+ if 'type' not in rule_conf[icmp]:
+ raise ConfigError(f'{name} rule {rule_id}: ICMP code can only be defined if ICMP type is defined')
+ if 'type' in rule_conf[icmp]:
+ icmp_defined = True
+
+ if icmp_defined and 'protocol' not in rule_conf or rule_conf['protocol'] != icmp:
+ raise ConfigError(f'{name} rule {rule_id}: ICMP type/code or type-name can only be defined if protocol is ICMP')
+
+ if 'set' in rule_conf:
+ if 'tcp_mss' in rule_conf['set']:
+ tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags')
+ if not tcp_flags or 'syn' not in tcp_flags:
+ raise ConfigError(f'{name} rule {rule_id}: TCP SYN flag must be set to modify TCP-MSS')
+
+ tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags')
+ if tcp_flags:
+ if dict_search_args(rule_conf, 'protocol') != 'tcp':
+ raise ConfigError('Protocol must be tcp when specifying tcp flags')
+
+ not_flags = dict_search_args(rule_conf, 'tcp', 'flags', 'not')
+ if not_flags:
+ duplicates = [flag for flag in tcp_flags if flag in not_flags]
+ if duplicates:
+ raise ConfigError(f'Cannot match a tcp flag as set and not set')
+
+ for side in ['destination', 'source']:
+ if side in rule_conf:
+ side_conf = rule_conf[side]
+
+ 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')
+
+ for group in valid_groups:
+ if group in side_conf['group']:
+ group_name = side_conf['group'][group]
+
+ if group_name.startswith('!'):
+ group_name = group_name[1:]
+
+ fw_group = f'ipv6_{group}' if ipv6 and group in ['address_group', 'network_group'] else group
+ error_group = fw_group.replace("_", "-")
+ group_obj = dict_search_args(policy['firewall_group'], fw_group, group_name)
- for route in ['route', 'ipv6_route']:
+ if group_obj is None:
+ raise ConfigError(f'Invalid {error_group} "{group_name}" on policy route rule')
+
+ if not group_obj:
+ print(f'WARNING: {error_group} "{group_name}" has no members')
+
+ if 'port' in side_conf or dict_search_args(side_conf, 'group', 'port_group'):
+ if 'protocol' not in rule_conf:
+ raise ConfigError('Protocol must be defined if specifying a port or port-group')
+
+ 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(policy):
+ for route in ['route', 'route6']:
+ ipv6 = route == 'route6'
if route in policy:
for name, pol_conf in policy[route].items():
if 'rule' in pol_conf:
- for rule_id, rule_conf in pol_conf.items():
- icmp = 'icmp' if route == 'route' else 'icmpv6'
- if icmp in rule_conf:
- icmp_defined = False
- if 'type_name' in rule_conf[icmp]:
- icmp_defined = True
- if 'code' in rule_conf[icmp] or 'type' in rule_conf[icmp]:
- raise ConfigError(f'{name} rule {rule_id}: Cannot use ICMP type/code with ICMP type-name')
- if 'code' in rule_conf[icmp]:
- icmp_defined = True
- if 'type' not in rule_conf[icmp]:
- raise ConfigError(f'{name} rule {rule_id}: ICMP code can only be defined if ICMP type is defined')
- if 'type' in rule_conf[icmp]:
- icmp_defined = True
-
- if icmp_defined and 'protocol' not in rule_conf or rule_conf['protocol'] != icmp:
- raise ConfigError(f'{name} rule {rule_id}: ICMP type/code or type-name can only be defined if protocol is ICMP')
- if 'set' in rule_conf:
- if 'tcp_mss' in rule_conf['set']:
- tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags')
- if not tcp_flags or 'SYN' not in tcp_flags.split(","):
- raise ConfigError(f'{name} rule {rule_id}: TCP SYN flag must be set to modify TCP-MSS')
- if 'tcp' in rule_conf:
- if 'flags' in rule_conf['tcp']:
- if 'protocol' not in rule_conf or rule_conf['protocol'] != 'tcp':
- raise ConfigError(f'{name} rule {rule_id}: TCP flags can only be set if protocol is set to TCP')
+ for rule_id, rule_conf in pol_conf['rule'].items():
+ verify_rule(policy, name, rule_conf, ipv6)
+
+ for ifname, if_policy in policy['interfaces'].items():
+ name = dict_search_args(if_policy, 'route')
+ ipv6_name = dict_search_args(if_policy, 'route6')
+ if name and not dict_search_args(policy, 'route', name):
+ raise ConfigError(f'Policy route "{name}" is still referenced on interface {ifname}')
+
+ if ipv6_name and not dict_search_args(policy, 'route6', ipv6_name):
+ raise ConfigError(f'Policy route6 "{ipv6_name}" is still referenced on interface {ifname}')
return None
-def generate(policy):
- if not policy:
- if os.path.exists(nftables_conf):
- os.unlink(nftables_conf)
- 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 = []
+ for table in ['ip mangle', 'ip6 mangle']:
+ json_str = cmd(f'nft -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"):
+ continue
+ 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
+
+def generate(policy):
if not os.path.exists(nftables_conf):
policy['first_install'] = True
+ else:
+ policy['cleanup_commands'] = cleanup_commands(policy)
render(nftables_conf, 'firewall/nftables-policy.tmpl', policy)
return None
def apply_table_marks(policy):
- for route in ['route', 'ipv6_route']:
+ for route in ['route', 'route6']:
if route in policy:
+ cmd_str = 'ip' if route == 'route' else 'ip -6'
+ tables = []
for name, pol_conf in policy[route].items():
if 'rule' in pol_conf:
for rule_id, rule_conf in pol_conf['rule'].items():
@@ -107,31 +218,27 @@ def apply_table_marks(policy):
if set_table:
if set_table == 'main':
set_table = '254'
+ if set_table in tables:
+ continue
+ tables.append(set_table)
table_mark = mark_offset - int(set_table)
- cmd(f'ip rule add fwmark {table_mark} table {set_table}')
+ cmd(f'{cmd_str} rule add pref {set_table} fwmark {table_mark} table {set_table}')
def cleanup_table_marks():
- json_rules = cmd('ip -j -N rule list')
- rules = loads(json_rules)
- for rule in rules:
- if 'fwmark' not in rule or 'table' not in rule:
- continue
- fwmark = rule['fwmark']
- table = int(rule['table'])
- if fwmark[:2] == '0x':
- fwmark = int(fwmark, 16)
- if (int(fwmark) == (mark_offset - table)):
- cmd(f'ip rule del fwmark {fwmark} table {table}')
+ for cmd_str in ['ip', 'ip -6']:
+ json_rules = cmd(f'{cmd_str} -j -N rule list')
+ rules = loads(json_rules)
+ for rule in rules:
+ if 'fwmark' not in rule or 'table' not in rule:
+ continue
+ fwmark = rule['fwmark']
+ table = int(rule['table'])
+ if fwmark[:2] == '0x':
+ fwmark = int(fwmark, 16)
+ if (int(fwmark) == (mark_offset - table)):
+ cmd(f'{cmd_str} rule del fwmark {fwmark} table {table}')
def apply(policy):
- if not policy or 'first_install' not in policy:
- run(f'nft flush table ip mangle')
- run(f'nft flush table ip6 mangle')
-
- if not policy:
- cleanup_table_marks()
- return None
-
install_result = run(f'nft -f {nftables_conf}')
if install_result == 1:
raise ConfigError('Failed to apply policy based routing')
diff --git a/src/conf_mode/policy.py b/src/conf_mode/policy.py
index e251396c7..9d8fcfa36 100755
--- a/src/conf_mode/policy.py
+++ b/src/conf_mode/policy.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
@@ -87,6 +87,7 @@ def verify(policy):
# human readable instance name (hypen instead of underscore)
policy_hr = policy_type.replace('_', '-')
+ entries = []
for rule, rule_config in instance_config['rule'].items():
mandatory_error = f'must be specified for "{policy_hr} {instance} rule {rule}"!'
if 'action' not in rule_config:
@@ -113,6 +114,10 @@ def verify(policy):
if 'prefix' not in rule_config:
raise ConfigError(f'A prefix {mandatory_error}')
+ if rule_config in entries:
+ raise ConfigError(f'Rule "{rule}" contains a duplicate prefix definition!')
+ entries.append(rule_config)
+
# route-maps tend to be a bit more complex so they get their own verify() section
if 'route_map' in policy:
diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py
index d8704727c..dace53d37 100755
--- a/src/conf_mode/protocols_bgp.py
+++ b/src/conf_mode/protocols_bgp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-2021 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -159,13 +159,21 @@ def verify(bgp):
# Only checks for ipv4 and ipv6 neighbors
# Check if neighbor address is assigned as system interface address
- if is_ip(peer) and is_addr_assigned(peer):
- raise ConfigError(f'Can not configure a local address as neighbor "{peer}"')
+ vrf = None
+ vrf_error_msg = f' in default VRF!'
+ if 'vrf' in bgp:
+ vrf = bgp['vrf']
+ vrf_error_msg = f' in VRF "{vrf}"!'
+
+ if is_ip(peer) and is_addr_assigned(peer, vrf):
+ raise ConfigError(f'Can not configure local address as neighbor "{peer}"{vrf_error_msg}')
elif is_interface(peer):
if 'peer_group' in peer_config:
raise ConfigError(f'peer-group must be set under the interface node of "{peer}"')
if 'remote_as' in peer_config:
raise ConfigError(f'remote-as must be set under the interface node of "{peer}"')
+ if 'source_interface' in peer_config['interface']:
+ raise ConfigError(f'"source-interface" option not allowed for neighbor "{peer}"')
for afi in ['ipv4_unicast', 'ipv4_multicast', 'ipv4_labeled_unicast', 'ipv4_flowspec',
'ipv6_unicast', 'ipv6_multicast', 'ipv6_labeled_unicast', 'ipv6_flowspec',
@@ -205,6 +213,11 @@ def verify(bgp):
if 'non_exist_map' in afi_config['conditionally_advertise']:
verify_route_map(afi_config['conditionally_advertise']['non_exist_map'], bgp)
+ # T4332: bgp deterministic-med cannot be disabled while addpath-tx-bestpath-per-AS is in use
+ if 'addpath_tx_per_as' in afi_config:
+ if dict_search('parameters.deterministic_med', bgp) == None:
+ raise ConfigError('addpath-tx-per-as requires BGP deterministic-med paramtere to be set!')
+
# Validate if configured Prefix list exists
if 'prefix_list' in afi_config:
for tmp in ['import', 'export']:
diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py
index 9b4b215de..f2501e38a 100755
--- a/src/conf_mode/protocols_isis.py
+++ b/src/conf_mode/protocols_isis.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-2021 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -169,28 +169,40 @@ def verify(isis):
# Segment routing checks
if dict_search('segment_routing.global_block', isis):
- high_label_value = dict_search('segment_routing.global_block.high_label_value', isis)
- low_label_value = dict_search('segment_routing.global_block.low_label_value', isis)
+ g_high_label_value = dict_search('segment_routing.global_block.high_label_value', isis)
+ g_low_label_value = dict_search('segment_routing.global_block.low_label_value', isis)
- # If segment routing global block high value is blank, throw error
- if (low_label_value and not high_label_value) or (high_label_value and not low_label_value):
- raise ConfigError('Segment routing global block requires both low and high value!')
+ # If segment routing global block high or low value is blank, throw error
+ if not (g_low_label_value or g_high_label_value):
+ raise ConfigError('Segment routing global-block requires both low and high value!')
# If segment routing global block low value is higher than the high value, throw error
- if int(low_label_value) > int(high_label_value):
- raise ConfigError('Segment routing global block low value must be lower than high value')
+ if int(g_low_label_value) > int(g_high_label_value):
+ raise ConfigError('Segment routing global-block low value must be lower than high value')
if dict_search('segment_routing.local_block', isis):
- high_label_value = dict_search('segment_routing.local_block.high_label_value', isis)
- low_label_value = dict_search('segment_routing.local_block.low_label_value', isis)
+ if dict_search('segment_routing.global_block', isis) == None:
+ raise ConfigError('Segment routing local-block requires global-block to be configured!')
- # If segment routing local block high value is blank, throw error
- if (low_label_value and not high_label_value) or (high_label_value and not low_label_value):
- raise ConfigError('Segment routing local block requires both high and low value!')
+ l_high_label_value = dict_search('segment_routing.local_block.high_label_value', isis)
+ l_low_label_value = dict_search('segment_routing.local_block.low_label_value', isis)
- # If segment routing local block low value is higher than the high value, throw error
- if int(low_label_value) > int(high_label_value):
- raise ConfigError('Segment routing local block low value must be lower than high value')
+ # If segment routing local-block high or low value is blank, throw error
+ if not (l_low_label_value or l_high_label_value):
+ raise ConfigError('Segment routing local-block requires both high and low value!')
+
+ # If segment routing local-block low value is higher than the high value, throw error
+ if int(l_low_label_value) > int(l_high_label_value):
+ raise ConfigError('Segment routing local-block low value must be lower than high value')
+
+ # local-block most live outside global block
+ global_range = range(int(g_low_label_value), int(g_high_label_value) +1)
+ local_range = range(int(l_low_label_value), int(l_high_label_value) +1)
+
+ # Check for overlapping ranges
+ if list(set(global_range) & set(local_range)):
+ raise ConfigError(f'Segment-Routing Global Block ({g_low_label_value}/{g_high_label_value}) '\
+ f'conflicts with Local Block ({l_low_label_value}/{l_high_label_value})!')
return None
diff --git a/src/conf_mode/protocols_mpls.py b/src/conf_mode/protocols_mpls.py
index 0b0c7d07b..933e23065 100755
--- a/src/conf_mode/protocols_mpls.py
+++ b/src/conf_mode/protocols_mpls.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -20,11 +20,10 @@ from sys import exit
from glob import glob
from vyos.config import Config
-from vyos.configdict import node_changed
from vyos.template import render_to_string
-from vyos.util import call
from vyos.util import dict_search
from vyos.util import read_file
+from vyos.util import sysctl_write
from vyos import ConfigError
from vyos import frr
from vyos import airbag
@@ -89,21 +88,21 @@ def apply(mpls):
labels = '0'
if 'interface' in mpls:
labels = '1048575'
- call(f'sysctl -wq net.mpls.platform_labels={labels}')
+ sysctl_write('net.mpls.platform_labels', labels)
# Check for changes in global MPLS options
if 'parameters' in mpls:
# Choose whether to copy IP TTL to MPLS header TTL
if 'no_propagate_ttl' in mpls['parameters']:
- call('sysctl -wq net.mpls.ip_ttl_propagate=0')
+ sysctl_write('net.mpls.ip_ttl_propagate', 0)
# Choose whether to limit maximum MPLS header TTL
if 'maximum_ttl' in mpls['parameters']:
ttl = mpls['parameters']['maximum_ttl']
- call(f'sysctl -wq net.mpls.default_ttl={ttl}')
+ sysctl_write('net.mpls.default_ttl', ttl)
else:
# Set default global MPLS options if not defined.
- call('sysctl -wq net.mpls.ip_ttl_propagate=1')
- call('sysctl -wq net.mpls.default_ttl=255')
+ sysctl_write('net.mpls.ip_ttl_propagate', 1)
+ sysctl_write('net.mpls.default_ttl', 255)
# Enable and disable MPLS processing on interfaces per configuration
if 'interface' in mpls:
@@ -117,11 +116,11 @@ def apply(mpls):
if '1' in interface_state:
if system_interface not in mpls['interface']:
system_interface = system_interface.replace('.', '/')
- call(f'sysctl -wq net.mpls.conf.{system_interface}.input=0')
+ sysctl_write(f'net.mpls.conf.{system_interface}.input', 0)
elif '0' in interface_state:
if system_interface in mpls['interface']:
system_interface = system_interface.replace('.', '/')
- call(f'sysctl -wq net.mpls.conf.{system_interface}.input=1')
+ sysctl_write(f'net.mpls.conf.{system_interface}.input', 1)
else:
system_interfaces = []
# If MPLS interfaces are not configured, set MPLS processing disabled
@@ -129,7 +128,7 @@ def apply(mpls):
system_interfaces.append(os.path.basename(interface))
for system_interface in system_interfaces:
system_interface = system_interface.replace('.', '/')
- call(f'sysctl -wq net.mpls.conf.{system_interface}.input=0')
+ sysctl_write(f'net.mpls.conf.{system_interface}.input', 0)
return None
diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py
index 4895cde6f..26d491838 100755
--- a/src/conf_mode/protocols_ospf.py
+++ b/src/conf_mode/protocols_ospf.py
@@ -25,6 +25,7 @@ from vyos.configdict import node_changed
from vyos.configverify import verify_common_route_maps
from vyos.configverify import verify_route_map
from vyos.configverify import verify_interface_exists
+from vyos.configverify import verify_access_list
from vyos.template import render_to_string
from vyos.util import dict_search
from vyos.util import get_interface_config
@@ -159,6 +160,16 @@ def verify(ospf):
route_map_name = dict_search('default_information.originate.route_map', ospf)
if route_map_name: verify_route_map(route_map_name, ospf)
+ # Validate if configured Access-list exists
+ if 'area' in ospf:
+ for area, area_config in ospf['area'].items():
+ if 'import_list' in area_config:
+ acl_import = area_config['import_list']
+ if acl_import: verify_access_list(acl_import, ospf)
+ if 'export_list' in area_config:
+ acl_export = area_config['export_list']
+ if acl_export: verify_access_list(acl_export, ospf)
+
if 'interface' in ospf:
for interface, interface_config in ospf['interface'].items():
verify_interface_exists(interface)
diff --git a/src/conf_mode/protocols_static.py b/src/conf_mode/protocols_static.py
index c1e427b16..f0ec48de4 100755
--- a/src/conf_mode/protocols_static.py
+++ b/src/conf_mode/protocols_static.py
@@ -82,6 +82,10 @@ def verify(static):
for interface, interface_config in prefix_options[type].items():
verify_vrf(interface_config)
+ if {'blackhole', 'reject'} <= set(prefix_options):
+ raise ConfigError(f'Can not use both blackhole and reject for '\
+ 'prefix "{prefix}"!')
+
return None
def generate(static):
diff --git a/src/conf_mode/qos.py b/src/conf_mode/qos.py
new file mode 100755
index 000000000..dbe3be225
--- /dev/null
+++ b/src/conf_mode/qos.py
@@ -0,0 +1,87 @@
+#!/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 vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.xml import defaults
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['qos']
+ if not conf.exists(base):
+ return None
+
+ qos = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+
+ if 'policy' in qos:
+ for policy in qos['policy']:
+ # CLI mangles - to _ for better Jinja2 compatibility - do we need
+ # Jinja2 here?
+ policy = policy.replace('-','_')
+
+ default_values = defaults(base + ['policy', policy])
+
+ # class is another tag node which requires individual handling
+ class_default_values = defaults(base + ['policy', policy, 'class'])
+ if 'class' in default_values:
+ del default_values['class']
+
+ for p_name, p_config in qos['policy'][policy].items():
+ qos['policy'][policy][p_name] = dict_merge(
+ default_values, qos['policy'][policy][p_name])
+
+ if 'class' in p_config:
+ for p_class in p_config['class']:
+ qos['policy'][policy][p_name]['class'][p_class] = dict_merge(
+ class_default_values, qos['policy'][policy][p_name]['class'][p_class])
+
+ import pprint
+ pprint.pprint(qos)
+ return qos
+
+def verify(qos):
+ if not qos:
+ return None
+
+ # network policy emulator
+ # reorder rerquires delay to be set
+
+ raise ConfigError('123')
+ return None
+
+def generate(qos):
+ return None
+
+def apply(qos):
+ 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/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py
index f676fdbbe..2ebee8018 100755
--- a/src/conf_mode/service_ipoe-server.py
+++ b/src/conf_mode/service_ipoe-server.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
@@ -41,6 +41,7 @@ default_config_data = {
'interfaces': [],
'dnsv4': [],
'dnsv6': [],
+ 'client_named_ip_pool': [],
'client_ipv6_pool': [],
'client_ipv6_delegate_prefix': [],
'radius_server': [],
@@ -219,6 +220,22 @@ def get_config(config=None):
conf.set_level(base_path)
+ # Named client-ip-pool
+ if conf.exists(['client-ip-pool', 'name']):
+ for name in conf.list_nodes(['client-ip-pool', 'name']):
+ tmp = {
+ 'name': name,
+ 'gateway_address': '',
+ 'subnet': ''
+ }
+
+ if conf.exists(['client-ip-pool', 'name', name, 'gateway-address']):
+ tmp['gateway_address'] += conf.return_value(['client-ip-pool', 'name', name, 'gateway-address'])
+ if conf.exists(['client-ip-pool', 'name', name, 'subnet']):
+ tmp['subnet'] += conf.return_value(['client-ip-pool', 'name', name, 'subnet'])
+
+ ipoe['client_named_ip_pool'].append(tmp)
+
if conf.exists(['client-ipv6-pool', 'prefix']):
for prefix in conf.list_nodes(['client-ipv6-pool', 'prefix']):
tmp = {
@@ -254,10 +271,6 @@ def verify(ipoe):
if not ipoe['interfaces']:
raise ConfigError('No IPoE interface configured')
- for interface in ipoe['interfaces']:
- if not interface['range']:
- raise ConfigError(f'No IPoE client subnet defined on interface "{ interface }"')
-
if len(ipoe['dnsv4']) > 2:
raise ConfigError('Not more then two IPv4 DNS name-servers can be configured')
diff --git a/src/conf_mode/service_monitoring_telegraf.py b/src/conf_mode/service_monitoring_telegraf.py
index a1e7a7286..8a972b9fe 100755
--- a/src/conf_mode/service_monitoring_telegraf.py
+++ b/src/conf_mode/service_monitoring_telegraf.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
@@ -22,6 +22,7 @@ from shutil import rmtree
from vyos.config import Config
from vyos.configdict import dict_merge
+from vyos.ifconfig import Section
from vyos.template import render
from vyos.util import call
from vyos.util import chown
@@ -42,6 +43,24 @@ systemd_telegraf_override_dir = '/etc/systemd/system/vyos-telegraf.service.d'
systemd_override = f'{systemd_telegraf_override_dir}/10-override.conf'
+def get_interfaces(type='', vlan=True):
+ """
+ Get interfaces
+ get_interfaces()
+ ['dum0', 'eth0', 'eth1', 'eth1.5', 'lo', 'tun0']
+
+ get_interfaces("dummy")
+ ['dum0']
+ """
+ interfaces = []
+ ifaces = Section.interfaces(type)
+ for iface in ifaces:
+ if vlan == False and '.' in iface:
+ continue
+ interfaces.append(iface)
+
+ return interfaces
+
def get_nft_filter_chains():
"""
Get nft chains for table filter
@@ -57,6 +76,7 @@ def get_nft_filter_chains():
return chain_list
+
def get_config(config=None):
if config:
@@ -75,8 +95,9 @@ def get_config(config=None):
default_values = defaults(base)
monitoring = dict_merge(default_values, monitoring)
- monitoring['nft_chains'] = get_nft_filter_chains()
monitoring['custom_scripts_dir'] = custom_scripts_dir
+ monitoring['interfaces_ethernet'] = get_interfaces('ethernet', vlan=False)
+ monitoring['nft_chains'] = get_nft_filter_chains()
return monitoring
diff --git a/src/conf_mode/service_upnp.py b/src/conf_mode/service_upnp.py
new file mode 100755
index 000000000..d21b31990
--- /dev/null
+++ b/src/conf_mode/service_upnp.py
@@ -0,0 +1,157 @@
+#!/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 sys import exit
+import uuid
+import netifaces
+from ipaddress import IPv4Network
+from ipaddress import IPv6Network
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.configdict import get_interface_dict
+from vyos.configverify import verify_vrf
+from vyos.util import call
+from vyos.template import render
+from vyos.template import is_ipv4
+from vyos.template import is_ipv6
+from vyos.xml import defaults
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/run/upnp/miniupnp.conf'
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['service', 'upnp']
+ upnpd = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+
+ if not upnpd:
+ return None
+
+ if 'rule' in upnpd:
+ default_member_values = defaults(base + ['rule'])
+ for rule,rule_config in upnpd['rule'].items():
+ upnpd['rule'][rule] = dict_merge(default_member_values, upnpd['rule'][rule])
+
+ uuidgen = uuid.uuid1()
+ upnpd.update({'uuid': uuidgen})
+
+ return upnpd
+
+def get_all_interface_addr(prefix, filter_dev, filter_family):
+ list_addr = []
+ interfaces = netifaces.interfaces()
+
+ for interface in interfaces:
+ if filter_dev and interface in filter_dev:
+ continue
+ addrs = netifaces.ifaddresses(interface)
+ if netifaces.AF_INET in addrs.keys():
+ if netifaces.AF_INET in filter_family:
+ for addr in addrs[netifaces.AF_INET]:
+ if prefix:
+ # we need to manually assemble a list of IPv4 address/prefix
+ prefix = '/' + \
+ str(IPv4Network('0.0.0.0/' + addr['netmask']).prefixlen)
+ list_addr.append(addr['addr'] + prefix)
+ else:
+ list_addr.append(addr['addr'])
+ if netifaces.AF_INET6 in addrs.keys():
+ if netifaces.AF_INET6 in filter_family:
+ for addr in addrs[netifaces.AF_INET6]:
+ if prefix:
+ # we need to manually assemble a list of IPv4 address/prefix
+ bits = bin(int(addr['netmask'].replace(':', '').split('/')[0], 16)).count('1')
+ prefix = '/' + str(bits)
+ list_addr.append(addr['addr'] + prefix)
+ else:
+ list_addr.append(addr['addr'])
+
+ return list_addr
+
+def verify(upnpd):
+ if not upnpd:
+ return None
+
+ if 'wan_interface' not in upnpd:
+ raise ConfigError('To enable UPNP, you must have the "wan-interface" option!')
+
+ if 'rule' in upnpd:
+ for rule, rule_config in upnpd['rule'].items():
+ for option in ['external_port_range', 'internal_port_range', 'ip', 'action']:
+ if option not in rule_config:
+ tmp = option.replace('_', '-')
+ raise ConfigError(f'Every UPNP rule requires "{tmp}" to be set!')
+
+ if 'stun' in upnpd:
+ for option in ['host', 'port']:
+ if option not in upnpd['stun']:
+ raise ConfigError(f'A UPNP stun support must have an "{option}" option!')
+
+ # Check the validity of the IP address
+ listen_dev = []
+ system_addrs_cidr = get_all_interface_addr(True, [], [netifaces.AF_INET, netifaces.AF_INET6])
+ system_addrs = get_all_interface_addr(False, [], [netifaces.AF_INET, netifaces.AF_INET6])
+ for listen_if_or_addr in upnpd['listen']:
+ if listen_if_or_addr not in netifaces.interfaces():
+ listen_dev.append(listen_if_or_addr)
+ if (listen_if_or_addr not in system_addrs) and (listen_if_or_addr not in system_addrs_cidr) and (listen_if_or_addr not in netifaces.interfaces()):
+ if is_ipv4(listen_if_or_addr) and IPv4Network(listen_if_or_addr).is_multicast:
+ raise ConfigError(f'The address "{listen_if_or_addr}" is an address that is not allowed to listen on. It is not an interface address nor a multicast address!')
+ if is_ipv6(listen_if_or_addr) and IPv6Network(listen_if_or_addr).is_multicast:
+ raise ConfigError(f'The address "{listen_if_or_addr}" is an address that is not allowed to listen on. It is not an interface address nor a multicast address!')
+
+ system_listening_dev_addrs_cidr = get_all_interface_addr(True, listen_dev, [netifaces.AF_INET6])
+ system_listening_dev_addrs = get_all_interface_addr(False, listen_dev, [netifaces.AF_INET6])
+ for listen_if_or_addr in upnpd['listen']:
+ if listen_if_or_addr not in netifaces.interfaces() and (listen_if_or_addr not in system_listening_dev_addrs_cidr) and (listen_if_or_addr not in system_listening_dev_addrs) and is_ipv6(listen_if_or_addr) and (not IPv6Network(listen_if_or_addr).is_multicast):
+ raise ConfigError(f'{listen_if_or_addr} must listen on the interface of the network card')
+
+def generate(upnpd):
+ if not upnpd:
+ return None
+
+ if os.path.isfile(config_file):
+ os.unlink(config_file)
+
+ render(config_file, 'firewall/upnpd.conf.tmpl', upnpd)
+
+def apply(upnpd):
+ systemd_service_name = 'miniupnpd.service'
+ if not upnpd:
+ # Stop the UPNP service
+ call(f'systemctl stop {systemd_service_name}')
+ else:
+ # Start the UPNP service
+ call(f'systemctl restart {systemd_service_name}')
+
+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/system-ip.py b/src/conf_mode/system-ip.py
index 32cb2f036..05fc3a97a 100755
--- a/src/conf_mode/system-ip.py
+++ b/src/conf_mode/system-ip.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
@@ -20,14 +20,13 @@ from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.util import call
from vyos.util import dict_search
+from vyos.util import sysctl_write
+from vyos.util import write_file
from vyos.xml import defaults
from vyos import ConfigError
from vyos import airbag
airbag.enable()
-def sysctl(name, value):
- call(f'sysctl -wq {name}={value}')
-
def get_config(config=None):
if config:
conf = config
@@ -50,29 +49,29 @@ def generate(opt):
pass
def apply(opt):
+ # Apply ARP threshold values
+ # table_size has a default value - thus the key always exists
size = int(dict_search('arp.table_size', opt))
- if size:
- # apply ARP threshold values
- sysctl('net.ipv4.neigh.default.gc_thresh3', str(size))
- sysctl('net.ipv4.neigh.default.gc_thresh2', str(size // 2))
- sysctl('net.ipv4.neigh.default.gc_thresh1', str(size // 8))
+ # Amount upon reaching which the records begin to be cleared immediately
+ sysctl_write('net.ipv4.neigh.default.gc_thresh3', size)
+ # Amount after which the records begin to be cleaned after 5 seconds
+ sysctl_write('net.ipv4.neigh.default.gc_thresh2', size // 2)
+ # Minimum number of stored records is indicated which is not cleared
+ sysctl_write('net.ipv4.neigh.default.gc_thresh1', size // 8)
# enable/disable IPv4 forwarding
- tmp = '1'
- if 'disable_forwarding' in opt:
- tmp = '0'
- sysctl('net.ipv4.conf.all.forwarding', tmp)
+ tmp = dict_search('disable_forwarding', opt)
+ value = '0' if (tmp != None) else '1'
+ write_file('/proc/sys/net/ipv4/conf/all/forwarding', value)
- tmp = '0'
- # configure multipath - dict_search() returns an empty dict if key was found
- if isinstance(dict_search('multipath.ignore_unreachable_nexthops', opt), dict):
- tmp = '1'
- sysctl('net.ipv4.fib_multipath_use_neigh', tmp)
+ # configure multipath
+ tmp = dict_search('multipath.ignore_unreachable_nexthops', opt)
+ value = '1' if (tmp != None) else '0'
+ sysctl_write('net.ipv4.fib_multipath_use_neigh', value)
- tmp = '0'
- if isinstance(dict_search('multipath.layer4_hashing', opt), dict):
- tmp = '1'
- sysctl('net.ipv4.fib_multipath_hash_policy', tmp)
+ tmp = dict_search('multipath.layer4_hashing', opt)
+ value = '1' if (tmp != None) else '0'
+ sysctl_write('net.ipv4.fib_multipath_hash_policy', value)
if __name__ == '__main__':
try:
diff --git a/src/conf_mode/system-ipv6.py b/src/conf_mode/system-ipv6.py
index f70ec2631..26aacf46b 100755
--- a/src/conf_mode/system-ipv6.py
+++ b/src/conf_mode/system-ipv6.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019 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,95 +15,68 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
-import sys
from sys import exit
-from copy import deepcopy
from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.util import dict_search
+from vyos.util import sysctl_write
+from vyos.util import write_file
+from vyos.xml import defaults
from vyos import ConfigError
-from vyos.util import call
-
from vyos import airbag
airbag.enable()
-ipv6_disable_file = '/etc/modprobe.d/vyos_disable_ipv6.conf'
-
-default_config_data = {
- 'reboot_message': False,
- 'ipv6_forward': '1',
- 'disable_addr_assignment': False,
- 'mp_layer4_hashing': '0',
- 'neighbor_cache': 8192,
- 'strict_dad': '1'
-
-}
-
-def sysctl(name, value):
- call('sysctl -wq {}={}'.format(name, value))
-
def get_config(config=None):
- ip_opt = deepcopy(default_config_data)
if config:
conf = config
else:
conf = Config()
- conf.set_level('system ipv6')
- if conf.exists(''):
- ip_opt['disable_addr_assignment'] = conf.exists('disable')
- if conf.exists_effective('disable') != conf.exists('disable'):
- ip_opt['reboot_message'] = True
-
- if conf.exists('disable-forwarding'):
- ip_opt['ipv6_forward'] = '0'
+ base = ['system', 'ipv6']
- if conf.exists('multipath layer4-hashing'):
- ip_opt['mp_layer4_hashing'] = '1'
+ opt = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
- if conf.exists('neighbor table-size'):
- ip_opt['neighbor_cache'] = int(conf.return_value('neighbor table-size'))
+ # 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)
+ opt = dict_merge(default_values, opt)
- if conf.exists('strict-dad'):
- ip_opt['strict_dad'] = 2
+ return opt
- return ip_opt
-
-def verify(ip_opt):
+def verify(opt):
pass
-def generate(ip_opt):
+def generate(opt):
pass
-def apply(ip_opt):
- # disable IPv6 address assignment
- if ip_opt['disable_addr_assignment']:
- with open(ipv6_disable_file, 'w') as f:
- f.write('options ipv6 disable_ipv6=1')
- else:
- if os.path.exists(ipv6_disable_file):
- os.unlink(ipv6_disable_file)
-
- if ip_opt['reboot_message']:
- print('Changing IPv6 disable parameter will only take affect\n' \
- 'when the system is rebooted.')
-
+def apply(opt):
# configure multipath
- sysctl('net.ipv6.fib_multipath_hash_policy', ip_opt['mp_layer4_hashing'])
-
- # apply neighbor table threshold values
- sysctl('net.ipv6.neigh.default.gc_thresh3', ip_opt['neighbor_cache'])
- sysctl('net.ipv6.neigh.default.gc_thresh2', ip_opt['neighbor_cache'] // 2)
- sysctl('net.ipv6.neigh.default.gc_thresh1', ip_opt['neighbor_cache'] // 8)
+ tmp = dict_search('multipath.layer4_hashing', opt)
+ value = '1' if (tmp != None) else '0'
+ sysctl_write('net.ipv6.fib_multipath_hash_policy', value)
+
+ # Apply ND threshold values
+ # table_size has a default value - thus the key always exists
+ size = int(dict_search('neighbor.table_size', opt))
+ # Amount upon reaching which the records begin to be cleared immediately
+ sysctl_write('net.ipv6.neigh.default.gc_thresh3', size)
+ # Amount after which the records begin to be cleaned after 5 seconds
+ sysctl_write('net.ipv6.neigh.default.gc_thresh2', size // 2)
+ # Minimum number of stored records is indicated which is not cleared
+ sysctl_write('net.ipv6.neigh.default.gc_thresh1', size // 8)
# enable/disable IPv6 forwarding
- with open('/proc/sys/net/ipv6/conf/all/forwarding', 'w') as f:
- f.write(ip_opt['ipv6_forward'])
+ tmp = dict_search('disable_forwarding', opt)
+ value = '0' if (tmp != None) else '1'
+ write_file('/proc/sys/net/ipv6/conf/all/forwarding', value)
# configure IPv6 strict-dad
+ tmp = dict_search('strict_dad', opt)
+ value = '2' if (tmp != None) else '1'
for root, dirs, files in os.walk('/proc/sys/net/ipv6/conf'):
for name in files:
- if name == "accept_dad":
- with open(os.path.join(root, name), 'w') as f:
- f.write(str(ip_opt['strict_dad']))
+ if name == 'accept_dad':
+ write_file(os.path.join(root, name), value)
if __name__ == '__main__':
try:
diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py
index 4dd7f936d..c9c6aa187 100755
--- a/src/conf_mode/system-login.py
+++ b/src/conf_mode/system-login.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-2021 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -23,6 +23,7 @@ from pwd import getpwall
from pwd import getpwnam
from spwd import getspnam
from sys import exit
+from time import sleep
from vyos.config import Config
from vyos.configdict import dict_merge
@@ -31,6 +32,7 @@ from vyos.template import render
from vyos.template import is_ipv4
from vyos.util import cmd
from vyos.util import call
+from vyos.util import run
from vyos.util import DEVNULL
from vyos.util import dict_search
from vyos.xml import defaults
@@ -250,13 +252,22 @@ def apply(login):
if 'rm_users' in login:
for user in login['rm_users']:
try:
+ # Disable user to prevent re-login
+ call(f'usermod -s /sbin/nologin {user}')
+
# Logout user if he is still logged in
if user in list(set([tmp[0] for tmp in users()])):
print(f'{user} is logged in, forcing logout!')
- call(f'pkill -HUP -u {user}')
-
- # Remove user account but leave home directory to be safe
- call(f'userdel --remove {user}', stderr=DEVNULL)
+ # re-run command until user is logged out
+ while run(f'pkill -HUP -u {user}'):
+ sleep(0.250)
+
+ # Remove user account but leave home directory in place. Re-run
+ # command until user is removed - userdel might return 8 as
+ # SSH sessions are not all yet properly cleaned away, thus we
+ # simply re-run the command until the account wen't away
+ while run(f'userdel --remove {user}', stderr=DEVNULL):
+ sleep(0.250)
except Exception as e:
raise ConfigError(f'Deleting user "{user}" raised exception: {e}')
diff --git a/src/conf_mode/system-syslog.py b/src/conf_mode/system-syslog.py
index 3d8a51cd8..309b4bdb0 100755
--- a/src/conf_mode/system-syslog.py
+++ b/src/conf_mode/system-syslog.py
@@ -17,6 +17,7 @@
import os
import re
+from pathlib import Path
from sys import exit
from vyos.config import Config
@@ -89,7 +90,7 @@ def get_config(config=None):
filename: {
'log-file': '/var/log/user/' + filename,
'max-files': '5',
- 'action-on-max-size': '/usr/sbin/logrotate /etc/logrotate.d/' + filename,
+ 'action-on-max-size': '/usr/sbin/logrotate /etc/logrotate.d/vyos-rsyslog-generated-' + filename,
'selectors': '*.err',
'max-size': 262144
}
@@ -205,10 +206,17 @@ def generate(c):
conf = '/etc/rsyslog.d/vyos-rsyslog.conf'
render(conf, 'syslog/rsyslog.conf.tmpl', c)
+ # cleanup current logrotate config files
+ logrotate_files = Path('/etc/logrotate.d/').glob('vyos-rsyslog-generated-*')
+ for file in logrotate_files:
+ file.unlink()
+
# eventually write for each file its own logrotate file, since size is
# defined it shouldn't matter
- conf = '/etc/logrotate.d/vyos-rsyslog'
- render(conf, 'syslog/logrotate.tmpl', c)
+ for filename, fileconfig in c.get('files', {}).items():
+ if fileconfig['log-file'].startswith('/var/log/user/'):
+ conf = '/etc/logrotate.d/vyos-rsyslog-generated-' + filename
+ render(conf, 'syslog/logrotate.tmpl', { 'config_render': fileconfig })
def verify(c):
diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py
index 38c0c4463..f79c8a21e 100755
--- a/src/conf_mode/vrf.py
+++ b/src/conf_mode/vrf.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-2021 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -29,6 +29,7 @@ from vyos.util import dict_search
from vyos.util import get_interface_config
from vyos.util import popen
from vyos.util import run
+from vyos.util import sysctl_write
from vyos import ConfigError
from vyos import frr
from vyos import airbag
@@ -37,10 +38,16 @@ airbag.enable()
config_file = '/etc/iproute2/rt_tables.d/vyos-vrf.conf'
nft_vrf_config = '/tmp/nftables-vrf-zones'
-def list_rules():
- command = 'ip -j -4 rule show'
- answer = loads(cmd(command))
- return [_ for _ in answer if _]
+def has_rule(af : str, priority : int, table : str):
+ """ Check if a given ip rule exists """
+ if af not in ['-4', '-6']:
+ raise ValueError()
+ command = f'ip -j {af} rule show'
+ for tmp in loads(cmd(command)):
+ if {'priority', 'table'} <= set(tmp):
+ if tmp['priority'] == priority and tmp['table'] == table:
+ return True
+ return False
def vrf_interfaces(c, match):
matched = []
@@ -69,7 +76,6 @@ def vrf_routing(c, match):
c.set_level(old_level)
return matched
-
def get_config(config=None):
if config:
conf = config
@@ -148,13 +154,11 @@ def apply(vrf):
bind_all = '0'
if 'bind-to-all' in vrf:
bind_all = '1'
- call(f'sysctl -wq net.ipv4.tcp_l3mdev_accept={bind_all}')
- call(f'sysctl -wq net.ipv4.udp_l3mdev_accept={bind_all}')
+ sysctl_write('net.ipv4.tcp_l3mdev_accept', bind_all)
+ sysctl_write('net.ipv4.udp_l3mdev_accept', bind_all)
for tmp in (dict_search('vrf_remove', vrf) or []):
if os.path.isdir(f'/sys/class/net/{tmp}'):
- call(f'ip -4 route del vrf {tmp} unreachable default metric 4278198272')
- call(f'ip -6 route del vrf {tmp} unreachable default metric 4278198272')
call(f'ip link delete dev {tmp}')
# Remove nftables conntrack zone map item
nft_del_element = f'delete element inet vrf_zones ct_iface_map {{ "{tmp}" }}'
@@ -165,31 +169,59 @@ def apply(vrf):
# check if table already exists
_, err = popen('nft list table inet vrf_zones')
# If not, create a table
- if err:
- if os.path.exists(nft_vrf_config):
- cmd(f'nft -f {nft_vrf_config}')
- os.unlink(nft_vrf_config)
+ if err and os.path.exists(nft_vrf_config):
+ cmd(f'nft -f {nft_vrf_config}')
+ os.unlink(nft_vrf_config)
+
+ # Linux routing uses rules to find tables - routing targets are then
+ # looked up in those tables. If the lookup got a matching route, the
+ # process ends.
+ #
+ # TL;DR; first table with a matching entry wins!
+ #
+ # You can see your routing table lookup rules using "ip rule", sadly the
+ # local lookup is hit before any VRF lookup. Pinging an addresses from the
+ # VRF will usually find a hit in the local table, and never reach the VRF
+ # routing table - this is usually not what you want. Thus we will
+ # re-arrange the tables and move the local lookup further down once VRFs
+ # are enabled.
+ #
+ # Thanks to https://stbuehler.de/blog/article/2020/02/29/using_vrf__virtual_routing_and_forwarding__on_linux.html
+
+ for afi in ['-4', '-6']:
+ # move lookup local to pref 32765 (from 0)
+ if not has_rule(afi, 32765, 'local'):
+ call(f'ip {afi} rule add pref 32765 table local')
+ if has_rule(afi, 0, 'local'):
+ call(f'ip {afi} rule del pref 0')
+ # make sure that in VRFs after failed lookup in the VRF specific table
+ # nothing else is reached
+ if not has_rule(afi, 1000, 'l3mdev'):
+ # this should be added by the kernel when a VRF is created
+ # add it here for completeness
+ call(f'ip {afi} rule add pref 1000 l3mdev protocol kernel')
+
+ # add another rule with an unreachable target which only triggers in VRF context
+ # if a route could not be reached
+ if not has_rule(afi, 2000, 'l3mdev'):
+ call(f'ip {afi} rule add pref 2000 l3mdev unreachable')
for name, config in vrf['name'].items():
table = config['table']
-
if not os.path.isdir(f'/sys/class/net/{name}'):
# For each VRF apart from your default context create a VRF
# interface with a separate routing table
call(f'ip link add {name} type vrf table {table}')
- # The kernel Documentation/networking/vrf.txt also recommends
- # adding unreachable routes to the VRF routing tables so that routes
- # afterwards are taken.
- call(f'ip -4 route add vrf {name} unreachable default metric 4278198272')
- call(f'ip -6 route add vrf {name} unreachable default metric 4278198272')
- # We also should add proper loopback IP addresses to the newly
- # created VRFs for services bound to the loopback address (SNMP, NTP)
- call(f'ip -4 addr add 127.0.0.1/8 dev {name}')
- call(f'ip -6 addr add ::1/128 dev {name}')
# set VRF description for e.g. SNMP monitoring
vrf_if = Interface(name)
+ # We also should add proper loopback IP addresses to the newly added
+ # VRF for services bound to the loopback address (SNMP, NTP)
+ vrf_if.add_addr('127.0.0.1/8')
+ vrf_if.add_addr('::1/128')
+ # add VRF description if available
vrf_if.set_alias(config.get('description', ''))
+
# Enable/Disable of an interface must always be done at the end of the
# derived class to make use of the ref-counting set_admin_state()
# function. We will only enable the interface if 'up' was called as
@@ -203,37 +235,9 @@ def apply(vrf):
nft_add_element = f'add element inet vrf_zones ct_iface_map {{ "{name}" : {table} }}'
cmd(f'nft {nft_add_element}')
- # Linux routing uses rules to find tables - routing targets are then
- # looked up in those tables. If the lookup got a matching route, the
- # process ends.
- #
- # TL;DR; first table with a matching entry wins!
- #
- # You can see your routing table lookup rules using "ip rule", sadly the
- # local lookup is hit before any VRF lookup. Pinging an addresses from the
- # VRF will usually find a hit in the local table, and never reach the VRF
- # routing table - this is usually not what you want. Thus we will
- # re-arrange the tables and move the local lookup furhter down once VRFs
- # are enabled.
-
- # get current preference on local table
- local_pref = [r.get('priority') for r in list_rules() if r.get('table') == 'local'][0]
-
- # change preference when VRFs are enabled and local lookup table is default
- if not local_pref and 'name' in vrf:
- for af in ['-4', '-6']:
- call(f'ip {af} rule add pref 32765 table local')
- call(f'ip {af} rule del pref 0')
# return to default lookup preference when no VRF is configured
if 'name' not in vrf:
- for af in ['-4', '-6']:
- call(f'ip {af} rule add pref 0 table local')
- call(f'ip {af} rule del pref 32765')
-
- # clean out l3mdev-table rule if present
- if 1000 in [r.get('priority') for r in list_rules() if r.get('priority') == 1000]:
- call(f'ip {af} rule del pref 1000')
# Remove VRF zones table from nftables
tmp = run('nft list table inet vrf_zones')
if tmp == 0:
diff --git a/src/conf_mode/zone_policy.py b/src/conf_mode/zone_policy.py
index 683f8f034..dc0617353 100755
--- a/src/conf_mode/zone_policy.py
+++ b/src/conf_mode/zone_policy.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
@@ -20,10 +20,12 @@ 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()
@@ -36,12 +38,22 @@ def get_config(config=None):
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 = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
- if zone_policy:
- zone_policy['firewall'] = conf.get_config_dict(['firewall'], 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