summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rwxr-xr-xsrc/conf_mode/conntrack_sync.py6
-rwxr-xr-xsrc/conf_mode/containers.py22
-rwxr-xr-xsrc/conf_mode/firewall-interface.py11
-rwxr-xr-xsrc/conf_mode/firewall.py40
-rwxr-xr-xsrc/conf_mode/flow_accounting_conf.py14
-rwxr-xr-xsrc/conf_mode/http-api.py9
-rwxr-xr-xsrc/conf_mode/interfaces-bonding.py10
-rwxr-xr-xsrc/conf_mode/interfaces-bridge.py5
-rwxr-xr-xsrc/conf_mode/interfaces-dummy.py2
-rwxr-xr-xsrc/conf_mode/interfaces-ethernet.py22
-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.py34
-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.py2
-rwxr-xr-xsrc/conf_mode/lldp.py235
-rwxr-xr-xsrc/conf_mode/policy-local-route.py36
-rwxr-xr-xsrc/conf_mode/policy-route.py8
-rwxr-xr-xsrc/conf_mode/protocols_bgp.py12
-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.py90
-rwxr-xr-xsrc/conf_mode/service_upnp.py44
-rwxr-xr-xsrc/conf_mode/system-ip.py43
-rwxr-xr-xsrc/conf_mode/system-ipv6.py103
-rwxr-xr-xsrc/conf_mode/system-syslog.py14
-rwxr-xr-xsrc/conf_mode/vrf.py108
-rwxr-xr-xsrc/conf_mode/zone_policy.py24
-rw-r--r--src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper16
-rw-r--r--src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup2
-rw-r--r--src/etc/logrotate.d/conntrackd9
-rw-r--r--src/etc/logrotate.d/vyos-rsyslog12
-rw-r--r--src/etc/systemd/system/uacctd.service.d/override.conf14
-rwxr-xr-xsrc/etc/telegraf/custom_scripts/show_firewall_input_filter.py73
-rwxr-xr-xsrc/helpers/system-versions-foot.py2
-rwxr-xr-xsrc/helpers/vyos-vrrp-conntracksync.sh8
-rwxr-xr-xsrc/migration-scripts/bgp/0-to-12
-rwxr-xr-xsrc/migration-scripts/firewall/6-to-720
-rwxr-xr-xsrc/migration-scripts/ipsec/8-to-948
-rwxr-xr-xsrc/migration-scripts/ssh/1-to-250
-rwxr-xr-xsrc/op_mode/cpu_summary.py36
-rwxr-xr-xsrc/op_mode/firewall.py3
-rwxr-xr-xsrc/op_mode/generate_ovpn_client_file.py145
-rwxr-xr-xsrc/op_mode/generate_public_key_command.py11
-rwxr-xr-xsrc/op_mode/lldp_op.py2
-rwxr-xr-xsrc/op_mode/powerctrl.py25
-rwxr-xr-xsrc/op_mode/show_cpu.py63
-rwxr-xr-xsrc/op_mode/show_ipsec_sa.py186
-rwxr-xr-xsrc/op_mode/show_ram.py19
-rwxr-xr-xsrc/op_mode/show_uptime.py27
-rwxr-xr-xsrc/op_mode/show_version.py22
-rwxr-xr-xsrc/services/vyos-http-api-server4
-rwxr-xr-xsrc/system/keepalived-fifo.py3
-rwxr-xr-xsrc/validators/ipv6-range31
63 files changed, 1267 insertions, 661 deletions
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 26c50cab6..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,7 +316,7 @@ 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:
run(f'{container_base_cmd} --net host {image}')
else:
diff --git a/src/conf_mode/firewall-interface.py b/src/conf_mode/firewall-interface.py
index a7442ecbd..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,13 +141,13 @@ 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'
@@ -155,7 +158,7 @@ def apply(if_firewall):
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 358b938e3..41df1b84a 100755
--- a/src/conf_mode/firewall.py
+++ b/src/conf_mode/firewall.py
@@ -54,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',
@@ -70,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',
@@ -201,6 +207,10 @@ def verify_rule(firewall, rule_conf, ipv6):
for group in valid_groups:
if group in side_conf['group']:
group_name = side_conf['group'][group]
+
+ if group_name and group_name[0] == '!':
+ 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(firewall, 'group', fw_group, group_name)
@@ -241,31 +251,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 not dict_search_args(firewall, fw_name, name):
+ 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 = []
- 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]}')
+ 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}')
@@ -281,9 +294,9 @@ 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)
@@ -296,7 +309,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):
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..661dc2298 100755
--- a/src/conf_mode/interfaces-bonding.py
+++ b/src/conf_mode/interfaces-bonding.py
@@ -27,8 +27,10 @@ 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
from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_redirect
+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 +134,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 +151,8 @@ def verify(bond):
verify_address(bond)
verify_dhcpv6(bond)
verify_vrf(bond)
+ verify_mirror(bond)
+ verify_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..e16c0e9f4 100755
--- a/src/conf_mode/interfaces-bridge.py
+++ b/src/conf_mode/interfaces-bridge.py
@@ -22,12 +22,13 @@ 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
+from vyos.configverify import verify_redirect
from vyos.configverify import verify_vrf
from vyos.ifconfig import BridgeIf
from vyos.validate import has_address_configured
@@ -106,6 +107,8 @@ def verify(bridge):
verify_dhcpv6(bridge)
verify_vrf(bridge)
+ verify_mirror(bridge)
+ verify_redirect(bridge)
ifname = bridge['ifname']
diff --git a/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py
index 55c783f38..4072c4452 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_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_redirect(dummy)
return None
diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py
index e7250fb49..3eeddf190 100755
--- a/src/conf_mode/interfaces-ethernet.py
+++ b/src/conf_mode/interfaces-ethernet.py
@@ -28,11 +28,14 @@ from vyos.configverify import verify_interface_exists
from vyos.configverify import verify_mirror
from vyos.configverify import verify_mtu
from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_redirect
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
@@ -82,6 +85,7 @@ def verify(ethernet):
verify_vrf(ethernet)
verify_eapol(ethernet)
verify_mirror(ethernet)
+ verify_redirect(ethernet)
ethtool = Ethtool(ifname)
# No need to check speed and duplex keys as both have default values.
@@ -159,16 +163,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..a94b5e1f7 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_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_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..5ea7159dc 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_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_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..e6a851113 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_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_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..6a29fdb11 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_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_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..8f9c0b3f1 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,6 +32,7 @@ from shutil import rmtree
from vyos.config import Config
from vyos.configdict import get_interface_dict
+from vyos.configdict import leaf_node_changed
from vyos.configverify import verify_vrf
from vyos.configverify import verify_bridge_delete
from vyos.ifconfig import VTunIf
@@ -47,6 +48,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 +89,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 +230,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 +274,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 +429,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':
@@ -647,9 +653,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..9962e0a08 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_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_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..f57e41cc4 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_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_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..005fae5eb 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_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_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..30e13536f 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_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_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..a29836efd 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_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_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..dc0fe7b9c 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_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_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..fdf9e3988 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_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_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..367a50e82 100755
--- a/src/conf_mode/interfaces-wwan.py
+++ b/src/conf_mode/interfaces-wwan.py
@@ -23,6 +23,7 @@ from vyos.config import Config
from vyos.configdict import get_interface_dict
from vyos.configverify import verify_authentication
from vyos.configverify import verify_interface_exists
+from vyos.configverify import verify_redirect
from vyos.configverify import verify_vrf
from vyos.ifconfig import WWANIf
from vyos.util import cmd
@@ -77,6 +78,7 @@ def verify(wwan):
verify_interface_exists(ifname)
verify_authentication(wwan)
verify_vrf(wwan)
+ verify_redirect(wwan)
return None
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/policy-local-route.py b/src/conf_mode/policy-local-route.py
index 71183c6ba..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
@@ -51,12 +52,15 @@ def get_config(config=None):
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)
@@ -72,6 +76,7 @@ def get_config(config=None):
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
@@ -100,6 +105,13 @@ def get_config(config=None):
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)
@@ -125,11 +137,18 @@ def verify(pbr):
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]:
- raise ConfigError('Source or destination address or fwmark is required!')
+ 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
@@ -143,8 +162,6 @@ def apply(pbr):
if not pbr:
return None
- print(pbr)
-
# Delete old rule if needed
for rule_rm in ['rule_remove', 'rule6_remove']:
if rule_rm in pbr:
@@ -159,7 +176,10 @@ def apply(pbr):
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} '
- call(f'ip{v6} rule del prio {rule} {f_src}{f_dst}{f_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
for route in ['local_route', 'local_route6']:
@@ -183,7 +203,11 @@ def apply(pbr):
if 'fwmark' in rule_config:
fwmk = rule_config['fwmark']
f_fwmk = f' fwmark {fwmk} '
- call(f'ip{v6} rule add prio {rule} {f_src}{f_dst}{f_fwmk} lookup {table}')
+ 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.py b/src/conf_mode/policy-route.py
index 7dcab4b58..3d1d7d8c5 100755
--- a/src/conf_mode/policy-route.py
+++ b/src/conf_mode/policy-route.py
@@ -123,6 +123,10 @@ def verify_rule(policy, name, rule_conf, ipv6):
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)
@@ -206,6 +210,7 @@ def apply_table_marks(policy):
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():
@@ -213,6 +218,9 @@ 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'{cmd_str} rule add pref {set_table} fwmark {table_mark} table {set_table}')
diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py
index d8704727c..64b113873 100755
--- a/src/conf_mode/protocols_bgp.py
+++ b/src/conf_mode/protocols_bgp.py
@@ -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',
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..cf447d4b5
--- /dev/null
+++ b/src/conf_mode/qos.py
@@ -0,0 +1,90 @@
+#!/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 = ['traffic-policy']
+ if not conf.exists(base):
+ return None
+
+ qos = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+
+ for traffic_policy in ['drop-tail', 'fair-queue', 'fq-codel', 'limiter',
+ 'network-emulator', 'priority-queue', 'random-detect',
+ 'rate-control', 'round-robin', 'shaper', 'shaper-hfsc']:
+ traffic_policy_us = traffic_policy.replace('-','_')
+ # Individual policy type not present on CLI - no need to blend in
+ # any default values
+ if traffic_policy_us not in qos:
+ continue
+
+ default_values = defaults(base + [traffic_policy_us])
+
+ # class is another tag node which requires individual handling
+ class_default_values = defaults(base + [traffic_policy_us, 'class'])
+ if 'class' in default_values:
+ del default_values['class']
+
+ for policy, policy_config in qos[traffic_policy_us].items():
+ qos[traffic_policy_us][policy] = dict_merge(
+ default_values, qos[traffic_policy_us][policy])
+
+ if 'class' in policy_config:
+ for policy_class in policy_config['class']:
+ qos[traffic_policy_us][policy]['class'][policy_class] = dict_merge(
+ class_default_values, qos[traffic_policy_us][policy]['class'][policy_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_upnp.py b/src/conf_mode/service_upnp.py
index 638296f45..d21b31990 100755
--- a/src/conf_mode/service_upnp.py
+++ b/src/conf_mode/service_upnp.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
@@ -24,7 +24,6 @@ from ipaddress import IPv6Network
from vyos.config import Config
from vyos.configdict import dict_merge
-from vyos.configdict import dict_search
from vyos.configdict import get_interface_dict
from vyos.configverify import verify_vrf
from vyos.util import call
@@ -43,17 +42,18 @@ def get_config(config=None):
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 dict_search('rule', upnpd):
+
+ 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})
@@ -62,7 +62,7 @@ def get_config(config=None):
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
@@ -87,27 +87,28 @@ def get_all_interface_addr(prefix, filter_dev, filter_family):
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 dict_search('rules', upnpd):
- for rule,rule_config in upnpd['rule'].items():
+
+ 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:
- raise ConfigError(f'A UPNP rule must have an "{option}" option!')
-
- if dict_search('stun', upnpd):
+ 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])
@@ -120,7 +121,7 @@ def verify(upnpd):
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']:
@@ -130,19 +131,20 @@ def verify(upnpd):
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('systemctl stop miniupnpd.service')
+ call(f'systemctl stop {systemd_service_name}')
else:
# Start the UPNP service
- call('systemctl restart miniupnpd.service')
+ call(f'systemctl restart {systemd_service_name}')
if __name__ == '__main__':
try:
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..7fb2dd1cf 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,82 @@
# 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 import ConfigError
+from vyos.configdict import dict_merge
+from vyos.configdict import leaf_node_changed
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()
-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'))
+ tmp = leaf_node_changed(conf, base + ['disable'])
+ if tmp: opt['reboot_required'] = {}
- if conf.exists('strict-dad'):
- ip_opt['strict_dad'] = 2
+ # 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)
- return ip_opt
+ return 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)
+def apply(opt):
+ # disable IPv6 globally
+ tmp = dict_search('disable', opt)
+ value = '1' if (tmp != None) else '0'
+ sysctl_write('net.ipv6.conf.all.disable_ipv6', value)
- if ip_opt['reboot_message']:
+ if 'reboot_required' in opt:
print('Changing IPv6 disable parameter will only take affect\n' \
'when the system is rebooted.')
# 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-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..6a521a0dd 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
+ # created VRFs for services bound to the loopback address (SNMP, NTP)
+ vrf_if.add_addr('127.0.0.1/8')
+ vrf_if.add_addr('::1/128')
+ # add VRF description if available
vrf_if.set_alias(config.get('description', ''))
+
# Enable/Disable of an interface must always be done at the end of the
# derived class to make use of the ref-counting set_admin_state()
# function. We will only enable the interface if 'up' was called as
@@ -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
diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper b/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper
index 9d5505758..74a7e83bf 100644
--- a/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper
+++ b/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper
@@ -4,7 +4,7 @@
IF_METRIC=${IF_METRIC:-210}
# Check if interface is inside a VRF
-VRF_OPTION=$(ip -j -d link show ${interface} | awk '{if(match($0, /.*"master":"(\w+)".*"info_slave_kind":"vrf"/, IFACE_DETAILS)) printf("vrf %s", IFACE_DETAILS[1])}')
+VRF_OPTION=$(/usr/sbin/ip -j -d link show ${interface} | awk '{if(match($0, /.*"master":"(\w+)".*"info_slave_kind":"vrf"/, IFACE_DETAILS)) printf("vrf %s", IFACE_DETAILS[1])}')
# get status of FRR
function frr_alive () {
@@ -66,9 +66,9 @@ function iptovtysh () {
# delete the same route from kernel before adding new one
function delroute () {
logmsg info "Checking if the route presented in kernel: $@ $VRF_OPTION"
- if ip route show $@ $VRF_OPTION | grep -qx "$1 " ; then
- logmsg info "Deleting IP route: \"ip route del $@ $VRF_OPTION\""
- ip route del $@ $VRF_OPTION
+ if /usr/sbin/ip route show $@ $VRF_OPTION | grep -qx "$1 " ; then
+ logmsg info "Deleting IP route: \"/usr/sbin/ip route del $@ $VRF_OPTION\""
+ /usr/sbin/ip route del $@ $VRF_OPTION
fi
}
@@ -76,8 +76,8 @@ function delroute () {
function ip () {
# pass comand to system `ip` if this is not related to routes change
if [ "$2" != "route" ] ; then
- logmsg info "Passing command to iproute2: \"$@\""
- ip $@
+ logmsg info "Passing command to /usr/sbin/ip: \"$@\""
+ /usr/sbin/ip $@
else
# if we want to work with routes, try to use FRR first
if frr_alive ; then
@@ -87,8 +87,8 @@ function ip () {
vtysh -c "conf t" -c "$VTYSH_CMD"
else
# add ip route to kernel
- logmsg info "Modifying routes in kernel: \"ip $@\""
- ip $@ $VRF_OPTION
+ logmsg info "Modifying routes in kernel: \"/usr/sbin/ip $@\""
+ /usr/sbin/ip $@ $VRF_OPTION
fi
fi
}
diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup
index a6989441b..ad6a1d5eb 100644
--- a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup
+++ b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup
@@ -1,7 +1,7 @@
##
## VyOS cleanup
##
-# NOTE: here we use 'ip' wrapper, therefore a route will be actually deleted via ip or vtysh, according to the system state
+# NOTE: here we use 'ip' wrapper, therefore a route will be actually deleted via /usr/sbin/ip or vtysh, according to the system state
hostsd_client="/usr/bin/vyos-hostsd-client"
hostsd_changes=
# check vyos-hostsd status
diff --git a/src/etc/logrotate.d/conntrackd b/src/etc/logrotate.d/conntrackd
new file mode 100644
index 000000000..b0b09dec1
--- /dev/null
+++ b/src/etc/logrotate.d/conntrackd
@@ -0,0 +1,9 @@
+/var/log/conntrackd-stats.log {
+ weekly
+ rotate 2
+ missingok
+
+ postrotate
+ systemctl restart conntrackd.service > /dev/null
+ endscript
+}
diff --git a/src/etc/logrotate.d/vyos-rsyslog b/src/etc/logrotate.d/vyos-rsyslog
new file mode 100644
index 000000000..3c087b94e
--- /dev/null
+++ b/src/etc/logrotate.d/vyos-rsyslog
@@ -0,0 +1,12 @@
+/var/log/messages {
+ create
+ missingok
+ nomail
+ notifempty
+ rotate 10
+ size 1M
+ postrotate
+ # inform rsyslog service about rotation
+ /usr/lib/rsyslog/rsyslog-rotate
+ endscript
+}
diff --git a/src/etc/systemd/system/uacctd.service.d/override.conf b/src/etc/systemd/system/uacctd.service.d/override.conf
deleted file mode 100644
index 38bcce515..000000000
--- a/src/etc/systemd/system/uacctd.service.d/override.conf
+++ /dev/null
@@ -1,14 +0,0 @@
-[Unit]
-After=
-After=vyos-router.service
-ConditionPathExists=
-ConditionPathExists=/run/pmacct/uacctd.conf
-
-[Service]
-EnvironmentFile=
-ExecStart=
-ExecStart=/usr/sbin/uacctd -f /run/pmacct/uacctd.conf
-WorkingDirectory=
-WorkingDirectory=/run/pmacct
-PIDFile=
-PIDFile=/run/pmacct/uacctd.pid
diff --git a/src/etc/telegraf/custom_scripts/show_firewall_input_filter.py b/src/etc/telegraf/custom_scripts/show_firewall_input_filter.py
new file mode 100755
index 000000000..bf4bfd05d
--- /dev/null
+++ b/src/etc/telegraf/custom_scripts/show_firewall_input_filter.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python3
+
+import json
+import re
+import time
+
+from vyos.util import cmd
+
+
+def get_nft_filter_chains():
+ """
+ Get list of nft chains for table filter
+ """
+ nft = cmd('/usr/sbin/nft --json list table ip filter')
+ nft = json.loads(nft)
+ chain_list = []
+
+ for output in nft['nftables']:
+ if 'chain' in output:
+ chain = output['chain']['name']
+ chain_list.append(chain)
+
+ return chain_list
+
+
+def get_nftables_details(name):
+ """
+ Get dict, counters packets and bytes for chain
+ """
+ command = f'/usr/sbin/nft list chain ip filter {name}'
+ try:
+ results = cmd(command)
+ except:
+ return {}
+
+ # Trick to remove 'NAME_' from chain name in the comment
+ # It was added to any chain T4218
+ # counter packets 0 bytes 0 return comment "FOO default-action accept"
+ comment_name = name.replace("NAME_", "")
+ out = {}
+ for line in results.split('\n'):
+ comment_search = re.search(rf'{comment_name}[\- ](\d+|default-action)', line)
+ if not comment_search:
+ continue
+
+ rule = {}
+ rule_id = comment_search[1]
+ counter_search = re.search(r'counter packets (\d+) bytes (\d+)', line)
+ if counter_search:
+ rule['packets'] = counter_search[1]
+ rule['bytes'] = counter_search[2]
+
+ rule['conditions'] = re.sub(r'(\b(counter packets \d+ bytes \d+|drop|reject|return|log)\b|comment "[\w\-]+")', '', line).strip()
+ out[rule_id] = rule
+ return out
+
+
+def get_nft_telegraf(name):
+ """
+ Get data for telegraf in influxDB format
+ """
+ for rule, rule_config in get_nftables_details(name).items():
+ print(f'nftables,table=filter,chain={name},'
+ f'ruleid={rule} '
+ f'pkts={rule_config["packets"]}i,'
+ f'bytes={rule_config["bytes"]}i '
+ f'{str(int(time.time()))}000000000')
+
+
+chains = get_nft_filter_chains()
+
+for chain in chains:
+ get_nft_telegraf(chain)
diff --git a/src/helpers/system-versions-foot.py b/src/helpers/system-versions-foot.py
index c33e41d79..2aa687221 100755
--- a/src/helpers/system-versions-foot.py
+++ b/src/helpers/system-versions-foot.py
@@ -21,7 +21,7 @@ import vyos.systemversions as systemversions
import vyos.defaults
import vyos.version
-sys_versions = systemversions.get_system_versions()
+sys_versions = systemversions.get_system_component_version()
component_string = formatversions.format_versions_string(sys_versions)
diff --git a/src/helpers/vyos-vrrp-conntracksync.sh b/src/helpers/vyos-vrrp-conntracksync.sh
index 4501aa63e..0cc718938 100755
--- a/src/helpers/vyos-vrrp-conntracksync.sh
+++ b/src/helpers/vyos-vrrp-conntracksync.sh
@@ -14,12 +14,10 @@
# Modified by : Mohit Mehta <mohit@vyatta.com>
# Slight modifications were made to this script for running with Vyatta
# The original script came from 0.9.14 debian conntrack-tools package
-#
-#
CONNTRACKD_BIN=/usr/sbin/conntrackd
CONNTRACKD_LOCK=/var/lock/conntrack.lock
-CONNTRACKD_CONFIG=/etc/conntrackd/conntrackd.conf
+CONNTRACKD_CONFIG=/run/conntrackd/conntrackd.conf
FACILITY=daemon
LEVEL=notice
TAG=conntrack-tools
@@ -29,6 +27,10 @@ FAILOVER_STATE="/var/run/vyatta-conntrackd-failover-state"
$LOGCMD "vyatta-vrrp-conntracksync invoked at `date`"
+if ! systemctl is-active --quiet conntrackd.service; then
+ echo "conntrackd service not running"
+ exit 1
+fi
if [ ! -e $FAILOVER_STATE ]; then
mkdir -p /var/run
diff --git a/src/migration-scripts/bgp/0-to-1 b/src/migration-scripts/bgp/0-to-1
index b1d5a6514..5e9dffe1f 100755
--- a/src/migration-scripts/bgp/0-to-1
+++ b/src/migration-scripts/bgp/0-to-1
@@ -33,7 +33,7 @@ with open(file_name, 'r') as f:
base = ['protocols', 'bgp']
config = ConfigTree(config_file)
-if not config.exists(base):
+if not config.exists(base) or not config.is_tag(base):
# Nothing to do
exit(0)
diff --git a/src/migration-scripts/firewall/6-to-7 b/src/migration-scripts/firewall/6-to-7
index efc901530..5f4cff90d 100755
--- a/src/migration-scripts/firewall/6-to-7
+++ b/src/migration-scripts/firewall/6-to-7
@@ -104,6 +104,7 @@ if config.exists(base + ['name']):
continue
for rule in config.list_nodes(base + ['name', name, 'rule']):
+ rule_recent = base + ['name', name, 'rule', rule, 'recent']
rule_time = base + ['name', name, 'rule', rule, 'time']
rule_tcp_flags = base + ['name', name, 'rule', rule, 'tcp', 'flags']
rule_icmp = base + ['name', name, 'rule', rule, 'icmp']
@@ -114,6 +115,15 @@ if config.exists(base + ['name']):
if config.exists(rule_time + ['utc']):
config.delete(rule_time + ['utc'])
+ if config.exists(rule_recent + ['time']):
+ tmp = int(config.return_value(rule_recent + ['time']))
+ unit = 'minute'
+ if tmp > 600:
+ unit = 'hour'
+ elif tmp < 10:
+ unit = 'second'
+ config.set(rule_recent + ['time'], value=unit)
+
if config.exists(rule_tcp_flags):
tmp = config.return_value(rule_tcp_flags)
config.delete(rule_tcp_flags)
@@ -148,6 +158,7 @@ if config.exists(base + ['ipv6-name']):
continue
for rule in config.list_nodes(base + ['ipv6-name', name, 'rule']):
+ rule_recent = base + ['ipv6-name', name, 'rule', rule, 'recent']
rule_time = base + ['ipv6-name', name, 'rule', rule, 'time']
rule_tcp_flags = base + ['ipv6-name', name, 'rule', rule, 'tcp', 'flags']
rule_icmp = base + ['ipv6-name', name, 'rule', rule, 'icmpv6']
@@ -158,6 +169,15 @@ if config.exists(base + ['ipv6-name']):
if config.exists(rule_time + ['utc']):
config.delete(rule_time + ['utc'])
+ if config.exists(rule_recent + ['time']):
+ tmp = int(config.return_value(rule_recent + ['time']))
+ unit = 'minute'
+ if tmp > 600:
+ unit = 'hour'
+ elif tmp < 10:
+ unit = 'second'
+ config.set(rule_recent + ['time'], value=unit)
+
if config.exists(rule_tcp_flags):
tmp = config.return_value(rule_tcp_flags)
config.delete(rule_tcp_flags)
diff --git a/src/migration-scripts/ipsec/8-to-9 b/src/migration-scripts/ipsec/8-to-9
new file mode 100755
index 000000000..eb44b6216
--- /dev/null
+++ b/src/migration-scripts/ipsec/8-to-9
@@ -0,0 +1,48 @@
+#!/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 argv
+from sys import exit
+
+from vyos.configtree import ConfigTree
+
+if (len(argv) < 1):
+ print("Must specify file name!")
+ exit(1)
+
+file_name = argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+base = ['vpn', 'ipsec', 'ike-group']
+config = ConfigTree(config_file)
+
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+else:
+ for ike_group in config.list_nodes(base):
+ base_closeaction = base + [ike_group, 'close-action']
+ if config.exists(base_closeaction) and config.return_value(base_closeaction) == 'clear':
+ config.set(base_closeaction, 'none', replace=True)
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print(f'Failed to save the modified config: {e}')
+ exit(1)
diff --git a/src/migration-scripts/ssh/1-to-2 b/src/migration-scripts/ssh/1-to-2
index bc8815753..31c40df16 100755
--- a/src/migration-scripts/ssh/1-to-2
+++ b/src/migration-scripts/ssh/1-to-2
@@ -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
@@ -30,26 +30,52 @@ file_name = argv[1]
with open(file_name, 'r') as f:
config_file = f.read()
-base = ['service', 'ssh', 'loglevel']
+base = ['service', 'ssh']
config = ConfigTree(config_file)
if not config.exists(base):
# Nothing to do
exit(0)
-else:
- # red in configured loglevel and convert it to lower case
- tmp = config.return_value(base).lower()
+path_loglevel = base + ['loglevel']
+if config.exists(path_loglevel):
+ # red in configured loglevel and convert it to lower case
+ tmp = config.return_value(path_loglevel).lower()
# VyOS 1.2 had no proper value validation on the CLI thus the
# user could use any arbitrary values - sanitize them
if tmp not in ['quiet', 'fatal', 'error', 'info', 'verbose']:
tmp = 'info'
+ config.set(path_loglevel, value=tmp)
+
+# T4273: migrate ssh cipher list to multi node
+path_ciphers = base + ['ciphers']
+if config.exists(path_ciphers):
+ tmp = []
+ # get curtrent cipher list - comma delimited
+ for cipher in config.return_values(path_ciphers):
+ tmp.extend(cipher.split(','))
+ # delete old cipher suite representation
+ config.delete(path_ciphers)
- config.set(base, value=tmp)
+ for cipher in tmp:
+ config.set(path_ciphers, value=cipher, replace=False)
- try:
- with open(file_name, 'w') as f:
- f.write(config.to_string())
- except OSError as e:
- print("Failed to save the modified config: {}".format(e))
- exit(1)
+# T4273: migrate ssh key-exchange list to multi node
+path_kex = base + ['key-exchange']
+if config.exists(path_kex):
+ tmp = []
+ # get curtrent cipher list - comma delimited
+ for kex in config.return_values(path_kex):
+ tmp.extend(kex.split(','))
+ # delete old cipher suite representation
+ config.delete(path_kex)
+
+ for kex in tmp:
+ config.set(path_kex, value=kex, replace=False)
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ exit(1)
diff --git a/src/op_mode/cpu_summary.py b/src/op_mode/cpu_summary.py
index cfd321522..3bdf5a718 100755
--- a/src/op_mode/cpu_summary.py
+++ b/src/op_mode/cpu_summary.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018 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
@@ -19,18 +19,30 @@ from vyos.util import colon_separated_to_dict
FILE_NAME = '/proc/cpuinfo'
-with open(FILE_NAME, 'r') as f:
- data_raw = f.read()
+def get_raw_data():
+ with open(FILE_NAME, 'r') as f:
+ data_raw = f.read()
-data = colon_separated_to_dict(data_raw)
+ data = colon_separated_to_dict(data_raw)
-# Accumulate all data in a dict for future support for machine-readable output
-cpu_data = {}
-cpu_data['cpu_number'] = len(data['processor'])
-cpu_data['models'] = list(set(data['model name']))
+ # Accumulate all data in a dict for future support for machine-readable output
+ cpu_data = {}
+ cpu_data['cpu_number'] = len(data['processor'])
+ cpu_data['models'] = list(set(data['model name']))
-# Strip extra whitespace from CPU model names, /proc/cpuinfo is prone to that
-cpu_data['models'] = map(lambda s: re.sub(r'\s+', ' ', s), cpu_data['models'])
+ # Strip extra whitespace from CPU model names, /proc/cpuinfo is prone to that
+ cpu_data['models'] = list(map(lambda s: re.sub(r'\s+', ' ', s), cpu_data['models']))
+
+ return cpu_data
+
+def get_formatted_output():
+ cpu_data = get_raw_data()
+
+ out = "CPU(s): {0}\n".format(cpu_data['cpu_number'])
+ out += "CPU model(s): {0}".format(",".join(cpu_data['models']))
+
+ return out
+
+if __name__ == '__main__':
+ print(get_formatted_output())
-print("CPU(s): {0}".format(cpu_data['cpu_number']))
-print("CPU model(s): {0}".format(",".join(cpu_data['models'])))
diff --git a/src/op_mode/firewall.py b/src/op_mode/firewall.py
index b6bb5b802..3146fc357 100755
--- a/src/op_mode/firewall.py
+++ b/src/op_mode/firewall.py
@@ -88,7 +88,8 @@ def get_config_firewall(conf, name=None, ipv6=False, interfaces=True):
def get_nftables_details(name, ipv6=False):
suffix = '6' if ipv6 else ''
- command = f'sudo nft list chain ip{suffix} filter {name}'
+ name_prefix = 'NAME6_' if ipv6 else 'NAME_'
+ command = f'sudo nft list chain ip{suffix} filter {name_prefix}{name}'
try:
results = cmd(command)
except:
diff --git a/src/op_mode/generate_ovpn_client_file.py b/src/op_mode/generate_ovpn_client_file.py
new file mode 100755
index 000000000..29db41e37
--- /dev/null
+++ b/src/op_mode/generate_ovpn_client_file.py
@@ -0,0 +1,145 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import argparse
+import os
+
+from jinja2 import Template
+
+from vyos.configquery import ConfigTreeQuery
+from vyos.ifconfig import Section
+from vyos.util import cmd
+
+
+client_config = """
+
+client
+nobind
+remote {{ remote_host }} {{ port }}
+remote-cert-tls server
+proto {{ 'tcp-client' if protocol == 'tcp-active' else 'udp' }}
+dev {{ device }}
+dev-type {{ device }}
+persist-key
+persist-tun
+verb 3
+
+# Encryption options
+{% if encryption is defined and encryption is not none %}
+{% if encryption.cipher is defined and encryption.cipher is not none %}
+cipher {{ encryption.cipher }}
+{% if encryption.cipher == 'bf128' %}
+keysize 128
+{% elif encryption.cipher == 'bf256' %}
+keysize 256
+{% endif %}
+{% endif %}
+{% if encryption.ncp_ciphers is defined and encryption.ncp_ciphers is not none %}
+data-ciphers {{ encryption.ncp_ciphers }}
+{% endif %}
+{% endif %}
+
+{% if hash is defined and hash is not none %}
+auth {{ hash }}
+{% endif %}
+keysize 256
+comp-lzo {{ '' if use_lzo_compression is defined else 'no' }}
+
+<ca>
+-----BEGIN CERTIFICATE-----
+{{ ca }}
+-----END CERTIFICATE-----
+
+</ca>
+
+<cert>
+-----BEGIN CERTIFICATE-----
+{{ cert }}
+-----END CERTIFICATE-----
+
+</cert>
+
+<key>
+-----BEGIN PRIVATE KEY-----
+{{ key }}
+-----END PRIVATE KEY-----
+
+</key>
+
+"""
+
+config = ConfigTreeQuery()
+base = ['interfaces', 'openvpn']
+
+if not config.exists(base):
+ print('OpenVPN not configured')
+ exit(0)
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument("-i", "--interface", type=str, help='OpenVPN interface the client is connecting to', required=True)
+ parser.add_argument("-a", "--ca", type=str, help='OpenVPN CA cerificate', required=True)
+ parser.add_argument("-c", "--cert", type=str, help='OpenVPN client cerificate', required=True)
+ parser.add_argument("-k", "--key", type=str, help='OpenVPN client cerificate key', action="store")
+ args = parser.parse_args()
+
+ interface = args.interface
+ ca = args.ca
+ cert = args.cert
+ key = args.key
+ if not key:
+ key = args.cert
+
+ if interface not in Section.interfaces('openvpn'):
+ exit(f'OpenVPN interface "{interface}" does not exist!')
+
+ if not config.exists(['pki', 'ca', ca, 'certificate']):
+ exit(f'OpenVPN CA certificate "{ca}" does not exist!')
+
+ if not config.exists(['pki', 'certificate', cert, 'certificate']):
+ exit(f'OpenVPN certificate "{cert}" does not exist!')
+
+ if not config.exists(['pki', 'certificate', cert, 'private', 'key']):
+ exit(f'OpenVPN certificate key "{key}" does not exist!')
+
+ ca = config.value(['pki', 'ca', ca, 'certificate'])
+ cert = config.value(['pki', 'certificate', cert, 'certificate'])
+ key = config.value(['pki', 'certificate', key, 'private', 'key'])
+ remote_host = config.value(base + [interface, 'local-host'])
+
+ ovpn_conf = config.get_config_dict(base + [interface], key_mangling=('-', '_'), get_first_key=True)
+
+ port = '1194' if 'local_port' not in ovpn_conf else ovpn_conf['local_port']
+ proto = 'udp' if 'protocol' not in ovpn_conf else ovpn_conf['protocol']
+ device = 'tun' if 'device_type' not in ovpn_conf else ovpn_conf['device_type']
+
+ config = {
+ 'interface' : interface,
+ 'ca' : ca,
+ 'cert' : cert,
+ 'key' : key,
+ 'device' : device,
+ 'port' : port,
+ 'proto' : proto,
+ 'remote_host' : remote_host,
+ 'address' : [],
+ }
+
+# Clear out terminal first
+print('\x1b[2J\x1b[H')
+client = Template(client_config, trim_blocks=True).render(config)
+print(client)
diff --git a/src/op_mode/generate_public_key_command.py b/src/op_mode/generate_public_key_command.py
index 7a7b6c923..f071ae350 100755
--- a/src/op_mode/generate_public_key_command.py
+++ b/src/op_mode/generate_public_key_command.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021 VyOS maintainers and contributors
+# 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
@@ -29,8 +29,12 @@ def get_key(path):
key_string = vyos.remote.get_remote_config(path)
return key_string.split()
-username = sys.argv[1]
-algorithm, key, identifier = get_key(sys.argv[2])
+try:
+ username = sys.argv[1]
+ algorithm, key, identifier = get_key(sys.argv[2])
+except Exception as e:
+ print("Failed to retrieve the public key: {}".format(e))
+ sys.exit(1)
print('# To add this key as an embedded key, run the following commands:')
print('configure')
@@ -39,3 +43,4 @@ print(f'set system login user {username} authentication public-keys {identifier}
print('commit')
print('save')
print('exit')
+
diff --git a/src/op_mode/lldp_op.py b/src/op_mode/lldp_op.py
index b9ebc991a..17f6bf552 100755
--- a/src/op_mode/lldp_op.py
+++ b/src/op_mode/lldp_op.py
@@ -54,6 +54,7 @@ def parse_data(data, interface):
for local_if, values in neighbor.items():
if interface is not None and local_if != interface:
continue
+ cap = ''
for chassis, c_value in values.get('chassis', {}).items():
# bail out early if no capabilities found
if 'capability' not in c_value:
@@ -62,7 +63,6 @@ def parse_data(data, interface):
if isinstance(capabilities, dict):
capabilities = [capabilities]
- cap = ''
for capability in capabilities:
if capability['enabled']:
if capability['type'] == 'Router':
diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py
index 679b03c0b..fd4f86d88 100755
--- a/src/op_mode/powerctrl.py
+++ b/src/op_mode/powerctrl.py
@@ -33,10 +33,12 @@ def utc2local(datetime):
def parse_time(s):
try:
- if re.match(r'^\d{1,2}$', s):
- if (int(s) > 59):
+ if re.match(r'^\d{1,9999}$', s):
+ if (int(s) > 59) and (int(s) < 1440):
s = str(int(s)//60) + ":" + str(int(s)%60)
return datetime.strptime(s, "%H:%M").time()
+ if (int(s) >= 1440):
+ return s.split()
else:
return datetime.strptime(s, "%M").time()
else:
@@ -141,7 +143,7 @@ def execute_shutdown(time, reboot=True, ask=True):
cmd(f'/usr/bin/wall "{wall_msg}"')
else:
if not ts:
- exit(f'Invalid time "{time[0]}". The valid format is HH:MM')
+ exit(f'Invalid time "{time[0]}". Uses 24 Hour Clock format')
else:
exit(f'Invalid date "{time[1]}". A valid format is YYYY-MM-DD [HH:MM]')
else:
@@ -172,7 +174,12 @@ def main():
action.add_argument("--reboot", "-r",
help="Reboot the system",
nargs="*",
- metavar="Minutes|HH:MM")
+ metavar="HH:MM")
+
+ action.add_argument("--reboot_in", "-i",
+ help="Reboot the system",
+ nargs="*",
+ metavar="Minutes")
action.add_argument("--poweroff", "-p",
help="Poweroff the system",
@@ -190,7 +197,17 @@ def main():
try:
if args.reboot is not None:
+ for r in args.reboot:
+ if ':' not in r and '/' not in r and '.' not in r:
+ print("Incorrect format! Use HH:MM")
+ exit(1)
execute_shutdown(args.reboot, reboot=True, ask=args.yes)
+ if args.reboot_in is not None:
+ for i in args.reboot_in:
+ if ':' in i:
+ print("Incorrect format! Use Minutes")
+ exit(1)
+ execute_shutdown(args.reboot_in, reboot=True, ask=args.yes)
if args.poweroff is not None:
execute_shutdown(args.poweroff, reboot=False, ask=args.yes)
if args.cancel:
diff --git a/src/op_mode/show_cpu.py b/src/op_mode/show_cpu.py
index 0040e950d..9973d9789 100755
--- a/src/op_mode/show_cpu.py
+++ b/src/op_mode/show_cpu.py
@@ -21,7 +21,7 @@ from sys import exit
from vyos.util import popen, DEVNULL
OUT_TMPL_SRC = """
-{% if cpu %}
+{%- if cpu -%}
{% if 'vendor' in cpu %}CPU Vendor: {{cpu.vendor}}{% endif %}
{% if 'model' in cpu %}Model: {{cpu.model}}{% endif %}
{% if 'cpus' in cpu %}Total CPUs: {{cpu.cpus}}{% endif %}
@@ -31,31 +31,42 @@ OUT_TMPL_SRC = """
{% if 'mhz' in cpu %}Current MHz: {{cpu.mhz}}{% endif %}
{% if 'mhz_min' in cpu %}Minimum MHz: {{cpu.mhz_min}}{% endif %}
{% if 'mhz_max' in cpu %}Maximum MHz: {{cpu.mhz_max}}{% endif %}
-{% endif %}
+{%- endif -%}
"""
-cpu = {}
-cpu_json, code = popen('lscpu -J', stderr=DEVNULL)
-
-if code == 0:
- cpu_info = json.loads(cpu_json)
- if len(cpu_info) > 0 and 'lscpu' in cpu_info:
- for prop in cpu_info['lscpu']:
- if (prop['field'].find('Thread(s)') > -1): cpu['threads'] = prop['data']
- if (prop['field'].find('Core(s)')) > -1: cpu['cores'] = prop['data']
- if (prop['field'].find('Socket(s)')) > -1: cpu['sockets'] = prop['data']
- if (prop['field'].find('CPU(s):')) > -1: cpu['cpus'] = prop['data']
- if (prop['field'].find('CPU MHz')) > -1: cpu['mhz'] = prop['data']
- if (prop['field'].find('CPU min MHz')) > -1: cpu['mhz_min'] = prop['data']
- if (prop['field'].find('CPU max MHz')) > -1: cpu['mhz_max'] = prop['data']
- if (prop['field'].find('Vendor ID')) > -1: cpu['vendor'] = prop['data']
- if (prop['field'].find('Model name')) > -1: cpu['model'] = prop['data']
-
-if len(cpu) > 0:
- tmp = { 'cpu':cpu }
+def get_raw_data():
+ cpu = {}
+ cpu_json, code = popen('lscpu -J', stderr=DEVNULL)
+
+ if code == 0:
+ cpu_info = json.loads(cpu_json)
+ if len(cpu_info) > 0 and 'lscpu' in cpu_info:
+ for prop in cpu_info['lscpu']:
+ if (prop['field'].find('Thread(s)') > -1): cpu['threads'] = prop['data']
+ if (prop['field'].find('Core(s)')) > -1: cpu['cores'] = prop['data']
+ if (prop['field'].find('Socket(s)')) > -1: cpu['sockets'] = prop['data']
+ if (prop['field'].find('CPU(s):')) > -1: cpu['cpus'] = prop['data']
+ if (prop['field'].find('CPU MHz')) > -1: cpu['mhz'] = prop['data']
+ if (prop['field'].find('CPU min MHz')) > -1: cpu['mhz_min'] = prop['data']
+ if (prop['field'].find('CPU max MHz')) > -1: cpu['mhz_max'] = prop['data']
+ if (prop['field'].find('Vendor ID')) > -1: cpu['vendor'] = prop['data']
+ if (prop['field'].find('Model name')) > -1: cpu['model'] = prop['data']
+
+ return cpu
+
+def get_formatted_output():
+ cpu = get_raw_data()
+
+ tmp = {'cpu':cpu}
tmpl = Template(OUT_TMPL_SRC)
- print(tmpl.render(tmp))
- exit(0)
-else:
- print('CPU information could not be determined\n')
- exit(1)
+ return tmpl.render(tmp)
+
+if __name__ == '__main__':
+ cpu = get_raw_data()
+
+ if len(cpu) > 0:
+ print(get_formatted_output())
+ else:
+ print('CPU information could not be determined\n')
+ exit(1)
+
diff --git a/src/op_mode/show_ipsec_sa.py b/src/op_mode/show_ipsec_sa.py
index e72f0f965..5b8f00dba 100755
--- a/src/op_mode/show_ipsec_sa.py
+++ b/src/op_mode/show_ipsec_sa.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019 VyOS maintainers and contributors
+# 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
@@ -14,119 +14,117 @@
# 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 re
-import sys
+from re import split as re_split
+from sys import exit
-import vici
-import tabulate
-import hurry.filesize
+from hurry import filesize
+from tabulate import tabulate
+from vici import Session as vici_session
+
+from vyos.util import seconds_to_human
-import vyos.util
def convert(text):
return int(text) if text.isdigit() else text.lower()
+
def alphanum_key(key):
- return [convert(c) for c in re.split('([0-9]+)', str(key))]
+ return [convert(c) for c in re_split('([0-9]+)', str(key))]
-def format_output(conns, sas):
+
+def format_output(sas):
sa_data = []
- for peer, parent_conn in conns.items():
- if peer not in sas:
- continue
-
- parent_sa = sas[peer]
- child_sas = parent_sa['child-sas']
- installed_sas = {v['name'].decode(): v for k, v in child_sas.items() if v["state"] == b"INSTALLED"}
-
- # parent_sa["state"] = IKE state, child_sas["state"] = ESP state
- state = 'down'
- uptime = 'N/A'
-
- if parent_sa["state"] == b"ESTABLISHED" and installed_sas:
- state = "up"
-
- remote_host = parent_sa["remote-host"].decode()
- remote_id = parent_sa["remote-id"].decode()
-
- if remote_host == remote_id:
- remote_id = "N/A"
-
- # The counters can only be obtained from the child SAs
- for child_conn in parent_conn['children']:
- if child_conn not in installed_sas:
- data = [child_conn, "down", "N/A", "N/A", "N/A", "N/A", "N/A", "N/A"]
- sa_data.append(data)
- continue
-
- isa = installed_sas[child_conn]
- csa_name = isa['name']
- csa_name = csa_name.decode()
-
- bytes_in = hurry.filesize.size(int(isa["bytes-in"].decode()))
- bytes_out = hurry.filesize.size(int(isa["bytes-out"].decode()))
- bytes_str = "{0}/{1}".format(bytes_in, bytes_out)
-
- pkts_in = hurry.filesize.size(int(isa["packets-in"].decode()), system=hurry.filesize.si)
- pkts_out = hurry.filesize.size(int(isa["packets-out"].decode()), system=hurry.filesize.si)
- pkts_str = "{0}/{1}".format(pkts_in, pkts_out)
- # Remove B from <1K values
- pkts_str = re.sub(r'B', r'', pkts_str)
-
- uptime = vyos.util.seconds_to_human(isa['install-time'].decode())
-
- enc = isa["encr-alg"].decode()
- if "encr-keysize" in isa:
- key_size = isa["encr-keysize"].decode()
- else:
- key_size = ""
- if "integ-alg" in isa:
- hash = isa["integ-alg"].decode()
- else:
- hash = ""
- if "dh-group" in isa:
- dh_group = isa["dh-group"].decode()
- else:
- dh_group = ""
-
- proposal = enc
- if key_size:
- proposal = "{0}_{1}".format(proposal, key_size)
- if hash:
- proposal = "{0}/{1}".format(proposal, hash)
- if dh_group:
- proposal = "{0}/{1}".format(proposal, dh_group)
-
- data = [csa_name, state, uptime, bytes_str, pkts_str, remote_host, remote_id, proposal]
- sa_data.append(data)
+ for sa in sas:
+ for parent_sa in sa.values():
+ # create an item for each child-sa
+ for child_sa in parent_sa.get('child-sas', {}).values():
+ # prepare a list for output data
+ sa_out_name = sa_out_state = sa_out_uptime = sa_out_bytes = sa_out_packets = sa_out_remote_addr = sa_out_remote_id = sa_out_proposal = 'N/A'
+
+ # collect raw data
+ sa_name = child_sa.get('name')
+ sa_state = child_sa.get('state')
+ sa_uptime = child_sa.get('install-time')
+ sa_bytes_in = child_sa.get('bytes-in')
+ sa_bytes_out = child_sa.get('bytes-out')
+ sa_packets_in = child_sa.get('packets-in')
+ sa_packets_out = child_sa.get('packets-out')
+ sa_remote_addr = parent_sa.get('remote-host')
+ sa_remote_id = parent_sa.get('remote-id')
+ sa_proposal_encr_alg = child_sa.get('encr-alg')
+ sa_proposal_integ_alg = child_sa.get('integ-alg')
+ sa_proposal_encr_keysize = child_sa.get('encr-keysize')
+ sa_proposal_dh_group = child_sa.get('dh-group')
+
+ # format data to display
+ if sa_name:
+ sa_out_name = sa_name.decode()
+ if sa_state:
+ if sa_state == b'INSTALLED':
+ sa_out_state = 'up'
+ else:
+ sa_out_state = 'down'
+ if sa_uptime:
+ sa_out_uptime = seconds_to_human(sa_uptime.decode())
+ if sa_bytes_in and sa_bytes_out:
+ bytes_in = filesize.size(int(sa_bytes_in.decode()))
+ bytes_out = filesize.size(int(sa_bytes_out.decode()))
+ sa_out_bytes = f'{bytes_in}/{bytes_out}'
+ if sa_packets_in and sa_packets_out:
+ packets_in = filesize.size(int(sa_packets_in.decode()),
+ system=filesize.si)
+ packets_out = filesize.size(int(sa_packets_out.decode()),
+ system=filesize.si)
+ sa_out_packets = f'{packets_in}/{packets_out}'
+ if sa_remote_addr:
+ sa_out_remote_addr = sa_remote_addr.decode()
+ if sa_remote_id:
+ sa_out_remote_id = sa_remote_id.decode()
+ # format proposal
+ if sa_proposal_encr_alg:
+ sa_out_proposal = sa_proposal_encr_alg.decode()
+ if sa_proposal_encr_keysize:
+ sa_proposal_encr_keysize_str = sa_proposal_encr_keysize.decode()
+ sa_out_proposal = f'{sa_out_proposal}_{sa_proposal_encr_keysize_str}'
+ if sa_proposal_integ_alg:
+ sa_proposal_integ_alg_str = sa_proposal_integ_alg.decode()
+ sa_out_proposal = f'{sa_out_proposal}/{sa_proposal_integ_alg_str}'
+ if sa_proposal_dh_group:
+ sa_proposal_dh_group_str = sa_proposal_dh_group.decode()
+ sa_out_proposal = f'{sa_out_proposal}/{sa_proposal_dh_group_str}'
+
+ # add a new item to output data
+ sa_data.append([
+ sa_out_name, sa_out_state, sa_out_uptime, sa_out_bytes,
+ sa_out_packets, sa_out_remote_addr, sa_out_remote_id,
+ sa_out_proposal
+ ])
+
+ # return output data
return sa_data
+
if __name__ == '__main__':
try:
- session = vici.Session()
- conns = {}
- sas = {}
+ session = vici_session()
+ sas = list(session.list_sas())
- for conn in session.list_conns():
- for key in conn:
- conns[key] = conn[key]
-
- for sa in session.list_sas():
- for key in sa:
- sas[key] = sa[key]
-
- headers = ["Connection", "State", "Uptime", "Bytes In/Out", "Packets In/Out", "Remote address", "Remote ID", "Proposal"]
- sa_data = format_output(conns, sas)
+ sa_data = format_output(sas)
sa_data = sorted(sa_data, key=alphanum_key)
- output = tabulate.tabulate(sa_data, headers)
+
+ headers = [
+ "Connection", "State", "Uptime", "Bytes In/Out", "Packets In/Out",
+ "Remote address", "Remote ID", "Proposal"
+ ]
+ output = tabulate(sa_data, headers)
print(output)
except PermissionError:
print("You do not have a permission to connect to the IPsec daemon")
- sys.exit(1)
+ exit(1)
except ConnectionRefusedError:
print("IPsec is not runing")
- sys.exit(1)
+ exit(1)
except Exception as e:
print("An error occured: {0}".format(e))
- sys.exit(1)
+ exit(1)
diff --git a/src/op_mode/show_ram.py b/src/op_mode/show_ram.py
index 5818ec132..2b0be3965 100755
--- a/src/op_mode/show_ram.py
+++ b/src/op_mode/show_ram.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021 VyOS maintainers and contributors
+# 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
@@ -55,10 +55,17 @@ def get_system_memory_human():
return mem
-if __name__ == '__main__':
- mem = get_system_memory_human()
+def get_raw_data():
+ return get_system_memory_human()
+
+def get_formatted_output():
+ mem = get_raw_data()
- print("Total: {}".format(mem["total"]))
- print("Free: {}".format(mem["free"]))
- print("Used: {}".format(mem["used"]))
+ out = "Total: {}\n".format(mem["total"])
+ out += "Free: {}\n".format(mem["free"])
+ out += "Used: {}".format(mem["used"])
+ return out
+
+if __name__ == '__main__':
+ print(get_formatted_output())
diff --git a/src/op_mode/show_uptime.py b/src/op_mode/show_uptime.py
index c3dea52e6..1b5e33fa9 100755
--- a/src/op_mode/show_uptime.py
+++ b/src/op_mode/show_uptime.py
@@ -37,14 +37,27 @@ def get_load_averages():
return res
-if __name__ == '__main__':
+def get_raw_data():
from vyos.util import seconds_to_human
- print("Uptime: {}\n".format(seconds_to_human(get_uptime_seconds())))
+ res = {}
+ res["uptime_seconds"] = get_uptime_seconds()
+ res["uptime"] = seconds_to_human(get_uptime_seconds())
+ res["load_average"] = get_load_averages()
+
+ return res
- avgs = get_load_averages()
+def get_formatted_output():
+ data = get_raw_data()
- print("Load averages:")
- print("1 minute: {:.02f}%".format(avgs[1]*100))
- print("5 minutes: {:.02f}%".format(avgs[5]*100))
- print("15 minutes: {:.02f}%".format(avgs[15]*100))
+ out = "Uptime: {}\n\n".format(data["uptime"])
+ avgs = data["load_average"]
+ out += "Load averages:\n"
+ out += "1 minute: {:.02f}%\n".format(avgs[1]*100)
+ out += "5 minutes: {:.02f}%\n".format(avgs[5]*100)
+ out += "15 minutes: {:.02f}%\n".format(avgs[15]*100)
+
+ return out
+
+if __name__ == '__main__':
+ print(get_formatted_output())
diff --git a/src/op_mode/show_version.py b/src/op_mode/show_version.py
index 7962e1e7b..b82ab6eca 100755
--- a/src/op_mode/show_version.py
+++ b/src/op_mode/show_version.py
@@ -26,10 +26,6 @@ from jinja2 import Template
from sys import exit
from vyos.util import call
-parser = argparse.ArgumentParser()
-parser.add_argument("-f", "--funny", action="store_true", help="Add something funny to the output")
-parser.add_argument("-j", "--json", action="store_true", help="Produce JSON output")
-
version_output_tmpl = """
Version: VyOS {{version}}
Release train: {{release_train}}
@@ -51,7 +47,20 @@ Hardware UUID: {{hardware_uuid}}
Copyright: VyOS maintainers and contributors
"""
+def get_raw_data():
+ version_data = vyos.version.get_full_version_data()
+ return version_data
+
+def get_formatted_output():
+ version_data = get_raw_data()
+ tmpl = Template(version_output_tmpl)
+ return tmpl.render(version_data)
+
if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument("-f", "--funny", action="store_true", help="Add something funny to the output")
+ parser.add_argument("-j", "--json", action="store_true", help="Produce JSON output")
+
args = parser.parse_args()
version_data = vyos.version.get_full_version_data()
@@ -60,9 +69,8 @@ if __name__ == '__main__':
import json
print(json.dumps(version_data))
exit(0)
-
- tmpl = Template(version_output_tmpl)
- print(tmpl.render(version_data))
+ else:
+ print(get_formatted_output())
if args.funny:
print(vyos.limericks.get_random())
diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server
index 06871f1d6..1000d8b72 100755
--- a/src/services/vyos-http-api-server
+++ b/src/services/vyos-http-api-server
@@ -648,10 +648,12 @@ if __name__ == '__main__':
app.state.vyos_keys = server_config['api_keys']
app.state.vyos_debug = server_config['debug']
+ app.state.vyos_gql = server_config['gql']
app.state.vyos_strict = server_config['strict']
app.state.vyos_origins = server_config.get('cors', {}).get('origins', [])
- graphql_init(app)
+ if app.state.vyos_gql:
+ graphql_init(app)
try:
if not server_config['socket']:
diff --git a/src/system/keepalived-fifo.py b/src/system/keepalived-fifo.py
index b1fe7e43f..a8df232ae 100755
--- a/src/system/keepalived-fifo.py
+++ b/src/system/keepalived-fifo.py
@@ -71,7 +71,8 @@ class KeepalivedFifo:
# Read VRRP configuration directly from CLI
self.vrrp_config_dict = conf.get_config_dict(base,
- key_mangling=('-', '_'), get_first_key=True)
+ key_mangling=('-', '_'), get_first_key=True,
+ no_tag_node_value_mangle=True)
logger.debug(f'Loaded configuration: {self.vrrp_config_dict}')
except Exception as err:
diff --git a/src/validators/ipv6-range b/src/validators/ipv6-range
index a3c401281..7080860c4 100755
--- a/src/validators/ipv6-range
+++ b/src/validators/ipv6-range
@@ -1,17 +1,20 @@
-#!/usr/bin/python3
+#!/usr/bin/env python3
-import sys
-import re
-from vyos.template import is_ipv6
+from ipaddress import IPv6Address
+from sys import argv, exit
if __name__ == '__main__':
- if len(sys.argv)>1:
- ipv6_range = sys.argv[1]
- # Regex for ipv6-ipv6 https://regexr.com/
- if re.search('([a-f0-9:]+:+)+[a-f0-9]+-([a-f0-9:]+:+)+[a-f0-9]+', ipv6_range):
- for tmp in ipv6_range.split('-'):
- if not is_ipv6(tmp):
- print(f'Error: {ipv6_range} is not a valid IPv6 range')
- sys.exit(1)
-
- sys.exit(0)
+ if len(argv) > 1:
+ # try to pass validation and raise an error if failed
+ try:
+ ipv6_range = argv[1]
+ range_left = ipv6_range.split('-')[0]
+ range_right = ipv6_range.split('-')[1]
+ if not IPv6Address(range_left) < IPv6Address(range_right):
+ raise ValueError(f'left element {range_left} must be less than right element {range_right}')
+ except Exception as err:
+ print(f'Error: {ipv6_range} is not a valid IPv6 range: {err}')
+ exit(1)
+ else:
+ print('Error: an IPv6 range argument must be provided')
+ exit(1)