summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rwxr-xr-xsrc/conf_mode/firewall.py28
-rwxr-xr-xsrc/conf_mode/pki.py129
-rwxr-xr-xsrc/conf_mode/policy.py10
-rwxr-xr-xsrc/conf_mode/protocols_eigrp.py123
-rwxr-xr-xsrc/conf_mode/protocols_rip.py2
-rwxr-xr-xsrc/conf_mode/service_event_handler.py91
-rwxr-xr-xsrc/conf_mode/service_sla.py113
-rwxr-xr-xsrc/helpers/vyos-domain-group-resolve.py60
-rwxr-xr-xsrc/migration-scripts/policy/2-to-358
-rwxr-xr-xsrc/migration-scripts/system/23-to-244
-rwxr-xr-xsrc/op_mode/pki.py201
-rwxr-xr-xsrc/op_mode/show_neigh.py162
-rwxr-xr-xsrc/system/vyos-event-handler.py160
-rw-r--r--src/systemd/vyos-domain-group-resolve.service11
-rw-r--r--src/systemd/vyos-event-handler.service11
15 files changed, 1059 insertions, 104 deletions
diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py
index 6924bf555..335098bf1 100755
--- a/src/conf_mode/firewall.py
+++ b/src/conf_mode/firewall.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021 VyOS maintainers and contributors
+# Copyright (C) 2021-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -26,7 +26,13 @@ from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.configdict import node_changed
from vyos.configdiff import get_config_diff, Diff
+from vyos.firewall import get_ips_domains_dict
+from vyos.firewall import nft_add_set_elements
+from vyos.firewall import nft_flush_set
+from vyos.firewall import nft_init_set
+from vyos.firewall import nft_update_set_elements
from vyos.template import render
+from vyos.util import call
from vyos.util import cmd
from vyos.util import dict_search_args
from vyos.util import process_named_running
@@ -408,6 +414,26 @@ def apply(firewall):
if install_result == 1:
raise ConfigError('Failed to apply firewall')
+ # set fireall group domain-group xxx
+ if 'group' in firewall:
+ if 'domain_group' in firewall['group']:
+ # T970 Enable a resolver (systemd daemon) that checks
+ # domain-group addresses and update entries for domains by timeout
+ # If router loaded without internet connection or for synchronization
+ call('systemctl restart vyos-domain-group-resolve.service')
+ for group, group_config in firewall['group']['domain_group'].items():
+ domains = []
+ for address in group_config['address']:
+ domains.append(address)
+ # Add elements to domain-group, try to resolve domain => ip
+ # and add elements to nft set
+ ip_dict = get_ips_domains_dict(domains)
+ elements = sum(ip_dict.values(), [])
+ nft_init_set(group)
+ nft_add_set_elements(group, elements)
+ else:
+ call('systemctl stop vyos-domain-group-resolve.service')
+
if 'state_policy' in firewall and not state_policy_rule_exists():
for chain in ['VYOS_FW_FORWARD', 'VYOS_FW_OUTPUT', 'VYOS_FW_LOCAL']:
cmd(f'nft insert rule ip filter {chain} jump VYOS_STATE_POLICY')
diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py
index efa3578b4..29ed7b1b7 100755
--- a/src/conf_mode/pki.py
+++ b/src/conf_mode/pki.py
@@ -29,12 +29,60 @@ from vyos.pki import load_private_key
from vyos.pki import load_crl
from vyos.pki import load_dh_parameters
from vyos.util import ask_input
+from vyos.util import call
+from vyos.util import dict_search_args
from vyos.util import dict_search_recursive
from vyos.xml import defaults
from vyos import ConfigError
from vyos import airbag
airbag.enable()
+# keys to recursively search for under specified path, script to call if update required
+sync_search = [
+ {
+ 'keys': ['certificate'],
+ 'path': ['service', 'https'],
+ 'script': '/usr/libexec/vyos/conf_mode/https.py'
+ },
+ {
+ 'keys': ['certificate', 'ca_certificate'],
+ 'path': ['interfaces', 'ethernet'],
+ 'script': '/usr/libexec/vyos/conf_mode/interfaces-ethernet.py'
+ },
+ {
+ 'keys': ['certificate', 'ca_certificate', 'dh_params', 'shared_secret_key', 'auth_key', 'crypt_key'],
+ 'path': ['interfaces', 'openvpn'],
+ 'script': '/usr/libexec/vyos/conf_mode/interfaces-openvpn.py'
+ },
+ {
+ 'keys': ['certificate', 'ca_certificate', 'local_key', 'remote_key'],
+ 'path': ['vpn', 'ipsec'],
+ 'script': '/usr/libexec/vyos/conf_mode/vpn_ipsec.py'
+ },
+ {
+ 'keys': ['certificate', 'ca_certificate'],
+ 'path': ['vpn', 'openconnect'],
+ 'script': '/usr/libexec/vyos/conf_mode/vpn_openconnect.py'
+ },
+ {
+ 'keys': ['certificate', 'ca_certificate'],
+ 'path': ['vpn', 'sstp'],
+ 'script': '/usr/libexec/vyos/conf_mode/vpn_sstp.py'
+ }
+]
+
+# key from other config nodes -> key in pki['changed'] and pki
+sync_translate = {
+ 'certificate': 'certificate',
+ 'ca_certificate': 'ca',
+ 'dh_params': 'dh',
+ 'local_key': 'key_pair',
+ 'remote_key': 'key_pair',
+ 'shared_secret_key': 'openvpn',
+ 'auth_key': 'openvpn',
+ 'crypt_key': 'openvpn'
+}
+
def get_config(config=None):
if config:
conf = config
@@ -47,12 +95,21 @@ def get_config(config=None):
no_tag_node_value_mangle=True)
pki['changed'] = {}
- tmp = node_changed(conf, base + ['ca'], key_mangling=('-', '_'))
+ tmp = node_changed(conf, base + ['ca'], key_mangling=('-', '_'), recursive=True)
if tmp: pki['changed'].update({'ca' : tmp})
- tmp = node_changed(conf, base + ['certificate'], key_mangling=('-', '_'))
+ tmp = node_changed(conf, base + ['certificate'], key_mangling=('-', '_'), recursive=True)
if tmp: pki['changed'].update({'certificate' : tmp})
+ tmp = node_changed(conf, base + ['dh'], key_mangling=('-', '_'), recursive=True)
+ if tmp: pki['changed'].update({'dh' : tmp})
+
+ tmp = node_changed(conf, base + ['key-pair'], key_mangling=('-', '_'), recursive=True)
+ if tmp: pki['changed'].update({'key_pair' : tmp})
+
+ tmp = node_changed(conf, base + ['openvpn', 'shared-secret'], key_mangling=('-', '_'), recursive=True)
+ if tmp: pki['changed'].update({'openvpn' : tmp})
+
# We only merge on the defaults of there is a configuration at all
if conf.exists(base):
default_values = defaults(base)
@@ -164,17 +221,30 @@ def verify(pki):
if 'changed' in pki:
# if the list is getting longer, we can move to a dict() and also embed the
# search key as value from line 173 or 176
- for cert_type in ['ca', 'certificate']:
- if not cert_type in pki['changed']:
- continue
- for certificate in pki['changed'][cert_type]:
- if cert_type not in pki or certificate not in pki['changed'][cert_type]:
- if cert_type == 'ca':
- if certificate in dict_search_recursive(pki['system'], 'ca_certificate'):
- raise ConfigError(f'CA certificate "{certificate}" is still in use!')
- elif cert_type == 'certificate':
- if certificate in dict_search_recursive(pki['system'], 'certificate'):
- raise ConfigError(f'Certificate "{certificate}" is still in use!')
+ for search in sync_search:
+ for key in search['keys']:
+ changed_key = sync_translate[key]
+
+ if changed_key not in pki['changed']:
+ continue
+
+ for item_name in pki['changed'][changed_key]:
+ node_present = False
+ if changed_key == 'openvpn':
+ node_present = dict_search_args(pki, 'openvpn', 'shared_secret', item_name)
+ else:
+ node_present = dict_search_args(pki, changed_key, item_name)
+
+ if not node_present:
+ search_dict = dict_search_args(pki['system'], *search['path'])
+
+ if not search_dict:
+ continue
+
+ for found_name, found_path in dict_search_recursive(search_dict, key):
+ if found_name == item_name:
+ path_str = " ".join(search['path'] + found_path)
+ raise ConfigError(f'PKI object "{item_name}" still in use by "{path_str}"')
return None
@@ -188,7 +258,38 @@ def apply(pki):
if not pki:
return None
- # XXX: restart services if the content of a certificate changes
+ if 'changed' in pki:
+ for search in sync_search:
+ for key in search['keys']:
+ changed_key = sync_translate[key]
+
+ if changed_key not in pki['changed']:
+ continue
+
+ for item_name in pki['changed'][changed_key]:
+ node_present = False
+ if changed_key == 'openvpn':
+ node_present = dict_search_args(pki, 'openvpn', 'shared_secret', item_name)
+ else:
+ node_present = dict_search_args(pki, changed_key, item_name)
+
+ if node_present:
+ search_dict = dict_search_args(pki['system'], *search['path'])
+
+ if not search_dict:
+ continue
+
+ for found_name, found_path in dict_search_recursive(search_dict, key):
+ if found_name == item_name:
+ path_str = ' '.join(search['path'] + found_path)
+ print(f'pki: Updating config: {path_str} {found_name}')
+
+ script = search['script']
+ if found_path[0] == 'interfaces':
+ ifname = found_path[2]
+ call(f'VYOS_TAGNODE_VALUE={ifname} {script}')
+ else:
+ call(script)
return None
diff --git a/src/conf_mode/policy.py b/src/conf_mode/policy.py
index ef6008140..3008a20e0 100755
--- a/src/conf_mode/policy.py
+++ b/src/conf_mode/policy.py
@@ -150,6 +150,16 @@ def verify(policy):
tmp = dict_search('match.ipv6.address.prefix_list', rule_config)
if tmp and tmp not in policy.get('prefix_list6', []):
raise ConfigError(f'prefix-list6 {tmp} does not exist!')
+
+ # Specified access_list6 in nexthop must exist
+ tmp = dict_search('match.ipv6.nexthop.access_list', rule_config)
+ if tmp and tmp not in policy.get('access_list6', []):
+ raise ConfigError(f'access_list6 {tmp} does not exist!')
+
+ # Specified prefix-list6 in nexthop must exist
+ tmp = dict_search('match.ipv6.nexthop.prefix_list', rule_config)
+ if tmp and tmp not in policy.get('prefix_list6', []):
+ raise ConfigError(f'prefix-list6 {tmp} does not exist!')
# When routing protocols are active some use prefix-lists, route-maps etc.
# to apply the systems routing policy to the learned or redistributed routes.
diff --git a/src/conf_mode/protocols_eigrp.py b/src/conf_mode/protocols_eigrp.py
new file mode 100755
index 000000000..c1a1a45e1
--- /dev/null
+++ b/src/conf_mode/protocols_eigrp.py
@@ -0,0 +1,123 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from sys import exit
+from sys import argv
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.template import render_to_string
+from vyos import ConfigError
+from vyos import frr
+from vyos import airbag
+airbag.enable()
+
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ vrf = None
+ if len(argv) > 1:
+ vrf = argv[1]
+
+ base_path = ['protocols', 'eigrp']
+
+ # eqivalent of the C foo ? 'a' : 'b' statement
+ base = vrf and ['vrf', 'name', vrf, 'protocols', 'eigrp'] or base_path
+ eigrp = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+
+ # Assign the name of our VRF context. This MUST be done before the return
+ # statement below, else on deletion we will delete the default instance
+ # instead of the VRF instance.
+ if vrf: eigrp.update({'vrf' : vrf})
+
+ if not conf.exists(base):
+ eigrp.update({'deleted' : ''})
+ if not vrf:
+ # We are running in the default VRF context, thus we can not delete
+ # our main EIGRP instance if there are dependent EIGRP VRF instances.
+ eigrp['dependent_vrfs'] = conf.get_config_dict(['vrf', 'name'],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ return eigrp
+
+ # We also need some additional information from the config, prefix-lists
+ # and route-maps for instance. They will be used in verify().
+ #
+ # XXX: one MUST always call this without the key_mangling() option! See
+ # vyos.configverify.verify_common_route_maps() for more information.
+ tmp = conf.get_config_dict(['policy'])
+ # Merge policy dict into "regular" config dict
+ eigrp = dict_merge(tmp, eigrp)
+
+ import pprint
+ pprint.pprint(eigrp)
+ return eigrp
+
+def verify(eigrp):
+ pass
+
+def generate(eigrp):
+ if not eigrp or 'deleted' in eigrp:
+ return None
+
+ eigrp['protocol'] = 'eigrp' # required for frr/vrf.route-map.frr.j2
+ eigrp['frr_zebra_config'] = render_to_string('frr/vrf.route-map.frr.j2', eigrp)
+ eigrp['frr_eigrpd_config'] = render_to_string('frr/eigrpd.frr.j2', eigrp)
+
+def apply(eigrp):
+ eigrp_daemon = 'eigrpd'
+ zebra_daemon = 'zebra'
+
+ # Save original configuration prior to starting any commit actions
+ frr_cfg = frr.FRRConfig()
+
+ # The route-map used for the FIB (zebra) is part of the zebra daemon
+ frr_cfg.load_configuration(zebra_daemon)
+ frr_cfg.modify_section(r'(\s+)?ip protocol eigrp route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)')
+ if 'frr_zebra_config' in eigrp:
+ frr_cfg.add_before(frr.default_add_before, eigrp['frr_zebra_config'])
+ frr_cfg.commit_configuration(zebra_daemon)
+
+ # Generate empty helper string which can be ammended to FRR commands, it
+ # will be either empty (default VRF) or contain the "vrf <name" statement
+ vrf = ''
+ if 'vrf' in eigrp:
+ vrf = ' vrf ' + eigrp['vrf']
+
+ frr_cfg.load_configuration(eigrp_daemon)
+ frr_cfg.modify_section(f'^router eigrp \d+{vrf}', stop_pattern='^exit', remove_stop_mark=True)
+ if 'frr_eigrpd_config' in eigrp:
+ frr_cfg.add_before(frr.default_add_before, eigrp['frr_eigrpd_config'])
+ frr_cfg.commit_configuration(eigrp_daemon)
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/protocols_rip.py b/src/conf_mode/protocols_rip.py
index a76c1ce76..c78d90396 100755
--- a/src/conf_mode/protocols_rip.py
+++ b/src/conf_mode/protocols_rip.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021 VyOS maintainers and contributors
+# Copyright (C) 2021-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
diff --git a/src/conf_mode/service_event_handler.py b/src/conf_mode/service_event_handler.py
new file mode 100755
index 000000000..5440d1056
--- /dev/null
+++ b/src/conf_mode/service_event_handler.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import json
+from pathlib import Path
+
+from vyos.config import Config
+from vyos.util import call, dict_search
+from vyos import ConfigError
+from vyos import airbag
+
+airbag.enable()
+
+service_name = 'vyos-event-handler'
+service_conf = Path(f'/run/{service_name}.conf')
+
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['service', 'event-handler', 'event']
+ config = conf.get_config_dict(base,
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ return config
+
+
+def verify(config):
+ # bail out early - looks like removal from running config
+ if not config:
+ return None
+
+ for name, event_config in config.items():
+ if not dict_search('filter.pattern', event_config) or not dict_search(
+ 'script.path', event_config):
+ raise ConfigError(
+ 'Event-handler: both pattern and script path items are mandatory'
+ )
+
+ if dict_search('script.environment.message', event_config):
+ raise ConfigError(
+ 'Event-handler: "message" environment variable is reserved for log message text'
+ )
+
+
+def generate(config):
+ if not config:
+ # Remove old config and return
+ service_conf.unlink(missing_ok=True)
+ return None
+
+ # Write configuration file
+ conf_json = json.dumps(config, indent=4)
+ service_conf.write_text(conf_json)
+
+ return None
+
+
+def apply(config):
+ if config:
+ call(f'systemctl restart {service_name}.service')
+ else:
+ call(f'systemctl stop {service_name}.service')
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/service_sla.py b/src/conf_mode/service_sla.py
new file mode 100755
index 000000000..e7c3ca59c
--- /dev/null
+++ b/src/conf_mode/service_sla.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.template import render
+from vyos.util import call
+from vyos.xml import defaults
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+
+owamp_config_dir = '/etc/owamp-server'
+owamp_config_file = f'{owamp_config_dir}/owamp-server.conf'
+systemd_override_owamp = r'/etc/systemd/system/owamp-server.d/20-override.conf'
+
+twamp_config_dir = '/etc/twamp-server'
+twamp_config_file = f'{twamp_config_dir}/twamp-server.conf'
+systemd_override_twamp = r'/etc/systemd/system/twamp-server.d/20-override.conf'
+
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['service', 'sla']
+ if not conf.exists(base):
+ return None
+
+ sla = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ # We have gathered the dict representation of the CLI, but there are default
+ # options which we need to update into the dictionary retrived.
+ default_values = defaults(base)
+ sla = dict_merge(default_values, sla)
+
+ # Ignore default XML values if config doesn't exists
+ # Delete key from dict
+ if not conf.exists(base + ['owamp-server']):
+ del sla['owamp_server']
+ if not conf.exists(base + ['twamp-server']):
+ del sla['twamp_server']
+
+ return sla
+
+def verify(sla):
+ if not sla:
+ return None
+
+def generate(sla):
+ if not sla:
+ return None
+
+ render(owamp_config_file, 'sla/owamp-server.conf.j2', sla)
+ render(systemd_override_owamp, 'sla/owamp-override.conf.j2', sla)
+
+ render(twamp_config_file, 'sla/twamp-server.conf.j2', sla)
+ render(systemd_override_twamp, 'sla/twamp-override.conf.j2', sla)
+
+ return None
+
+def apply(sla):
+ owamp_service = 'owamp-server.service'
+ twamp_service = 'twamp-server.service'
+
+ call('systemctl daemon-reload')
+
+ if not sla or 'owamp_server' not in sla:
+ call(f'systemctl stop {owamp_service}')
+
+ if os.path.exists(owamp_config_file):
+ os.unlink(owamp_config_file)
+
+ if not sla or 'twamp_server' not in sla:
+ call(f'systemctl stop {twamp_service}')
+ if os.path.exists(twamp_config_file):
+ os.unlink(twamp_config_file)
+
+ if sla and 'owamp_server' in sla:
+ call(f'systemctl reload-or-restart {owamp_service}')
+
+ if sla and 'twamp_server' in sla:
+ call(f'systemctl reload-or-restart {twamp_service}')
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/helpers/vyos-domain-group-resolve.py b/src/helpers/vyos-domain-group-resolve.py
new file mode 100755
index 000000000..e8501cfc6
--- /dev/null
+++ b/src/helpers/vyos-domain-group-resolve.py
@@ -0,0 +1,60 @@
+#!/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 time
+
+from vyos.configquery import ConfigTreeQuery
+from vyos.firewall import get_ips_domains_dict
+from vyos.firewall import nft_add_set_elements
+from vyos.firewall import nft_flush_set
+from vyos.firewall import nft_init_set
+from vyos.firewall import nft_update_set_elements
+from vyos.util import call
+
+
+base = ['firewall', 'group', 'domain-group']
+check_required = True
+# count_failed = 0
+# Timeout in sec between checks
+timeout = 300
+
+domain_state = {}
+
+if __name__ == '__main__':
+
+ while check_required:
+ config = ConfigTreeQuery()
+ if config.exists(base):
+ domain_groups = config.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ for set_name, domain_config in domain_groups.items():
+ list_domains = domain_config['address']
+ elements = []
+ ip_dict = get_ips_domains_dict(list_domains)
+
+ for domain in list_domains:
+ # Resolution succeeded, update domain state
+ if domain in ip_dict:
+ domain_state[domain] = ip_dict[domain]
+ elements += ip_dict[domain]
+ # Resolution failed, use previous domain state
+ elif domain in domain_state:
+ elements += domain_state[domain]
+
+ # Resolve successful
+ if elements:
+ nft_update_set_elements(set_name, elements)
+ time.sleep(timeout)
diff --git a/src/migration-scripts/policy/2-to-3 b/src/migration-scripts/policy/2-to-3
new file mode 100755
index 000000000..84cb1ff4a
--- /dev/null
+++ b/src/migration-scripts/policy/2-to-3
@@ -0,0 +1,58 @@
+#!/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/>.
+
+# T3976: change cli
+# from: set policy route-map FOO rule 10 match ipv6 nexthop 'h:h:h:h:h:h:h:h'
+# to: set policy route-map FOO rule 10 match ipv6 nexthop address 'h:h:h:h:h:h:h:h'
+
+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 = ['policy', 'route-map']
+config = ConfigTree(config_file)
+
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+
+for route_map in config.list_nodes(base):
+ if not config.exists(base + [route_map, 'rule']):
+ continue
+ for rule in config.list_nodes(base + [route_map, 'rule']):
+ base_rule = base + [route_map, 'rule', rule]
+
+ if config.exists(base_rule + ['match', 'ipv6', 'nexthop']):
+ tmp = config.return_value(base_rule + ['match', 'ipv6', 'nexthop'])
+ config.delete(base_rule + ['match', 'ipv6', 'nexthop'])
+ config.set(base_rule + ['match', 'ipv6', 'nexthop', 'address'], value=tmp)
+
+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) \ No newline at end of file
diff --git a/src/migration-scripts/system/23-to-24 b/src/migration-scripts/system/23-to-24
index 5ea71d51a..97fe82462 100755
--- a/src/migration-scripts/system/23-to-24
+++ b/src/migration-scripts/system/23-to-24
@@ -20,6 +20,7 @@ from ipaddress import ip_interface
from ipaddress import ip_address
from sys import exit, argv
from vyos.configtree import ConfigTree
+from vyos.template import is_ipv4
if (len(argv) < 1):
print("Must specify file name!")
@@ -37,6 +38,9 @@ def fixup_cli(config, path, interface):
if config.exists(path + ['address']):
for address in config.return_values(path + ['address']):
tmp = ip_interface(address)
+ # ARP is only available for IPv4 ;-)
+ if not is_ipv4(tmp):
+ continue
if ip_address(host) in tmp.network.hosts():
mac = config.return_value(tmp_base + [host, 'hwaddr'])
iface_path = ['protocols', 'static', 'arp', 'interface']
diff --git a/src/op_mode/pki.py b/src/op_mode/pki.py
index bc7813052..1e78c3a03 100755
--- a/src/op_mode/pki.py
+++ b/src/op_mode/pki.py
@@ -17,6 +17,7 @@
import argparse
import ipaddress
import os
+import re
import sys
import tabulate
@@ -30,7 +31,8 @@ from vyos.pki import encode_certificate, encode_public_key, encode_private_key,
from vyos.pki import create_certificate, create_certificate_request, create_certificate_revocation_list
from vyos.pki import create_private_key
from vyos.pki import create_dh_parameters
-from vyos.pki import load_certificate, load_certificate_request, load_private_key, load_crl
+from vyos.pki import load_certificate, load_certificate_request, load_private_key
+from vyos.pki import load_crl, load_dh_parameters, load_public_key
from vyos.pki import verify_certificate
from vyos.xml import defaults
from vyos.util import ask_input, ask_yes_no
@@ -183,13 +185,13 @@ def install_ssh_key(name, public_key, private_key, passphrase=None):
])
print(encode_private_key(private_key, encoding='PEM', key_format='OpenSSH', passphrase=passphrase))
-def install_keypair(name, key_type, private_key=None, public_key=None, passphrase=None):
+def install_keypair(name, key_type, private_key=None, public_key=None, passphrase=None, prompt=True):
# Show/install conf commands for key-pair
config_paths = []
if public_key:
- install_public_key = ask_yes_no('Do you want to install the public key?', default=True)
+ install_public_key = not prompt or ask_yes_no('Do you want to install the public key?', default=True)
public_key_pem = encode_public_key(public_key)
if install_public_key:
@@ -200,7 +202,7 @@ def install_keypair(name, key_type, private_key=None, public_key=None, passphras
print(public_key_pem)
if private_key:
- install_private_key = ask_yes_no('Do you want to install the private key?', default=True)
+ install_private_key = not prompt or ask_yes_no('Do you want to install the private key?', default=True)
private_key_pem = encode_private_key(private_key, passphrase=passphrase)
if install_private_key:
@@ -214,6 +216,13 @@ def install_keypair(name, key_type, private_key=None, public_key=None, passphras
install_into_config(conf, config_paths)
+def install_openvpn_key(name, key_data, key_version='1'):
+ config_paths = [
+ f"pki openvpn shared-secret {name} key '{key_data}'",
+ f"pki openvpn shared-secret {name} version '{key_version}'"
+ ]
+ install_into_config(conf, config_paths)
+
def install_wireguard_key(interface, private_key, public_key):
# Show conf commands for installing wireguard key pairs
from vyos.ifconfig import Section
@@ -640,15 +649,11 @@ def generate_openvpn_key(name, install=False, file=False):
key_data = "".join(key_lines[1:-1]) # Remove wrapper tags and line endings
key_version = '1'
- import re
version_search = re.search(r'BEGIN OpenVPN Static key V(\d+)', result) # Future-proofing (hopefully)
if version_search:
key_version = version_search[1]
- base = f"set pki openvpn shared-secret {name}"
- print("Configure mode commands to install OpenVPN key:")
- print(f"{base} key '{key_data}'")
- print(f"{base} version '{key_version}'")
+ install_openvpn_key(name, key_data, key_version)
if file:
write_file(f'{name}.key', result)
@@ -670,6 +675,167 @@ def generate_wireguard_psk(interface=None, peer=None, install=False):
else:
print(f'Pre-shared key: {psk}')
+# Import functions
+def import_ca_certificate(name, path=None, key_path=None):
+ if path:
+ if not os.path.exists(path):
+ print(f'File not found: {path}')
+ return
+
+ cert = None
+
+ with open(path) as f:
+ cert_data = f.read()
+ cert = load_certificate(cert_data, wrap_tags=False)
+
+ if not cert:
+ print(f'Invalid certificate: {path}')
+ return
+
+ install_certificate(name, cert, is_ca=True)
+
+ if key_path:
+ if not os.path.exists(key_path):
+ print(f'File not found: {key_path}')
+ return
+
+ key = None
+ passphrase = ask_input('Enter private key passphrase: ') or None
+
+ with open(key_path) as f:
+ key_data = f.read()
+ key = load_private_key(key_data, passphrase=passphrase, wrap_tags=False)
+
+ if not key:
+ print(f'Invalid private key or passphrase: {path}')
+ return
+
+ install_certificate(name, private_key=key, is_ca=True)
+
+def import_certificate(name, path=None, key_path=None):
+ if path:
+ if not os.path.exists(path):
+ print(f'File not found: {path}')
+ return
+
+ cert = None
+
+ with open(path) as f:
+ cert_data = f.read()
+ cert = load_certificate(cert_data, wrap_tags=False)
+
+ if not cert:
+ print(f'Invalid certificate: {path}')
+ return
+
+ install_certificate(name, cert, is_ca=False)
+
+ if key_path:
+ if not os.path.exists(key_path):
+ print(f'File not found: {key_path}')
+ return
+
+ key = None
+ passphrase = ask_input('Enter private key passphrase: ') or None
+
+ with open(key_path) as f:
+ key_data = f.read()
+ key = load_private_key(key_data, passphrase=passphrase, wrap_tags=False)
+
+ if not key:
+ print(f'Invalid private key or passphrase: {path}')
+ return
+
+ install_certificate(name, private_key=key, is_ca=False)
+
+def import_crl(name, path):
+ if not os.path.exists(path):
+ print(f'File not found: {path}')
+ return
+
+ crl = None
+
+ with open(path) as f:
+ crl_data = f.read()
+ crl = load_crl(crl_data, wrap_tags=False)
+
+ if not crl:
+ print(f'Invalid certificate: {path}')
+ return
+
+ install_crl(name, crl)
+
+def import_dh_parameters(name, path):
+ if not os.path.exists(path):
+ print(f'File not found: {path}')
+ return
+
+ dh = None
+
+ with open(path) as f:
+ dh_data = f.read()
+ dh = load_dh_parameters(dh_data, wrap_tags=False)
+
+ if not dh:
+ print(f'Invalid DH parameters: {path}')
+ return
+
+ install_dh_parameters(name, dh)
+
+def import_keypair(name, path=None, key_path=None):
+ if path:
+ if not os.path.exists(path):
+ print(f'File not found: {path}')
+ return
+
+ key = None
+
+ with open(path) as f:
+ key_data = f.read()
+ key = load_public_key(key_data, wrap_tags=False)
+
+ if not key:
+ print(f'Invalid public key: {path}')
+ return
+
+ install_keypair(name, None, public_key=key, prompt=False)
+
+ if key_path:
+ if not os.path.exists(key_path):
+ print(f'File not found: {key_path}')
+ return
+
+ key = None
+ passphrase = ask_input('Enter private key passphrase: ') or None
+
+ with open(key_path) as f:
+ key_data = f.read()
+ key = load_private_key(key_data, passphrase=passphrase, wrap_tags=False)
+
+ if not key:
+ print(f'Invalid private key or passphrase: {path}')
+ return
+
+ install_keypair(name, None, private_key=key, prompt=False)
+
+def import_openvpn_secret(name, path):
+ if not os.path.exists(path):
+ print(f'File not found: {path}')
+ return
+
+ key_data = None
+ key_version = '1'
+
+ with open(path) as f:
+ key_lines = f.read().split("\n")
+ key_data = "".join(key_lines[1:-1]) # Remove wrapper tags and line endings
+
+ version_search = re.search(r'BEGIN OpenVPN Static key V(\d+)', key_lines[0]) # Future-proofing (hopefully)
+ if version_search:
+ key_version = version_search[1]
+
+ install_openvpn_key(name, key_data, key_version)
+
# Show functions
def show_certificate_authority(name=None):
headers = ['Name', 'Subject', 'Issuer CN', 'Issued', 'Expiry', 'Private Key', 'Parent']
@@ -799,6 +965,9 @@ if __name__ == '__main__':
parser.add_argument('--file', help='Write generated keys into specified filename', action='store_true')
parser.add_argument('--install', help='Install generated keys into running-config', action='store_true')
+ parser.add_argument('--filename', help='Write certificate into specified filename', action='store')
+ parser.add_argument('--key-filename', help='Write key into specified filename', action='store')
+
args = parser.parse_args()
try:
@@ -840,7 +1009,19 @@ if __name__ == '__main__':
generate_wireguard_key(args.interface, install=args.install)
if args.psk:
generate_wireguard_psk(args.interface, peer=args.peer, install=args.install)
-
+ elif args.action == 'import':
+ if args.ca:
+ import_ca_certificate(args.ca, path=args.filename, key_path=args.key_filename)
+ elif args.certificate:
+ import_certificate(args.certificate, path=args.filename, key_path=args.key_filename)
+ elif args.crl:
+ import_crl(args.crl, args.filename)
+ elif args.dh:
+ import_dh_parameters(args.dh, args.filename)
+ elif args.keypair:
+ import_keypair(args.keypair, path=args.filename, key_path=args.key_filename)
+ elif args.openvpn:
+ import_openvpn_secret(args.openvpn, args.filename)
elif args.action == 'show':
if args.ca:
ca_name = None if args.ca == 'all' else args.ca
diff --git a/src/op_mode/show_neigh.py b/src/op_mode/show_neigh.py
index 94e745493..d874bd544 100755
--- a/src/op_mode/show_neigh.py
+++ b/src/op_mode/show_neigh.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020 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,83 +14,89 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-#ip -j -f inet neigh list | jq
-#[
- #{
- #"dst": "192.168.101.8",
- #"dev": "enp0s25",
- #"lladdr": "78:d2:94:72:77:7e",
- #"state": [
- #"STALE"
- #]
- #},
- #{
- #"dst": "192.168.101.185",
- #"dev": "enp0s25",
- #"lladdr": "34:46:ec:76:f8:9b",
- #"state": [
- #"STALE"
- #]
- #},
- #{
- #"dst": "192.168.101.225",
- #"dev": "enp0s25",
- #"lladdr": "c2:cb:fa:bf:a0:35",
- #"state": [
- #"STALE"
- #]
- #},
- #{
- #"dst": "192.168.101.1",
- #"dev": "enp0s25",
- #"lladdr": "00:98:2b:f8:3f:11",
- #"state": [
- #"REACHABLE"
- #]
- #},
- #{
- #"dst": "192.168.101.181",
- #"dev": "enp0s25",
- #"lladdr": "d8:9b:3b:d5:88:22",
- #"state": [
- #"STALE"
- #]
- #}
-#]
+# Sample output of `ip --json neigh list`:
+#
+# [
+# {
+# "dst": "192.168.1.1",
+# "dev": "eth0", # Missing if `dev ...` option is used
+# "lladdr": "00:aa:bb:cc:dd:ee", # May be missing for failed entries
+# "state": [
+# "REACHABLE"
+# ]
+# },
+# ]
import sys
-import argparse
-import json
-from vyos.util import cmd
-
-def main():
- #parese args
- parser = argparse.ArgumentParser()
- parser.add_argument('--family', help='Protocol family', required=True)
- args = parser.parse_args()
-
- neigh_raw_json = cmd(f'ip -j -f {args.family} neigh list')
- neigh_raw_json = neigh_raw_json.lower()
- neigh_json = json.loads(neigh_raw_json)
-
- format_neigh = '%-50s %-10s %-20s %s'
- print(format_neigh % ("IP Address", "Device", "State", "LLADDR"))
- print(format_neigh % ("----------", "------", "-----", "------"))
-
- if neigh_json is not None:
- for neigh_item in neigh_json:
- dev = neigh_item['dev']
- dst = neigh_item['dst']
- lladdr = neigh_item['lladdr'] if 'lladdr' in neigh_item else ''
- state = neigh_item['state']
-
- i = 0
- for state_item in state:
- if i == 0:
- print(format_neigh % (dst, dev, state_item, lladdr))
- else:
- print(format_neigh % ('', '', state_item, ''))
- i+=1
-
+
+
+def get_raw_data(family, device=None, state=None):
+ from json import loads
+ from vyos.util import cmd
+
+ if device:
+ device = f"dev {device}"
+ else:
+ device = ""
+
+ if state:
+ state = f"nud {state}"
+ else:
+ state = ""
+
+ neigh_cmd = f"ip --family {family} --json neighbor list {device} {state}"
+
+ data = loads(cmd(neigh_cmd))
+
+ return data
+
+def get_formatted_output(family, device=None, state=None):
+ from tabulate import tabulate
+
+ def entry_to_list(e, intf=None):
+ dst = e["dst"]
+
+ # State is always a list in the iproute2 output
+ state = ", ".join(e["state"])
+
+ # Link layer address is absent from e.g. FAILED entries
+ if "lladdr" in e:
+ lladdr = e["lladdr"]
+ else:
+ lladdr = None
+
+ # Device field is absent from outputs of `ip neigh list dev ...`
+ if "dev" in e:
+ dev = e["dev"]
+ elif device:
+ dev = device
+ else:
+ raise ValueError("interface is not defined")
+
+ return [dst, dev, lladdr, state]
+
+ neighs = get_raw_data(family, device=device, state=state)
+ neighs = map(entry_to_list, neighs)
+
+ headers = ["Address", "Interface", "Link layer address", "State"]
+ return tabulate(neighs, headers)
+
if __name__ == '__main__':
- main()
+ from argparse import ArgumentParser
+
+ parser = ArgumentParser()
+ parser.add_argument("-f", "--family", type=str, default="inet", help="Address family")
+ parser.add_argument("-i", "--interface", type=str, help="Network interface")
+ parser.add_argument("-s", "--state", type=str, help="Neighbor table entry state")
+
+ args = parser.parse_args()
+
+ if args.state:
+ if args.state not in ["reachable", "failed", "stale", "permanent"]:
+ raise ValueError(f"""Incorrect state "{args.state}"! Must be one of: reachable, stale, failed, permanent""")
+
+ try:
+ print(get_formatted_output(args.family, device=args.interface, state=args.state))
+ except ValueError as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/system/vyos-event-handler.py b/src/system/vyos-event-handler.py
new file mode 100755
index 000000000..691f674b2
--- /dev/null
+++ b/src/system/vyos-event-handler.py
@@ -0,0 +1,160 @@
+#!/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 select
+import re
+import json
+from os import getpid, environ
+from pathlib import Path
+from signal import signal, SIGTERM, SIGINT
+from systemd import journal
+from sys import exit
+from vyos.util import run, dict_search
+
+# Identify this script
+my_pid = getpid()
+my_name = Path(__file__).stem
+
+
+# handle termination signal
+def handle_signal(signal_type, frame):
+ if signal_type == SIGTERM:
+ journal.send('Received SIGTERM signal, stopping normally',
+ SYSLOG_IDENTIFIER=my_name)
+ if signal_type == SIGINT:
+ journal.send('Received SIGINT signal, stopping normally',
+ SYSLOG_IDENTIFIER=my_name)
+ exit(0)
+
+
+# Class for analyzing and process messages
+class Analyzer:
+ # Initialize settings
+ def __init__(self, config: dict) -> None:
+ self.config = {}
+ # Prepare compiled regex objects
+ for event_id, event_config in config.items():
+ script = dict_search('script.path', event_config)
+ # Check for arguments
+ if dict_search('script.arguments', event_config):
+ script_arguments = dict_search('script.arguments', event_config)
+ script = f'{script} {script_arguments}'
+ # Prepare environment
+ environment = environ
+ # Check for additional environment options
+ if dict_search('script.environment', event_config):
+ for env_variable, env_value in dict_search(
+ 'script.environment', event_config).items():
+ environment[env_variable] = env_value.get('value')
+ # Create final config dictionary
+ pattern_raw = event_config['filter']['pattern']
+ pattern_compiled = re.compile(
+ rf'{event_config["filter"]["pattern"]}')
+ pattern_config = {
+ pattern_compiled: {
+ 'pattern_raw':
+ pattern_raw,
+ 'syslog_id':
+ dict_search('filter.syslog_identifier', event_config),
+ 'pattern_script': {
+ 'path': script,
+ 'environment': environment
+ }
+ }
+ }
+ self.config.update(pattern_config)
+
+ # Execute script safely
+ def script_run(self, pattern: str, script_path: str,
+ script_env: dict) -> None:
+ try:
+ run(script_path, env=script_env)
+ journal.send(
+ f'Pattern found: "{pattern}", script executed: "{script_path}"',
+ SYSLOG_IDENTIFIER=my_name)
+ except Exception as err:
+ journal.send(
+ f'Pattern found: "{pattern}", failed to execute script "{script_path}": {err}',
+ SYSLOG_IDENTIFIER=my_name)
+
+ # Analyze a message
+ def process_message(self, message: dict) -> None:
+ for pattern_compiled, pattern_config in self.config.items():
+ # Check if syslog id is presented in config and matches
+ syslog_id = pattern_config.get('syslog_id')
+ if syslog_id and message['SYSLOG_IDENTIFIER'] != syslog_id:
+ continue
+ if pattern_compiled.fullmatch(message['MESSAGE']):
+ # Add message to environment variables
+ pattern_config['pattern_script']['environment'][
+ 'message'] = message['MESSAGE']
+ # Run script
+ self.script_run(
+ pattern=pattern_config['pattern_raw'],
+ script_path=pattern_config['pattern_script']['path'],
+ script_env=pattern_config['pattern_script']['environment'])
+
+
+if __name__ == '__main__':
+ # Parse command arguments and get config
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-c',
+ '--config',
+ action='store',
+ help='Path to even-handler configuration',
+ required=True,
+ type=Path)
+
+ args = parser.parse_args()
+ try:
+ config_path = Path(args.config)
+ config = json.loads(config_path.read_text())
+ # Create an object for analazyng messages
+ analyzer = Analyzer(config)
+ except Exception as err:
+ print(
+ f'Configuration file "{config_path}" does not exist or malformed: {err}'
+ )
+ exit(1)
+
+ # Prepare for proper exitting
+ signal(SIGTERM, handle_signal)
+ signal(SIGINT, handle_signal)
+
+ # Set up journal connection
+ data = journal.Reader()
+ data.seek_tail()
+ data.get_previous()
+ p = select.poll()
+ p.register(data, data.get_events())
+
+ journal.send(f'Started with configuration: {config}',
+ SYSLOG_IDENTIFIER=my_name)
+
+ while p.poll():
+ if data.process() != journal.APPEND:
+ continue
+ for entry in data:
+ message = entry['MESSAGE']
+ pid = entry['_PID']
+ # Skip empty messages and messages from this process
+ if message and pid != my_pid:
+ try:
+ analyzer.process_message(entry)
+ except Exception as err:
+ journal.send(f'Unable to process message: {err}',
+ SYSLOG_IDENTIFIER=my_name)
diff --git a/src/systemd/vyos-domain-group-resolve.service b/src/systemd/vyos-domain-group-resolve.service
new file mode 100644
index 000000000..29628fddb
--- /dev/null
+++ b/src/systemd/vyos-domain-group-resolve.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=VyOS firewall domain-group resolver
+After=vyos-router.service
+
+[Service]
+Type=simple
+Restart=always
+ExecStart=/usr/bin/python3 /usr/libexec/vyos/vyos-domain-group-resolve.py
+
+[Install]
+WantedBy=multi-user.target
diff --git a/src/systemd/vyos-event-handler.service b/src/systemd/vyos-event-handler.service
new file mode 100644
index 000000000..6afe4f95b
--- /dev/null
+++ b/src/systemd/vyos-event-handler.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=VyOS event handler
+After=network.target vyos-router.service
+
+[Service]
+Type=simple
+Restart=always
+ExecStart=/usr/bin/python3 /usr/libexec/vyos/system/vyos-event-handler.py --config /run/vyos-event-handler.conf
+
+[Install]
+WantedBy=multi-user.target