summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rwxr-xr-xsrc/conf_mode/container.py173
-rwxr-xr-xsrc/conf_mode/firewall.py85
-rwxr-xr-xsrc/conf_mode/http-api.py10
-rwxr-xr-xsrc/conf_mode/interfaces-bonding.py2
-rwxr-xr-xsrc/conf_mode/interfaces-ethernet.py2
-rwxr-xr-xsrc/conf_mode/interfaces-pppoe.py1
-rwxr-xr-xsrc/conf_mode/interfaces-sstpc.py142
-rwxr-xr-xsrc/conf_mode/interfaces-virtual-ethernet.py114
-rwxr-xr-xsrc/conf_mode/interfaces-wireguard.py7
-rwxr-xr-xsrc/conf_mode/nat.py85
-rwxr-xr-xsrc/conf_mode/pki.py71
-rwxr-xr-xsrc/conf_mode/policy-route-interface.py132
-rwxr-xr-xsrc/conf_mode/policy-route.py106
-rwxr-xr-xsrc/conf_mode/policy.py5
-rwxr-xr-xsrc/conf_mode/protocols_failover.py121
-rwxr-xr-xsrc/conf_mode/protocols_mpls.py5
-rwxr-xr-xsrc/conf_mode/service_pppoe-server.py13
-rwxr-xr-xsrc/conf_mode/service_webproxy.py100
-rwxr-xr-xsrc/conf_mode/vpn_ipsec.py22
-rwxr-xr-xsrc/conf_mode/vpn_l2tp.py39
-rwxr-xr-xsrc/conf_mode/vpn_openconnect.py6
-rw-r--r--src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf4
-rw-r--r--src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup4
-rw-r--r--src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook6
-rwxr-xr-xsrc/etc/ppp/ip-down.d/98-vyos-pppoe-cleanup-nameservers15
-rwxr-xr-xsrc/etc/ppp/ip-up.d/96-vyos-sstpc-callback49
-rwxr-xr-xsrc/etc/ppp/ip-up.d/98-vyos-pppoe-setup-nameservers24
-rwxr-xr-xsrc/helpers/system-versions-foot.py21
-rwxr-xr-xsrc/helpers/vyos-domain-group-resolve.py60
-rwxr-xr-xsrc/helpers/vyos-domain-resolver.py183
-rwxr-xr-xsrc/helpers/vyos-failover.py184
-rwxr-xr-xsrc/migration-scripts/https/3-to-453
-rwxr-xr-xsrc/migration-scripts/policy/4-to-592
-rwxr-xr-xsrc/op_mode/accelppp.py133
-rwxr-xr-xsrc/op_mode/bgp.py120
-rwxr-xr-xsrc/op_mode/bridge.py6
-rwxr-xr-xsrc/op_mode/connect_disconnect.py4
-rwxr-xr-xsrc/op_mode/dhcp.py278
-rwxr-xr-xsrc/op_mode/dns.py4
-rwxr-xr-xsrc/op_mode/firewall.py2
-rwxr-xr-xsrc/op_mode/generate_ipsec_debug_archive.py89
-rwxr-xr-xsrc/op_mode/generate_ipsec_debug_archive.sh36
-rwxr-xr-xsrc/op_mode/ipsec.py321
-rwxr-xr-xsrc/op_mode/log.py94
-rwxr-xr-xsrc/op_mode/memory.py27
-rwxr-xr-xsrc/op_mode/nat.py22
-rwxr-xr-xsrc/op_mode/openconnect.py14
-rwxr-xr-xsrc/op_mode/openvpn.py220
-rwxr-xr-xsrc/op_mode/ping.py83
-rwxr-xr-xsrc/op_mode/policy_route.py42
-rwxr-xr-xsrc/op_mode/route.py19
-rwxr-xr-xsrc/op_mode/show_openvpn.py6
-rwxr-xr-xsrc/op_mode/storage.py18
-rwxr-xr-xsrc/op_mode/traceroute.py85
-rwxr-xr-xsrc/op_mode/vrf.py6
-rwxr-xr-xsrc/op_mode/webproxy_update_blacklist.sh29
-rw-r--r--src/services/api/graphql/__init__.py0
-rw-r--r--src/services/api/graphql/bindings.py14
-rw-r--r--src/services/api/graphql/generate/composite_function.py11
-rw-r--r--src/services/api/graphql/generate/config_session_function.py (renamed from src/services/api/graphql/utils/config_session_function.py)0
-rwxr-xr-xsrc/services/api/graphql/generate/schema_from_composite.py165
-rwxr-xr-xsrc/services/api/graphql/generate/schema_from_config_session.py (renamed from src/services/api/graphql/utils/schema_from_config_session.py)60
-rwxr-xr-xsrc/services/api/graphql/generate/schema_from_op_mode.py (renamed from src/services/api/graphql/utils/schema_from_op_mode.py)57
-rw-r--r--src/services/api/graphql/graphql/auth_token_mutation.py49
-rw-r--r--src/services/api/graphql/graphql/directives.py15
-rw-r--r--src/services/api/graphql/graphql/mutations.py68
-rw-r--r--src/services/api/graphql/graphql/queries.py69
-rw-r--r--src/services/api/graphql/graphql/schema/auth_token.graphql19
-rw-r--r--src/services/api/graphql/graphql/schema/configsession.graphql115
-rw-r--r--src/services/api/graphql/graphql/schema/schema.graphql8
-rw-r--r--src/services/api/graphql/graphql/schema/system_status.graphql18
-rw-r--r--src/services/api/graphql/libs/key_auth.py (renamed from src/services/api/graphql/key_auth.py)2
-rw-r--r--src/services/api/graphql/libs/op_mode.py (renamed from src/services/api/graphql/utils/util.py)13
-rw-r--r--src/services/api/graphql/libs/token_auth.py71
-rwxr-xr-xsrc/services/api/graphql/session/composite/system_status.py2
-rw-r--r--src/services/api/graphql/session/errors/op_mode_errors.py6
-rw-r--r--src/services/api/graphql/session/session.py5
-rwxr-xr-xsrc/services/vyos-hostsd3
-rwxr-xr-xsrc/services/vyos-http-api-server40
-rwxr-xr-xsrc/system/keepalived-fifo.py8
-rw-r--r--src/systemd/vyos-domain-group-resolve.service11
-rw-r--r--src/systemd/vyos-domain-resolver.service13
-rw-r--r--src/tests/test_op_mode.py65
-rw-r--r--src/tests/test_util.py14
-rwxr-xr-xsrc/validators/allowed-vlan19
-rwxr-xr-xsrc/validators/dotted-decimal33
-rwxr-xr-xsrc/validators/file-exists61
-rwxr-xr-xsrc/validators/fqdn29
-rwxr-xr-xsrc/validators/interface-name34
-rwxr-xr-xsrc/validators/mac-address29
-rwxr-xr-xsrc/validators/mac-address-exclude2
-rwxr-xr-xsrc/validators/mac-address-firewall27
-rwxr-xr-xsrc/validators/tcp-flag17
-rw-r--r--src/xdp/common/common.mk2
-rw-r--r--src/xdp/common/common_user_bpf_xdp.c2
95 files changed, 3571 insertions, 1199 deletions
diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py
index ac3dc536b..8efeaed54 100755
--- a/src/conf_mode/container.py
+++ b/src/conf_mode/container.py
@@ -40,20 +40,7 @@ airbag.enable()
config_containers_registry = '/etc/containers/registries.conf'
config_containers_storage = '/etc/containers/storage.conf'
-
-def _run_rerun(container_cmd):
- counter = 0
- while True:
- if counter >= 10:
- break
- try:
- _cmd(container_cmd)
- break
- except:
- counter = counter +1
- sleep(0.5)
-
- return None
+systemd_unit_path = '/run/systemd/system'
def _cmd(command):
if os.path.exists('/tmp/vyos.container.debug'):
@@ -122,7 +109,7 @@ def verify(container):
# of image upgrade and deletion.
image = container_config['image']
if run(f'podman image exists {image}') != 0:
- Warning(f'Image "{image}" used in contianer "{name}" does not exist '\
+ Warning(f'Image "{image}" used in container "{name}" does not exist '\
f'locally. Please use "add container image {image}" to add it '\
f'to the system! Container "{name}" will not be started!')
@@ -136,9 +123,6 @@ def verify(container):
raise ConfigError(f'Container network "{network_name}" does not exist!')
if 'address' in container_config['network'][network_name]:
- if 'network' not in container_config:
- raise ConfigError(f'Can not use "address" without "network" for container "{name}"!')
-
address = container_config['network'][network_name]['address']
network = None
if is_ipv4(address):
@@ -220,6 +204,72 @@ def verify(container):
return None
+def generate_run_arguments(name, container_config):
+ image = container_config['image']
+ memory = container_config['memory']
+ shared_memory = container_config['shared_memory']
+ restart = container_config['restart']
+
+ # Add capability options. Should be in uppercase
+ cap_add = ''
+ if 'cap_add' in container_config:
+ for c in container_config['cap_add']:
+ c = c.upper()
+ 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:
+ for k, v in container_config['environment'].items():
+ env_opt += f" -e \"{k}={v['value']}\""
+
+ # Publish ports
+ port = ''
+ if 'port' in container_config:
+ protocol = ''
+ for portmap in container_config['port']:
+ if 'protocol' in container_config['port'][portmap]:
+ protocol = container_config['port'][portmap]['protocol']
+ protocol = f'/{protocol}'
+ else:
+ protocol = '/tcp'
+ sport = container_config['port'][portmap]['source']
+ dport = container_config['port'][portmap]['destination']
+ port += f' -p {sport}:{dport}{protocol}'
+
+ # Bind volume
+ volume = ''
+ if 'volume' in container_config:
+ for vol, vol_config in container_config['volume'].items():
+ svol = vol_config['source']
+ dvol = vol_config['destination']
+ volume += f' -v {svol}:{dvol}'
+
+ container_base_cmd = f'--detach --interactive --tty --replace {cap_add} ' \
+ f'--memory {memory}m --shm-size {shared_memory}m --memory-swap 0 --restart {restart} ' \
+ f'--name {name} {device} {port} {volume} {env_opt}'
+
+ if 'allow_host_networks' in container_config:
+ return f'{container_base_cmd} --net host {image}'
+
+ ip_param = ''
+ networks = ",".join(container_config['network'])
+ for network in container_config['network']:
+ if 'address' in container_config['network'][network]:
+ address = container_config['network'][network]['address']
+ ip_param = f'--ip {address}'
+
+ return f'{container_base_cmd} --net {networks} {ip_param} {image}'
+
def generate(container):
# bail out early - looks like removal from running config
if not container:
@@ -263,6 +313,15 @@ def generate(container):
render(config_containers_registry, 'container/registries.conf.j2', container)
render(config_containers_storage, 'container/storage.conf.j2', container)
+ if 'name' in container:
+ for name, container_config in container['name'].items():
+ if 'disable' in container_config:
+ continue
+
+ file_path = os.path.join(systemd_unit_path, f'vyos-container-{name}.service')
+ run_args = generate_run_arguments(name, container_config)
+ render(file_path, 'container/systemd-unit.j2', {'name': name, 'run_args': run_args})
+
return None
def apply(container):
@@ -270,8 +329,12 @@ def apply(container):
# Option "--force" allows to delete containers with any status
if 'container_remove' in container:
for name in container['container_remove']:
- call(f'podman stop --time 3 {name}')
- call(f'podman rm --force {name}')
+ file_path = os.path.join(systemd_unit_path, f'vyos-container-{name}.service')
+ call(f'systemctl stop vyos-container-{name}.service')
+ if os.path.exists(file_path):
+ os.unlink(file_path)
+
+ call('systemctl daemon-reload')
# Delete old networks if needed
if 'network_remove' in container:
@@ -282,6 +345,7 @@ def apply(container):
os.unlink(tmp)
# Add container
+ disabled_new = False
if 'name' in container:
for name, container_config in container['name'].items():
image = container_config['image']
@@ -295,70 +359,17 @@ def apply(container):
# check if there is a container by that name running
tmp = _cmd('podman ps -a --format "{{.Names}}"')
if name in tmp:
- _cmd(f'podman stop --time 3 {name}')
- _cmd(f'podman rm --force {name}')
+ file_path = os.path.join(systemd_unit_path, f'vyos-container-{name}.service')
+ call(f'systemctl stop vyos-container-{name}.service')
+ if os.path.exists(file_path):
+ disabled_new = True
+ os.unlink(file_path)
continue
- memory = container_config['memory']
- restart = container_config['restart']
-
- # Add capability options. Should be in uppercase
- cap_add = ''
- if 'cap_add' in container_config:
- for c in container_config['cap_add']:
- c = c.upper()
- 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:
- for k, v in container_config['environment'].items():
- env_opt += f" -e \"{k}={v['value']}\""
-
- # Publish ports
- port = ''
- if 'port' in container_config:
- protocol = ''
- for portmap in container_config['port']:
- if 'protocol' in container_config['port'][portmap]:
- protocol = container_config['port'][portmap]['protocol']
- protocol = f'/{protocol}'
- else:
- protocol = '/tcp'
- sport = container_config['port'][portmap]['source']
- dport = container_config['port'][portmap]['destination']
- port += f' -p {sport}:{dport}{protocol}'
-
- # Bind volume
- volume = ''
- if 'volume' in container_config:
- for vol, vol_config in container_config['volume'].items():
- svol = vol_config['source']
- dvol = vol_config['destination']
- volume += f' -v {svol}:{dvol}'
-
- container_base_cmd = f'podman run --detach --interactive --tty --replace {cap_add} ' \
- f'--memory {memory}m --memory-swap 0 --restart {restart} ' \
- f'--name {name} {device} {port} {volume} {env_opt}'
- if 'allow_host_networks' in container_config:
- _run_rerun(f'{container_base_cmd} --net host {image}')
- else:
- for network in container_config['network']:
- ipparam = ''
- if 'address' in container_config['network'][network]:
- address = container_config['network'][network]['address']
- ipparam = f'--ip {address}'
+ cmd(f'systemctl restart vyos-container-{name}.service')
- _run_rerun(f'{container_base_cmd} --net {network} {ipparam} {image}')
+ if disabled_new:
+ call('systemctl daemon-reload')
return None
diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py
index cbd9cbe90..f68acfe02 100755
--- a/src/conf_mode/firewall.py
+++ b/src/conf_mode/firewall.py
@@ -26,13 +26,10 @@ 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.configdep import set_dependents, call_dependents
# from vyos.configverify import verify_interface_exists
+from vyos.firewall import fqdn_config_parse
from vyos.firewall import geoip_update
-from vyos.firewall import get_ips_domains_dict
-from vyos.firewall import nft_add_set_elements
-from vyos.firewall import nft_flush_set
-from vyos.firewall import nft_init_set
-from vyos.firewall import nft_update_set_elements
from vyos.template import render
from vyos.util import call
from vyos.util import cmd
@@ -45,7 +42,8 @@ from vyos import ConfigError
from vyos import airbag
airbag.enable()
-policy_route_conf_script = '/usr/libexec/vyos/conf_mode/policy-route.py'
+nat_conf_script = 'nat.py'
+policy_route_conf_script = 'policy-route.py'
nftables_conf = '/run/nftables.conf'
@@ -162,7 +160,10 @@ def get_config(config=None):
for zone in firewall['zone']:
firewall['zone'][zone] = dict_merge(default_values, firewall['zone'][zone])
- firewall['policy_resync'] = bool('group' in firewall or node_changed(conf, base + ['group']))
+ firewall['group_resync'] = bool('group' in firewall or node_changed(conf, base + ['group']))
+ if firewall['group_resync']:
+ # Update nat and policy-route as firewall groups were updated
+ set_dependents('group_resync', conf)
if 'config_trap' in firewall and firewall['config_trap'] == 'enable':
diff = get_config_diff(conf)
@@ -173,6 +174,8 @@ def get_config(config=None):
firewall['geoip_updated'] = geoip_updated(conf, firewall)
+ fqdn_config_parse(firewall)
+
return firewall
def verify_rule(firewall, rule_conf, ipv6):
@@ -232,29 +235,28 @@ def verify_rule(firewall, rule_conf, ipv6):
if side in rule_conf:
side_conf = rule_conf[side]
- if dict_search_args(side_conf, 'geoip', 'country_code'):
- if 'address' in side_conf:
- raise ConfigError('Address and GeoIP cannot both be defined')
-
- if dict_search_args(side_conf, 'group', 'address_group'):
- raise ConfigError('Address-group and GeoIP cannot both be defined')
-
- if dict_search_args(side_conf, 'group', 'network_group'):
- raise ConfigError('Network-group and GeoIP cannot both be defined')
+ if len({'address', 'fqdn', 'geoip'} & set(side_conf)) > 1:
+ raise ConfigError('Only one of address, fqdn or geoip can be specified')
if 'group' in side_conf:
- if {'address_group', 'network_group'} <= set(side_conf['group']):
- raise ConfigError('Only one address-group or network-group can be specified')
+ if len({'address_group', 'network_group', 'domain_group'} & set(side_conf['group'])) > 1:
+ raise ConfigError('Only one address-group, network-group or domain-group can be specified')
for group in valid_groups:
if group in side_conf['group']:
group_name = side_conf['group'][group]
+ fw_group = f'ipv6_{group}' if ipv6 and group in ['address_group', 'network_group'] else group
+ error_group = fw_group.replace("_", "-")
+
+ if group in ['address_group', 'network_group', 'domain_group']:
+ types = [t for t in ['address', 'fqdn', 'geoip'] if t in side_conf]
+ if types:
+ raise ConfigError(f'{error_group} and {types[0]} cannot both be defined')
+
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)
if group_obj is None:
@@ -274,6 +276,8 @@ def verify_nested_group(group_name, group, groups, seen):
if 'include' not in group:
return
+ seen.append(group_name)
+
for g in group['include']:
if g not in groups:
raise ConfigError(f'Nested group "{g}" does not exist')
@@ -281,8 +285,6 @@ def verify_nested_group(group_name, group, groups, seen):
if g in seen:
raise ConfigError(f'Group "{group_name}" has a circular reference')
- seen.append(g)
-
if 'include' in groups[g]:
verify_nested_group(g, groups[g], groups, seen)
@@ -466,42 +468,23 @@ def post_apply_trap(firewall):
cmd(base_cmd + ' '.join(objects))
-def resync_policy_route():
- # Update policy route as firewall groups were updated
- tmp, out = rc_cmd(policy_route_conf_script)
- if tmp > 0:
- Warning(f'Failed to re-apply policy route configuration! {out}')
-
def apply(firewall):
install_result, output = rc_cmd(f'nft -f {nftables_conf}')
if install_result == 1:
raise ConfigError(f'Failed to apply firewall: {output}')
- # set firewall group domain-group xxx
- if 'group' in firewall:
- if 'domain_group' in firewall['group']:
- # T970 Enable a resolver (systemd daemon) that checks
- # domain-group addresses and update entries for domains by timeout
- # If router loaded without internet connection or for synchronization
- call('systemctl restart vyos-domain-group-resolve.service')
- for group, group_config in firewall['group']['domain_group'].items():
- domains = []
- if group_config.get('address') is not None:
- for address in group_config.get('address'):
- domains.append(address)
- # Add elements to domain-group, try to resolve domain => ip
- # and add elements to nft set
- ip_dict = get_ips_domains_dict(domains)
- elements = sum(ip_dict.values(), [])
- nft_init_set(f'D_{group}')
- nft_add_set_elements(f'D_{group}', elements)
- else:
- call('systemctl stop vyos-domain-group-resolve.service')
-
apply_sysfs(firewall)
- if firewall['policy_resync']:
- resync_policy_route()
+ if firewall['group_resync']:
+ call_dependents()
+
+ # T970 Enable a resolver (systemd daemon) that checks
+ # domain-group/fqdn addresses and update entries for domains by timeout
+ # If router loaded without internet connection or for synchronization
+ domain_action = 'stop'
+ if dict_search_args(firewall, 'group', 'domain_group') or firewall['ip_fqdn'] or firewall['ip6_fqdn']:
+ domain_action = 'restart'
+ call(f'systemctl {domain_action} vyos-domain-resolver.service')
if firewall['geoip_updated']:
# Call helper script to Update set contents
diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py
index c196e272b..6328294c1 100755
--- a/src/conf_mode/http-api.py
+++ b/src/conf_mode/http-api.py
@@ -25,6 +25,7 @@ import vyos.defaults
from vyos.config import Config
from vyos.configdict import dict_merge
+from vyos.configdep import set_dependents, call_dependents
from vyos.template import render
from vyos.util import cmd
from vyos.util import call
@@ -61,6 +62,11 @@ def get_config(config=None):
else:
conf = Config()
+ # reset on creation/deletion of 'api' node
+ https_base = ['service', 'https']
+ if conf.exists(https_base):
+ set_dependents("https", conf)
+
base = ['service', 'https', 'api']
if not conf.exists(base):
return None
@@ -86,7 +92,7 @@ def get_config(config=None):
if 'api_keys' in api_dict:
keys_added = True
- if 'gql' in api_dict:
+ if 'graphql' in api_dict:
api_dict = dict_merge(defaults(base), api_dict)
http_api.update(api_dict)
@@ -132,7 +138,7 @@ def apply(http_api):
# Let uvicorn settle before restarting Nginx
sleep(1)
- cmd(f'{vyos_conf_scripts_dir}/https.py', raising=ConfigError)
+ call_dependents()
if __name__ == '__main__':
try:
diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py
index 21cf204fc..b883ebef2 100755
--- a/src/conf_mode/interfaces-bonding.py
+++ b/src/conf_mode/interfaces-bonding.py
@@ -116,7 +116,7 @@ def get_config(config=None):
if dict_search('member.interface', bond):
for interface, interface_config in bond['member']['interface'].items():
# Check if member interface is a new member
- if not conf.exists_effective(['member', 'interface', interface]):
+ if not conf.exists_effective(base + [ifname, 'member', 'interface', interface]):
bond['shutdown_required'] = {}
# Check if member interface is disabled
diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py
index e02841831..b49c945cd 100755
--- a/src/conf_mode/interfaces-ethernet.py
+++ b/src/conf_mode/interfaces-ethernet.py
@@ -175,7 +175,7 @@ def generate(ethernet):
loaded_pki_cert = load_certificate(pki_cert['certificate'])
loaded_ca_certs = {load_certificate(c['certificate'])
- for c in ethernet['pki']['ca'].values()}
+ for c in ethernet['pki']['ca'].values()} if 'ca' in ethernet['pki'] else {}
cert_full_chain = find_chain(loaded_pki_cert, loaded_ca_certs)
diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py
index e2fdc7a42..ee4defa0d 100755
--- a/src/conf_mode/interfaces-pppoe.py
+++ b/src/conf_mode/interfaces-pppoe.py
@@ -23,7 +23,6 @@ from netifaces import interfaces
from vyos.config import Config
from vyos.configdict import get_interface_dict
from vyos.configdict import is_node_changed
-from vyos.configdict import leaf_node_changed
from vyos.configdict import get_pppoe_interfaces
from vyos.configverify import verify_authentication
from vyos.configverify import verify_source_interface
diff --git a/src/conf_mode/interfaces-sstpc.py b/src/conf_mode/interfaces-sstpc.py
new file mode 100755
index 000000000..6b8094c51
--- /dev/null
+++ b/src/conf_mode/interfaces-sstpc.py
@@ -0,0 +1,142 @@
+#!/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 get_interface_dict
+from vyos.configdict import is_node_changed
+from vyos.configverify import verify_authentication
+from vyos.configverify import verify_vrf
+from vyos.ifconfig import SSTPCIf
+from vyos.pki import encode_certificate
+from vyos.pki import find_chain
+from vyos.pki import load_certificate
+from vyos.template import render
+from vyos.util import call
+from vyos.util import dict_search
+from vyos.util import is_systemd_service_running
+from vyos.util import write_file
+from vyos import ConfigError
+from vyos import airbag
+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
+ """
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['interfaces', 'sstpc']
+ ifname, sstpc = get_interface_dict(conf, base)
+
+ # We should only terminate the SSTP client session if critical parameters
+ # change. All parameters that can be changed on-the-fly (like interface
+ # description) should not lead to a reconnect!
+ for options in ['authentication', 'no_peer_dns', 'no_default_route',
+ 'server', 'ssl']:
+ if is_node_changed(conf, base + [ifname, options]):
+ sstpc.update({'shutdown_required': {}})
+ # bail out early - no need to further process other nodes
+ break
+
+ # Load PKI certificates for later processing
+ sstpc['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
+ return sstpc
+
+def verify(sstpc):
+ if 'deleted' in sstpc:
+ return None
+
+ verify_authentication(sstpc)
+ verify_vrf(sstpc)
+
+ if dict_search('ssl.ca_certificate', sstpc) == None:
+ raise ConfigError('Missing mandatory CA certificate!')
+
+ return None
+
+def generate(sstpc):
+ ifname = sstpc['ifname']
+ config_sstpc = f'/etc/ppp/peers/{ifname}'
+
+ sstpc['ca_file_path'] = f'/run/sstpc/{ifname}_ca-cert.pem'
+
+ if 'deleted' in sstpc:
+ for file in [sstpc['ca_file_path'], config_sstpc]:
+ if os.path.exists(file):
+ os.unlink(file)
+ return None
+
+ ca_name = sstpc['ssl']['ca_certificate']
+ pki_ca_cert = sstpc['pki']['ca'][ca_name]
+
+ loaded_ca_cert = load_certificate(pki_ca_cert['certificate'])
+ loaded_ca_certs = {load_certificate(c['certificate'])
+ for c in sstpc['pki']['ca'].values()} if 'ca' in sstpc['pki'] else {}
+
+ ca_full_chain = find_chain(loaded_ca_cert, loaded_ca_certs)
+
+ write_file(sstpc['ca_file_path'], '\n'.join(encode_certificate(c) for c in ca_full_chain))
+ render(config_sstpc, 'sstp-client/peer.j2', sstpc, permission=0o640)
+
+ return None
+
+def apply(sstpc):
+ ifname = sstpc['ifname']
+ if 'deleted' in sstpc or 'disable' in sstpc:
+ if os.path.isdir(f'/sys/class/net/{ifname}'):
+ p = SSTPCIf(ifname)
+ p.remove()
+ call(f'systemctl stop ppp@{ifname}.service')
+ return None
+
+ # reconnect should only be necessary when specific options change,
+ # like server, authentication ... (see get_config() for details)
+ if ((not is_systemd_service_running(f'ppp@{ifname}.service')) or
+ 'shutdown_required' in sstpc):
+
+ # cleanup system (e.g. FRR routes first)
+ if os.path.isdir(f'/sys/class/net/{ifname}'):
+ p = SSTPCIf(ifname)
+ p.remove()
+
+ call(f'systemctl restart ppp@{ifname}.service')
+ # When interface comes "live" a hook is called:
+ # /etc/ppp/ip-up.d/96-vyos-sstpc-callback
+ # which triggers SSTPCIf.update()
+ else:
+ if os.path.isdir(f'/sys/class/net/{ifname}'):
+ p = SSTPCIf(ifname)
+ p.update(sstpc)
+
+ 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/interfaces-virtual-ethernet.py b/src/conf_mode/interfaces-virtual-ethernet.py
new file mode 100755
index 000000000..8efe89c41
--- /dev/null
+++ b/src/conf_mode/interfaces-virtual-ethernet.py
@@ -0,0 +1,114 @@
+#!/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 netifaces import interfaces
+from vyos import ConfigError
+from vyos import airbag
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configverify import verify_address
+from vyos.configverify import verify_bridge_delete
+from vyos.configverify import verify_vrf
+from vyos.ifconfig import VethIf
+
+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
+ """
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['interfaces', 'virtual-ethernet']
+ ifname, veth = get_interface_dict(conf, base)
+
+ # We need to know all other veth related interfaces as veth requires a 1:1
+ # mapping for the peer-names. The Linux kernel automatically creates both
+ # interfaces, the local one and the peer-name, but VyOS also needs a peer
+ # interfaces configrued on the CLI so we can assign proper IP addresses etc.
+ veth['other_interfaces'] = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+
+ return veth
+
+
+def verify(veth):
+ if 'deleted' in veth:
+ verify_bridge_delete(veth)
+ # Prevent to delete veth interface which used for another "vethX peer-name"
+ for iface, iface_config in veth['other_interfaces'].items():
+ if veth['ifname'] in iface_config['peer_name']:
+ ifname = veth['ifname']
+ raise ConfigError(
+ f'Cannot delete "{ifname}" used for "interface {iface} peer-name"'
+ )
+ return None
+
+ verify_vrf(veth)
+ verify_address(veth)
+
+ if 'peer_name' not in veth:
+ raise ConfigError(f'Remote peer name must be set for "{veth["ifname"]}"!')
+
+ peer_name = veth['peer_name']
+ ifname = veth['ifname']
+
+ if veth['peer_name'] not in veth['other_interfaces']:
+ raise ConfigError(f'Used peer-name "{peer_name}" on interface "{ifname}" ' \
+ 'is not configured!')
+
+ if veth['other_interfaces'][peer_name]['peer_name'] != ifname:
+ raise ConfigError(
+ f'Configuration mismatch between "{ifname}" and "{peer_name}"!')
+
+ if peer_name == ifname:
+ raise ConfigError(
+ f'Peer-name "{peer_name}" cannot be the same as interface "{ifname}"!')
+
+ return None
+
+
+def generate(peth):
+ return None
+
+def apply(veth):
+ # Check if the Veth interface already exists
+ if 'rebuild_required' in veth or 'deleted' in veth:
+ if veth['ifname'] in interfaces():
+ p = VethIf(veth['ifname'])
+ p.remove()
+
+ if 'deleted' not in veth:
+ p = VethIf(**veth)
+ p.update(veth)
+
+ 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/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py
index 8d738f55e..762bad94f 100755
--- a/src/conf_mode/interfaces-wireguard.py
+++ b/src/conf_mode/interfaces-wireguard.py
@@ -87,6 +87,8 @@ def verify(wireguard):
'cannot be used for the interface!')
# run checks on individual configured WireGuard peer
+ public_keys = []
+
for tmp in wireguard['peer']:
peer = wireguard['peer'][tmp]
@@ -100,6 +102,11 @@ def verify(wireguard):
raise ConfigError('Both Wireguard port and address must be defined '
f'for peer "{tmp}" if either one of them is set!')
+ if peer['public_key'] in public_keys:
+ raise ConfigError(f'Duplicate public-key defined on peer "{tmp}"')
+
+ public_keys.append(peer['public_key'])
+
def apply(wireguard):
tmp = WireGuardIf(wireguard['ifname'])
if 'deleted' in wireguard:
diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py
index 8b1a5a720..9f8221514 100755
--- a/src/conf_mode/nat.py
+++ b/src/conf_mode/nat.py
@@ -32,6 +32,7 @@ from vyos.util import cmd
from vyos.util import run
from vyos.util import check_kmod
from vyos.util import dict_search
+from vyos.util import dict_search_args
from vyos.validate import is_addr_assigned
from vyos.xml import defaults
from vyos import ConfigError
@@ -47,6 +48,13 @@ else:
nftables_nat_config = '/run/nftables_nat.conf'
nftables_static_nat_conf = '/run/nftables_static-nat-rules.nft'
+valid_groups = [
+ 'address_group',
+ 'domain_group',
+ 'network_group',
+ 'port_group'
+]
+
def get_handler(json, chain, target):
""" Get nftable rule handler number of given chain/target combination.
Handler is required when adding NAT/Conntrack helper targets """
@@ -60,7 +68,7 @@ def get_handler(json, chain, target):
return None
-def verify_rule(config, err_msg):
+def verify_rule(config, err_msg, groups_dict):
""" Common verify steps used for both source and destination NAT """
if (dict_search('translation.port', config) != None or
@@ -78,6 +86,45 @@ def verify_rule(config, err_msg):
'statically maps a whole network of addresses onto another\n' \
'network of addresses')
+ for side in ['destination', 'source']:
+ if side in config:
+ side_conf = config[side]
+
+ if len({'address', 'fqdn'} & set(side_conf)) > 1:
+ raise ConfigError('Only one of address, fqdn or geoip can be specified')
+
+ if 'group' in side_conf:
+ if len({'address_group', 'network_group', 'domain_group'} & set(side_conf['group'])) > 1:
+ raise ConfigError('Only one address-group, network-group or domain-group can be specified')
+
+ for group in valid_groups:
+ if group in side_conf['group']:
+ group_name = side_conf['group'][group]
+ error_group = group.replace("_", "-")
+
+ if group in ['address_group', 'network_group', 'domain_group']:
+ types = [t for t in ['address', 'fqdn'] if t in side_conf]
+ if types:
+ raise ConfigError(f'{error_group} and {types[0]} cannot both be defined')
+
+ if group_name and group_name[0] == '!':
+ group_name = group_name[1:]
+
+ group_obj = dict_search_args(groups_dict, group, group_name)
+
+ if group_obj is None:
+ raise ConfigError(f'Invalid {error_group} "{group_name}" on firewall rule')
+
+ if not group_obj:
+ Warning(f'{error_group} "{group_name}" has no members!')
+
+ if dict_search_args(side_conf, 'group', 'port_group'):
+ if 'protocol' not in config:
+ raise ConfigError('Protocol must be defined if specifying a port-group')
+
+ if config['protocol'] not in ['tcp', 'udp', 'tcp_udp']:
+ raise ConfigError('Protocol must be tcp, udp, or tcp_udp when specifying a port-group')
+
def get_config(config=None):
if config:
conf = config
@@ -105,16 +152,20 @@ def get_config(config=None):
condensed_json = jmespath.search(pattern, nftable_json)
if not conf.exists(base):
- nat['helper_functions'] = 'remove'
-
- # Retrieve current table handler positions
- nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYOS_CT_HELPER')
- nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK')
- nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYOS_CT_HELPER')
- nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'NAT_CONNTRACK')
+ if get_handler(condensed_json, 'PREROUTING', 'VYOS_CT_HELPER'):
+ nat['helper_functions'] = 'remove'
+
+ # Retrieve current table handler positions
+ nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYOS_CT_HELPER')
+ nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK')
+ nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYOS_CT_HELPER')
+ nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'NAT_CONNTRACK')
nat['deleted'] = ''
return nat
+ nat['firewall_group'] = conf.get_config_dict(['firewall', 'group'], key_mangling=('-', '_'), get_first_key=True,
+ no_tag_node_value_mangle=True)
+
# check if NAT connection tracking helpers need to be set up - this has to
# be done only once
if not get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK'):
@@ -146,6 +197,10 @@ def verify(nat):
if config['outbound_interface'] not in 'any' and config['outbound_interface'] not in interfaces():
Warning(f'rule "{rule}" interface "{config["outbound_interface"]}" does not exist on this system')
+ if not dict_search('translation.address', config) and not dict_search('translation.port', config):
+ if 'exclude' not in config:
+ raise ConfigError(f'{err_msg} translation requires address and/or port')
+
addr = dict_search('translation.address', config)
if addr != None and addr != 'masquerade' and not is_ip_network(addr):
for ip in addr.split('-'):
@@ -153,7 +208,7 @@ def verify(nat):
Warning(f'IP address {ip} does not exist on the system!')
# common rule verification
- verify_rule(config, err_msg)
+ verify_rule(config, err_msg, nat['firewall_group'])
if dict_search('destination.rule', nat):
@@ -166,8 +221,12 @@ def verify(nat):
elif config['inbound_interface'] not in 'any' and config['inbound_interface'] not in interfaces():
Warning(f'rule "{rule}" interface "{config["inbound_interface"]}" does not exist on this system')
+ if not dict_search('translation.address', config) and not dict_search('translation.port', config):
+ if 'exclude' not in config:
+ raise ConfigError(f'{err_msg} translation requires address and/or port')
+
# common rule verification
- verify_rule(config, err_msg)
+ verify_rule(config, err_msg, nat['firewall_group'])
if dict_search('static.rule', nat):
for rule, config in dict_search('static.rule', nat).items():
@@ -178,7 +237,7 @@ def verify(nat):
'inbound-interface not specified')
# common rule verification
- verify_rule(config, err_msg)
+ verify_rule(config, err_msg, nat['firewall_group'])
return None
@@ -204,6 +263,10 @@ def apply(nat):
cmd(f'nft -f {nftables_nat_config}')
cmd(f'nft -f {nftables_static_nat_conf}')
+ if not nat or 'deleted' in nat:
+ os.unlink(nftables_nat_config)
+ os.unlink(nftables_static_nat_conf)
+
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py
index 29ed7b1b7..e8f3cc87a 100755
--- a/src/conf_mode/pki.py
+++ b/src/conf_mode/pki.py
@@ -16,20 +16,16 @@
from sys import exit
-import jmespath
-
from vyos.config import Config
+from vyos.configdep import set_dependents, call_dependents
from vyos.configdict import dict_merge
from vyos.configdict import node_changed
from vyos.pki import is_ca_certificate
from vyos.pki import load_certificate
-from vyos.pki import load_certificate_request
from vyos.pki import load_public_key
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
@@ -121,6 +117,39 @@ def get_config(config=None):
get_first_key=True,
no_tag_node_value_mangle=True)
+ 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 = search['path']
+ path_str = ' '.join(path + found_path)
+ print(f'pki: Updating config: {path_str} {found_name}')
+
+ if path[0] == 'interfaces':
+ ifname = found_path[0]
+ set_dependents(path[1], conf, ifname)
+ else:
+ set_dependents(path[1], conf)
+
return pki
def is_valid_certificate(raw_data):
@@ -259,37 +288,7 @@ def apply(pki):
return None
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)
+ call_dependents()
return None
diff --git a/src/conf_mode/policy-route-interface.py b/src/conf_mode/policy-route-interface.py
deleted file mode 100755
index 58c5fd93d..000000000
--- a/src/conf_mode/policy-route-interface.py
+++ /dev/null
@@ -1,132 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2021 VyOS maintainers and contributors
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 or later as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-import os
-import re
-
-from sys import argv
-from sys import exit
-
-from vyos.config import Config
-from vyos.ifconfig import Section
-from vyos.template import render
-from vyos.util import cmd
-from vyos.util import run
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-def get_config(config=None):
- if config:
- conf = config
- else:
- conf = Config()
-
- ifname = argv[1]
- ifpath = Section.get_config_path(ifname)
- if_policy_path = f'interfaces {ifpath} policy'
-
- if_policy = conf.get_config_dict(if_policy_path, key_mangling=('-', '_'), get_first_key=True,
- no_tag_node_value_mangle=True)
-
- if_policy['ifname'] = ifname
- if_policy['policy'] = conf.get_config_dict(['policy'], key_mangling=('-', '_'), get_first_key=True,
- no_tag_node_value_mangle=True)
-
- return if_policy
-
-def verify_chain(table, chain):
- # Verify policy route applied
- code = run(f'nft list chain {table} {chain}')
- return code == 0
-
-def verify(if_policy):
- # bail out early - looks like removal from running config
- if not if_policy:
- return None
-
- for route in ['route', 'route6']:
- if route in if_policy:
- if route not in if_policy['policy']:
- raise ConfigError('Policy route not configured')
-
- route_name = if_policy[route]
-
- if route_name not in if_policy['policy'][route]:
- raise ConfigError(f'Invalid policy route name "{name}"')
-
- nft_prefix = 'VYOS_PBR6_' if route == 'route6' else 'VYOS_PBR_'
- nft_table = 'ip6 mangle' if route == 'route6' else 'ip mangle'
-
- if not verify_chain(nft_table, nft_prefix + route_name):
- raise ConfigError('Policy route did not apply')
-
- return None
-
-def generate(if_policy):
- return None
-
-def cleanup_rule(table, chain, ifname, new_name=None):
- results = cmd(f'nft -a list chain {table} {chain}').split("\n")
- retval = None
- for line in results:
- if f'ifname "{ifname}"' in line:
- if new_name and f'jump {new_name}' in line:
- # new_name is used to clear rules for any previously referenced chains
- # returns true when rule exists and doesn't need to be created
- retval = True
- continue
-
- handle_search = re.search('handle (\d+)', line)
- if handle_search:
- cmd(f'nft delete rule {table} {chain} handle {handle_search[1]}')
- return retval
-
-def apply(if_policy):
- ifname = if_policy['ifname']
-
- route_chain = 'VYOS_PBR_PREROUTING'
- ipv6_route_chain = 'VYOS_PBR6_PREROUTING'
-
- if 'route' in if_policy:
- name = 'VYOS_PBR_' + if_policy['route']
- rule_exists = cleanup_rule('ip mangle', route_chain, ifname, name)
-
- if not rule_exists:
- cmd(f'nft insert rule ip mangle {route_chain} iifname {ifname} counter jump {name}')
- else:
- cleanup_rule('ip mangle', route_chain, ifname)
-
- if 'route6' in if_policy:
- name = 'VYOS_PBR6_' + if_policy['route6']
- rule_exists = cleanup_rule('ip6 mangle', ipv6_route_chain, ifname, name)
-
- if not rule_exists:
- cmd(f'nft insert rule ip6 mangle {ipv6_route_chain} iifname {ifname} counter jump {name}')
- else:
- cleanup_rule('ip6 mangle', ipv6_route_chain, ifname)
-
- return None
-
-if __name__ == '__main__':
- try:
- c = get_config()
- verify(c)
- generate(c)
- apply(c)
- except ConfigError as e:
- print(e)
- exit(1)
diff --git a/src/conf_mode/policy-route.py b/src/conf_mode/policy-route.py
index 00539b9c7..1d016695e 100755
--- a/src/conf_mode/policy-route.py
+++ b/src/conf_mode/policy-route.py
@@ -15,7 +15,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
-import re
from json import loads
from sys import exit
@@ -25,7 +24,6 @@ from vyos.config import Config
from vyos.template import render
from vyos.util import cmd
from vyos.util import dict_search_args
-from vyos.util import dict_search_recursive
from vyos.util import run
from vyos import ConfigError
from vyos import airbag
@@ -34,48 +32,13 @@ airbag.enable()
mark_offset = 0x7FFFFFFF
nftables_conf = '/run/nftables_policy.conf'
-ROUTE_PREFIX = 'VYOS_PBR_'
-ROUTE6_PREFIX = 'VYOS_PBR6_'
-
-preserve_chains = [
- 'VYOS_PBR_PREROUTING',
- 'VYOS_PBR_POSTROUTING',
- 'VYOS_PBR6_PREROUTING',
- 'VYOS_PBR6_POSTROUTING'
-]
-
valid_groups = [
'address_group',
+ 'domain_group',
'network_group',
'port_group'
]
-group_set_prefix = {
- 'A_': 'address_group',
- 'A6_': 'ipv6_address_group',
-# 'D_': 'domain_group',
- 'M_': 'mac_group',
- 'N_': 'network_group',
- 'N6_': 'ipv6_network_group',
- 'P_': 'port_group'
-}
-
-def get_policy_interfaces(conf):
- out = {}
- interfaces = conf.get_config_dict(['interfaces'], key_mangling=('-', '_'), get_first_key=True,
- no_tag_node_value_mangle=True)
- def find_interfaces(iftype_conf, output={}, prefix=''):
- for ifname, if_conf in iftype_conf.items():
- if 'policy' in if_conf:
- output[prefix + ifname] = if_conf['policy']
- for vif in ['vif', 'vif_s', 'vif_c']:
- if vif in if_conf:
- output.update(find_interfaces(if_conf[vif], output, f'{prefix}{ifname}.'))
- return output
- for iftype, iftype_conf in interfaces.items():
- out.update(find_interfaces(iftype_conf))
- return out
-
def get_config(config=None):
if config:
conf = config
@@ -88,7 +51,6 @@ def get_config(config=None):
policy['firewall_group'] = conf.get_config_dict(['firewall', 'group'], key_mangling=('-', '_'), get_first_key=True,
no_tag_node_value_mangle=True)
- policy['interfaces'] = get_policy_interfaces(conf)
return policy
@@ -132,8 +94,8 @@ def verify_rule(policy, name, rule_conf, ipv6, rule_id):
side_conf = rule_conf[side]
if 'group' in side_conf:
- if {'address_group', 'network_group'} <= set(side_conf['group']):
- raise ConfigError('Only one address-group or network-group can be specified')
+ if len({'address_group', 'domain_group', 'network_group'} & set(side_conf['group'])) > 1:
+ raise ConfigError('Only one address-group, domain-group or network-group can be specified')
for group in valid_groups:
if group in side_conf['group']:
@@ -168,73 +130,11 @@ def verify(policy):
for rule_id, rule_conf in pol_conf['rule'].items():
verify_rule(policy, name, rule_conf, ipv6, rule_id)
- for ifname, if_policy in policy['interfaces'].items():
- name = dict_search_args(if_policy, 'route')
- ipv6_name = dict_search_args(if_policy, 'route6')
-
- if name and not dict_search_args(policy, 'route', name):
- raise ConfigError(f'Policy route "{name}" is still referenced on interface {ifname}')
-
- if ipv6_name and not dict_search_args(policy, 'route6', ipv6_name):
- raise ConfigError(f'Policy route6 "{ipv6_name}" is still referenced on interface {ifname}')
-
return None
-def cleanup_commands(policy):
- commands = []
- commands_chains = []
- commands_sets = []
- for table in ['ip mangle', 'ip6 mangle']:
- route_node = 'route' if table == 'ip mangle' else 'route6'
- chain_prefix = ROUTE_PREFIX if table == 'ip mangle' else ROUTE6_PREFIX
-
- json_str = cmd(f'nft -t -j list table {table}')
- obj = loads(json_str)
- if 'nftables' not in obj:
- continue
- for item in obj['nftables']:
- if 'chain' in item:
- chain = item['chain']['name']
- if chain in preserve_chains or not chain.startswith("VYOS_PBR"):
- continue
-
- if dict_search_args(policy, route_node, chain.replace(chain_prefix, "", 1)) != None:
- commands.append(f'flush chain {table} {chain}')
- else:
- commands_chains.append(f'delete chain {table} {chain}')
-
- if 'rule' in item:
- rule = item['rule']
- chain = rule['chain']
- handle = rule['handle']
-
- if chain not in preserve_chains:
- continue
-
- target, _ = next(dict_search_recursive(rule['expr'], 'target'))
-
- if target.startswith(chain_prefix):
- if dict_search_args(policy, route_node, target.replace(chain_prefix, "", 1)) == None:
- commands.append(f'delete rule {table} {chain} handle {handle}')
-
- if 'set' in item:
- set_name = item['set']['name']
-
- for prefix, group_type in group_set_prefix.items():
- if set_name.startswith(prefix):
- group_name = set_name.replace(prefix, "", 1)
- if dict_search_args(policy, 'firewall_group', group_type, group_name) != None:
- commands_sets.append(f'flush set {table} {set_name}')
- else:
- commands_sets.append(f'delete set {table} {set_name}')
-
- return commands + commands_chains + commands_sets
-
def generate(policy):
if not os.path.exists(nftables_conf):
policy['first_install'] = True
- else:
- policy['cleanup_commands'] = cleanup_commands(policy)
render(nftables_conf, 'firewall/nftables-policy.j2', policy)
return None
diff --git a/src/conf_mode/policy.py b/src/conf_mode/policy.py
index a0d288e91..331194fec 100755
--- a/src/conf_mode/policy.py
+++ b/src/conf_mode/policy.py
@@ -167,6 +167,11 @@ def verify(policy):
continue
for rule, rule_config in route_map_config['rule'].items():
+ # Action 'deny' cannot be used with "continue"
+ # FRR does not validate it T4827
+ if rule_config['action'] == 'deny' and 'continue' in rule_config:
+ raise ConfigError(f'rule {rule} "continue" cannot be used with action deny!')
+
# Specified community-list must exist
tmp = dict_search('match.community.community_list',
rule_config)
diff --git a/src/conf_mode/protocols_failover.py b/src/conf_mode/protocols_failover.py
new file mode 100755
index 000000000..048ba7a89
--- /dev/null
+++ b/src/conf_mode/protocols_failover.py
@@ -0,0 +1,121 @@
+#!/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.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()
+
+
+service_name = 'vyos-failover'
+service_conf = Path(f'/run/{service_name}.conf')
+systemd_service = '/etc/systemd/system/vyos-failover.service'
+rt_proto_failover = '/etc/iproute2/rt_protos.d/failover.conf'
+
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['protocols', 'failover']
+ failover = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+
+ # Set default values only if we set config
+ if failover.get('route'):
+ for route, route_config in failover.get('route').items():
+ for next_hop, next_hop_config in route_config.get('next_hop').items():
+ default_values = defaults(base + ['route'])
+ failover['route'][route]['next_hop'][next_hop] = dict_merge(
+ default_values['next_hop'], failover['route'][route]['next_hop'][next_hop])
+
+ return failover
+
+def verify(failover):
+ # bail out early - looks like removal from running config
+ if not failover:
+ return None
+
+ if 'route' not in failover:
+ raise ConfigError(f'Failover "route" is mandatory!')
+
+ for route, route_config in failover['route'].items():
+ if not route_config.get('next_hop'):
+ raise ConfigError(f'Next-hop for "{route}" is mandatory!')
+
+ for next_hop, next_hop_config in route_config.get('next_hop').items():
+ if 'interface' not in next_hop_config:
+ raise ConfigError(f'Interface for route "{route}" next-hop "{next_hop}" is mandatory!')
+
+ if not next_hop_config.get('check'):
+ raise ConfigError(f'Check target for next-hop "{next_hop}" is mandatory!')
+
+ if 'target' not in next_hop_config['check']:
+ raise ConfigError(f'Check target for next-hop "{next_hop}" is mandatory!')
+
+ check_type = next_hop_config['check']['type']
+ if check_type == 'tcp' and 'port' not in next_hop_config['check']:
+ raise ConfigError(f'Check port for next-hop "{next_hop}" and type TCP is mandatory!')
+
+ return None
+
+def generate(failover):
+ if not failover:
+ service_conf.unlink(missing_ok=True)
+ return None
+
+ # Add own rt_proto 'failover'
+ # Helps to detect all own routes 'proto failover'
+ with open(rt_proto_failover, 'w') as f:
+ f.write('111 failover\n')
+
+ # Write configuration file
+ conf_json = json.dumps(failover, indent=4)
+ service_conf.write_text(conf_json)
+ render(systemd_service, 'protocols/systemd_vyos_failover_service.j2', failover)
+
+ return None
+
+def apply(failover):
+ if not failover:
+ call(f'systemctl stop {service_name}.service')
+ call('ip route flush protocol failover')
+ else:
+ call('systemctl daemon-reload')
+ call(f'systemctl restart {service_name}.service')
+ call(f'ip route flush protocol failover')
+
+ 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_mpls.py b/src/conf_mode/protocols_mpls.py
index 5da8e7b06..73af6595b 100755
--- a/src/conf_mode/protocols_mpls.py
+++ b/src/conf_mode/protocols_mpls.py
@@ -24,6 +24,7 @@ from vyos.template import render_to_string
from vyos.util import dict_search
from vyos.util import read_file
from vyos.util import sysctl_write
+from vyos.configverify import verify_interface_exists
from vyos import ConfigError
from vyos import frr
from vyos import airbag
@@ -46,6 +47,10 @@ def verify(mpls):
if not mpls:
return None
+ if 'interface' in mpls:
+ for interface in mpls['interface']:
+ verify_interface_exists(interface)
+
# Checks to see if LDP is properly configured
if 'ldp' in mpls:
# If router ID not defined
diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py
index ba0249efd..600ba4e92 100755
--- a/src/conf_mode/service_pppoe-server.py
+++ b/src/conf_mode/service_pppoe-server.py
@@ -20,6 +20,7 @@ from sys import exit
from vyos.config import Config
from vyos.configdict import get_accel_dict
+from vyos.configdict import is_node_changed
from vyos.configverify import verify_accel_ppp_base_service
from vyos.configverify import verify_interface_exists
from vyos.template import render
@@ -43,6 +44,13 @@ def get_config(config=None):
# retrieve common dictionary keys
pppoe = get_accel_dict(conf, base, pppoe_chap_secrets)
+
+ # reload-or-restart does not implemented in accel-ppp
+ # use this workaround until it will be implemented
+ # https://phabricator.accel-ppp.org/T3
+ if is_node_changed(conf, base + ['client-ip-pool']) or is_node_changed(
+ conf, base + ['client-ipv6-pool']):
+ pppoe.update({'restart_required': {}})
return pppoe
def verify(pppoe):
@@ -95,7 +103,10 @@ def apply(pppoe):
os.unlink(file)
return None
- call(f'systemctl reload-or-restart {systemd_service}')
+ if 'restart_required' in pppoe:
+ call(f'systemctl restart {systemd_service}')
+ else:
+ call(f'systemctl reload-or-restart {systemd_service}')
if __name__ == '__main__':
try:
diff --git a/src/conf_mode/service_webproxy.py b/src/conf_mode/service_webproxy.py
index 32af31bde..41a1deaa3 100755
--- a/src/conf_mode/service_webproxy.py
+++ b/src/conf_mode/service_webproxy.py
@@ -28,8 +28,10 @@ from vyos.util import dict_search
from vyos.util import write_file
from vyos.validate import is_addr_assigned
from vyos.xml import defaults
+from vyos.base import Warning
from vyos import ConfigError
from vyos import airbag
+
airbag.enable()
squid_config_file = '/etc/squid/squid.conf'
@@ -37,24 +39,57 @@ squidguard_config_file = '/etc/squidguard/squidGuard.conf'
squidguard_db_dir = '/opt/vyatta/etc/config/url-filtering/squidguard/db'
user_group = 'proxy'
-def generate_sg_localdb(category, list_type, role, proxy):
+
+def check_blacklist_categorydb(config_section):
+ if 'block_category' in config_section:
+ for category in config_section['block_category']:
+ check_categorydb(category)
+ if 'allow_category' in config_section:
+ for category in config_section['allow_category']:
+ check_categorydb(category)
+
+
+def check_categorydb(category: str):
+ """
+ Check if category's db exist
+ :param category:
+ :type str:
+ """
+ path_to_cat: str = f'{squidguard_db_dir}/{category}'
+ if not os.path.exists(f'{path_to_cat}/domains.db') \
+ and not os.path.exists(f'{path_to_cat}/urls.db') \
+ and not os.path.exists(f'{path_to_cat}/expressions.db'):
+ Warning(f'DB of category {category} does not exist.\n '
+ f'Use [update webproxy blacklists] '
+ f'or delete undefined category!')
+
+
+def generate_sg_rule_localdb(category, list_type, role, proxy):
+ if not category or not list_type or not role:
+ return None
+
cat_ = category.replace('-', '_')
- if isinstance(dict_search(f'url_filtering.squidguard.{cat_}', proxy),
- list):
+ if role == 'default':
+ path_to_cat = f'{cat_}'
+ else:
+ path_to_cat = f'rule.{role}.{cat_}'
+ if isinstance(
+ dict_search(f'url_filtering.squidguard.{path_to_cat}', proxy),
+ list):
# local block databases must be generated "on-the-fly"
tmp = {
- 'squidguard_db_dir' : squidguard_db_dir,
- 'category' : f'{category}-default',
- 'list_type' : list_type,
- 'rule' : role
+ 'squidguard_db_dir': squidguard_db_dir,
+ 'category': f'{category}-{role}',
+ 'list_type': list_type,
+ 'rule': role
}
sg_tmp_file = '/tmp/sg.conf'
- db_file = f'{category}-default/{list_type}'
- domains = '\n'.join(dict_search(f'url_filtering.squidguard.{cat_}', proxy))
-
+ db_file = f'{category}-{role}/{list_type}'
+ domains = '\n'.join(
+ dict_search(f'url_filtering.squidguard.{path_to_cat}', proxy))
# local file
- write_file(f'{squidguard_db_dir}/{category}-default/local', '',
+ write_file(f'{squidguard_db_dir}/{category}-{role}/local', '',
user=user_group, group=user_group)
# database input file
write_file(f'{squidguard_db_dir}/{db_file}', domains,
@@ -64,17 +99,18 @@ def generate_sg_localdb(category, list_type, role, proxy):
render(sg_tmp_file, 'squid/sg_acl.conf.j2', tmp,
user=user_group, group=user_group)
- call(f'su - {user_group} -c "squidGuard -d -c {sg_tmp_file} -C {db_file}"')
+ call(
+ f'su - {user_group} -c "squidGuard -d -c {sg_tmp_file} -C {db_file}"')
if os.path.exists(sg_tmp_file):
os.unlink(sg_tmp_file)
-
else:
# if category is not part of our configuration, clean out the
# squidguard lists
- tmp = f'{squidguard_db_dir}/{category}-default'
+ tmp = f'{squidguard_db_dir}/{category}-{role}'
if os.path.exists(tmp):
- rmtree(f'{squidguard_db_dir}/{category}-default')
+ rmtree(f'{squidguard_db_dir}/{category}-{role}')
+
def get_config(config=None):
if config:
@@ -85,7 +121,8 @@ def get_config(config=None):
if not conf.exists(base):
return None
- proxy = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ proxy = 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)
@@ -110,10 +147,11 @@ def get_config(config=None):
default_values = defaults(base + ['cache-peer'])
for peer in proxy['cache_peer']:
proxy['cache_peer'][peer] = dict_merge(default_values,
- proxy['cache_peer'][peer])
+ proxy['cache_peer'][peer])
return proxy
+
def verify(proxy):
if not proxy:
return None
@@ -170,17 +208,30 @@ def generate(proxy):
render(squidguard_config_file, 'squid/squidGuard.conf.j2', proxy)
cat_dict = {
- 'local-block' : 'domains',
- 'local-block-keyword' : 'expressions',
- 'local-block-url' : 'urls',
- 'local-ok' : 'domains',
- 'local-ok-url' : 'urls'
+ 'local-block': 'domains',
+ 'local-block-keyword': 'expressions',
+ 'local-block-url': 'urls',
+ 'local-ok': 'domains',
+ 'local-ok-url': 'urls'
}
- for category, list_type in cat_dict.items():
- generate_sg_localdb(category, list_type, 'default', proxy)
+ if dict_search(f'url_filtering.squidguard', proxy) is not None:
+ squidgard_config_section = proxy['url_filtering']['squidguard']
+
+ for category, list_type in cat_dict.items():
+ generate_sg_rule_localdb(category, list_type, 'default', proxy)
+ check_blacklist_categorydb(squidgard_config_section)
+
+ if 'rule' in squidgard_config_section:
+ for rule in squidgard_config_section['rule']:
+ rule_config_section = squidgard_config_section['rule'][
+ rule]
+ for category, list_type in cat_dict.items():
+ generate_sg_rule_localdb(category, list_type, rule, proxy)
+ check_blacklist_categorydb(rule_config_section)
return None
+
def apply(proxy):
if not proxy:
# proxy is removed in the commit
@@ -198,6 +249,7 @@ def apply(proxy):
call('systemctl restart squid.service')
return None
+
if __name__ == '__main__':
try:
c = get_config()
diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
index 77a425f8b..b79e9847a 100755
--- a/src/conf_mode/vpn_ipsec.py
+++ b/src/conf_mode/vpn_ipsec.py
@@ -22,6 +22,7 @@ from sys import exit
from time import sleep
from time import time
+from vyos.base import Warning
from vyos.config import Config
from vyos.configdict import leaf_node_changed
from vyos.configverify import verify_interface_exists
@@ -117,13 +118,26 @@ def get_config(config=None):
ipsec['ike_group'][group]['proposal'][proposal] = dict_merge(default_values,
ipsec['ike_group'][group]['proposal'][proposal])
- if 'remote_access' in ipsec and 'connection' in ipsec['remote_access']:
+ # XXX: T2665: we can not safely rely on the defaults() when there are
+ # tagNodes in place, it is better to blend in the defaults manually.
+ if dict_search('remote_access.connection', ipsec):
default_values = defaults(base + ['remote-access', 'connection'])
for rw in ipsec['remote_access']['connection']:
ipsec['remote_access']['connection'][rw] = dict_merge(default_values,
ipsec['remote_access']['connection'][rw])
- if 'remote_access' in ipsec and 'radius' in ipsec['remote_access'] and 'server' in ipsec['remote_access']['radius']:
+ # XXX: T2665: we can not safely rely on the defaults() when there are
+ # tagNodes in place, it is better to blend in the defaults manually.
+ if dict_search('remote_access.radius.server', ipsec):
+ # Fist handle the "base" stuff like RADIUS timeout
+ default_values = defaults(base + ['remote-access', 'radius'])
+ if 'server' in default_values:
+ del default_values['server']
+ ipsec['remote_access']['radius'] = dict_merge(default_values,
+ ipsec['remote_access']['radius'])
+
+ # Take care about individual RADIUS servers implemented as tagNodes - this
+ # requires special treatment
default_values = defaults(base + ['remote-access', 'radius', 'server'])
for server in ipsec['remote_access']['radius']['server']:
ipsec['remote_access']['radius']['server'][server] = dict_merge(default_values,
@@ -425,6 +439,10 @@ def verify(ipsec):
if 'local_address' in peer_conf and 'dhcp_interface' in peer_conf:
raise ConfigError(f"A single local-address or dhcp-interface is required when using VTI on site-to-site peer {peer}")
+ if dict_search('options.disable_route_autoinstall',
+ ipsec) == None:
+ Warning('It\'s recommended to use ipsec vty with the next command\n[set vpn ipsec option disable-route-autoinstall]')
+
if 'bind' in peer_conf['vti']:
vti_interface = peer_conf['vti']['bind']
if not os.path.exists(f'/sys/class/net/{vti_interface}'):
diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py
index fd5a4acd8..27e78db99 100755
--- a/src/conf_mode/vpn_l2tp.py
+++ b/src/conf_mode/vpn_l2tp.py
@@ -26,7 +26,10 @@ from ipaddress import ip_network
from vyos.config import Config
from vyos.template import is_ipv4
from vyos.template import render
-from vyos.util import call, get_half_cpus
+from vyos.util import call
+from vyos.util import get_half_cpus
+from vyos.util import check_port_availability
+from vyos.util import is_listen_port_bind_service
from vyos import ConfigError
from vyos import airbag
@@ -43,6 +46,7 @@ default_config_data = {
'client_ip_pool': None,
'client_ip_subnets': [],
'client_ipv6_pool': [],
+ 'client_ipv6_pool_configured': False,
'client_ipv6_delegate_prefix': [],
'dnsv4': [],
'dnsv6': [],
@@ -64,7 +68,7 @@ default_config_data = {
'radius_source_address': '',
'radius_shaper_attr': '',
'radius_shaper_vendor': '',
- 'radius_dynamic_author': '',
+ 'radius_dynamic_author': {},
'wins': [],
'ip6_column': [],
'thread_cnt': get_half_cpus()
@@ -205,21 +209,21 @@ def get_config(config=None):
l2tp['radius_source_address'] = conf.return_value(['source-address'])
# Dynamic Authorization Extensions (DOA)/Change Of Authentication (COA)
- if conf.exists(['dynamic-author']):
+ if conf.exists(['dae-server']):
dae = {
'port' : '',
'server' : '',
'key' : ''
}
- if conf.exists(['dynamic-author', 'server']):
- dae['server'] = conf.return_value(['dynamic-author', 'server'])
+ if conf.exists(['dae-server', 'ip-address']):
+ dae['server'] = conf.return_value(['dae-server', 'ip-address'])
- if conf.exists(['dynamic-author', 'port']):
- dae['port'] = conf.return_value(['dynamic-author', 'port'])
+ if conf.exists(['dae-server', 'port']):
+ dae['port'] = conf.return_value(['dae-server', 'port'])
- if conf.exists(['dynamic-author', 'key']):
- dae['key'] = conf.return_value(['dynamic-author', 'key'])
+ if conf.exists(['dae-server', 'secret']):
+ dae['key'] = conf.return_value(['dae-server', 'secret'])
l2tp['radius_dynamic_author'] = dae
@@ -244,6 +248,7 @@ def get_config(config=None):
l2tp['client_ip_subnets'] = conf.return_values(['client-ip-pool', 'subnet'])
if conf.exists(['client-ipv6-pool', 'prefix']):
+ l2tp['client_ipv6_pool_configured'] = True
l2tp['ip6_column'].append('ip6')
for prefix in conf.list_nodes(['client-ipv6-pool', 'prefix']):
tmp = {
@@ -306,6 +311,9 @@ def get_config(config=None):
if conf.exists(['ppp-options', 'lcp-echo-interval']):
l2tp['ppp_echo_interval'] = conf.return_value(['ppp-options', 'lcp-echo-interval'])
+ if conf.exists(['ppp-options', 'ipv6']):
+ l2tp['ppp_ipv6'] = conf.return_value(['ppp-options', 'ipv6'])
+
return l2tp
@@ -329,6 +337,19 @@ def verify(l2tp):
if not radius['key']:
raise ConfigError(f"Missing RADIUS secret for server { radius['key'] }")
+ if l2tp['radius_dynamic_author']:
+ if not l2tp['radius_dynamic_author']['server']:
+ raise ConfigError("Missing ip-address for dae-server")
+ if not l2tp['radius_dynamic_author']['key']:
+ raise ConfigError("Missing secret for dae-server")
+ address = l2tp['radius_dynamic_author']['server']
+ port = l2tp['radius_dynamic_author']['port']
+ proto = 'tcp'
+ # check if dae listen port is not used by another service
+ if check_port_availability(address, int(port), proto) is not True and \
+ not is_listen_port_bind_service(int(port), 'accel-pppd'):
+ raise ConfigError(f'"{proto}" port "{port}" is used by another service')
+
# check for the existence of a client ip pool
if not (l2tp['client_ip_pool'] or l2tp['client_ip_subnets']):
raise ConfigError(
diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py
index c050b796b..af3c51efc 100755
--- a/src/conf_mode/vpn_openconnect.py
+++ b/src/conf_mode/vpn_openconnect.py
@@ -58,7 +58,7 @@ def get_config():
default_values = defaults(base)
ocserv = dict_merge(default_values, ocserv)
- if "local" in ocserv["authentication"]["mode"]:
+ if 'mode' in ocserv["authentication"] and "local" in ocserv["authentication"]["mode"]:
# workaround a "know limitation" - https://phabricator.vyos.net/T2665
del ocserv['authentication']['local_users']['username']['otp']
if not ocserv["authentication"]["local_users"]["username"]:
@@ -157,7 +157,7 @@ def verify(ocserv):
ocserv["network_settings"]["push_route"].remove("0.0.0.0/0")
ocserv["network_settings"]["push_route"].append("default")
else:
- ocserv["network_settings"]["push_route"] = "default"
+ ocserv["network_settings"]["push_route"] = ["default"]
else:
raise ConfigError('openconnect network settings required')
@@ -247,7 +247,7 @@ def apply(ocserv):
if os.path.exists(file):
os.unlink(file)
else:
- call('systemctl restart ocserv.service')
+ call('systemctl reload-or-restart ocserv.service')
counter = 0
while True:
# exit early when service runs
diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf b/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf
index b1902b585..518abeaec 100644
--- a/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf
+++ b/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf
@@ -33,8 +33,8 @@ if /usr/bin/systemctl -q is-active vyos-hostsd; then
if [ -n "$new_dhcp6_name_servers" ]; then
logmsg info "Deleting nameservers with tag \"dhcpv6-$interface\" via vyos-hostsd-client"
$hostsd_client --delete-name-servers --tag "dhcpv6-$interface"
- logmsg info "Adding nameservers \"$new_dhcpv6_name_servers\" with tag \"dhcpv6-$interface\" via vyos-hostsd-client"
- $hostsd_client --add-name-servers $new_dhcpv6_name_servers --tag "dhcpv6-$interface"
+ logmsg info "Adding nameservers \"$new_dhcp6_name_servers\" with tag \"dhcpv6-$interface\" via vyos-hostsd-client"
+ $hostsd_client --add-name-servers $new_dhcp6_name_servers --tag "dhcpv6-$interface"
hostsd_changes=y
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 ad6a1d5eb..da1bda137 100644
--- a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup
+++ b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup
@@ -8,7 +8,7 @@ hostsd_changes=
/usr/bin/systemctl -q is-active vyos-hostsd
hostsd_status=$?
-if [[ $reason =~ (EXPIRE|FAIL|RELEASE|STOP) ]]; then
+if [[ $reason =~ ^(EXPIRE|FAIL|RELEASE|STOP)$ ]]; then
if [[ $hostsd_status -eq 0 ]]; then
# delete search domains and nameservers via vyos-hostsd
logmsg info "Deleting search domains with tag \"dhcp-$interface\" via vyos-hostsd-client"
@@ -96,7 +96,7 @@ if [[ $reason =~ (EXPIRE|FAIL|RELEASE|STOP) ]]; then
fi
fi
-if [[ $reason =~ (EXPIRE6|RELEASE6|STOP6) ]]; then
+if [[ $reason =~ ^(EXPIRE6|RELEASE6|STOP6)$ ]]; then
if [[ $hostsd_status -eq 0 ]]; then
# delete search domains and nameservers via vyos-hostsd
logmsg info "Deleting search domains with tag \"dhcpv6-$interface\" via vyos-hostsd-client"
diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook b/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook
index eeb8b0782..49bb18372 100644
--- a/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook
+++ b/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook
@@ -8,12 +8,12 @@
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 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.
-#
+#
# This code was originally developed by Vyatta, Inc.
# Portions created by Vyatta are Copyright (C) 2006, 2007, 2008 Vyatta, Inc.
# All Rights Reserved.
@@ -23,7 +23,7 @@
RUN="yes"
proto=""
-if [[ $reason =~ (REBOOT6|INIT6|EXPIRE6|RELEASE6|STOP6|INFORM6|BOUND6|REBIND6|DELEGATED6) ]]; then
+if [[ $reason =~ ^(REBOOT6|INIT6|EXPIRE6|RELEASE6|STOP6|INFORM6|BOUND6|REBIND6|DELEGATED6)$ ]]; then
proto="v6"
fi
diff --git a/src/etc/ppp/ip-down.d/98-vyos-pppoe-cleanup-nameservers b/src/etc/ppp/ip-down.d/98-vyos-pppoe-cleanup-nameservers
new file mode 100755
index 000000000..222c75f21
--- /dev/null
+++ b/src/etc/ppp/ip-down.d/98-vyos-pppoe-cleanup-nameservers
@@ -0,0 +1,15 @@
+#!/bin/bash
+### Autogenerated by interfaces-pppoe.py ###
+
+interface=$6
+if [ -z "$interface" ]; then
+ exit
+fi
+
+if ! /usr/bin/systemctl -q is-active vyos-hostsd; then
+ exit # vyos-hostsd is not running
+fi
+
+hostsd_client="/usr/bin/vyos-hostsd-client"
+$hostsd_client --delete-name-servers --tag "dhcp-$interface"
+$hostsd_client --apply
diff --git a/src/etc/ppp/ip-up.d/96-vyos-sstpc-callback b/src/etc/ppp/ip-up.d/96-vyos-sstpc-callback
new file mode 100755
index 000000000..4e8804f29
--- /dev/null
+++ b/src/etc/ppp/ip-up.d/96-vyos-sstpc-callback
@@ -0,0 +1,49 @@
+#!/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/>.
+
+# This is a Python hook script which is invoked whenever a SSTP client session
+# goes "ip-up". It will call into our vyos.ifconfig library and will then
+# execute common tasks for the SSTP interface. The reason we have to "hook" this
+# is that we can not create a sstpcX interface in advance in linux and then
+# connect pppd to this already existing interface.
+
+from sys import argv
+from sys import exit
+
+from vyos.configquery import ConfigTreeQuery
+from vyos.configdict import get_interface_dict
+from vyos.ifconfig import SSTPCIf
+
+# When the ppp link comes up, this script is called with the following
+# parameters
+# $1 the interface name used by pppd (e.g. ppp3)
+# $2 the tty device name
+# $3 the tty device speed
+# $4 the local IP address for the interface
+# $5 the remote IP address
+# $6 the parameter specified by the 'ipparam' option to pppd
+
+if (len(argv) < 7):
+ exit(1)
+
+interface = argv[6]
+
+conf = ConfigTreeQuery()
+_, sstpc = get_interface_dict(conf.config, ['interfaces', 'sstpc'], interface)
+
+# Update the config
+p = SSTPCIf(interface)
+p.update(sstpc)
diff --git a/src/etc/ppp/ip-up.d/98-vyos-pppoe-setup-nameservers b/src/etc/ppp/ip-up.d/98-vyos-pppoe-setup-nameservers
new file mode 100755
index 000000000..0fcedbedc
--- /dev/null
+++ b/src/etc/ppp/ip-up.d/98-vyos-pppoe-setup-nameservers
@@ -0,0 +1,24 @@
+#!/bin/bash
+### Autogenerated by interfaces-pppoe.py ###
+
+interface=$6
+if [ -z "$interface" ]; then
+ exit
+fi
+
+if ! /usr/bin/systemctl -q is-active vyos-hostsd; then
+ exit # vyos-hostsd is not running
+fi
+
+hostsd_client="/usr/bin/vyos-hostsd-client"
+
+$hostsd_client --delete-name-servers --tag "dhcp-$interface"
+
+if [ "$USEPEERDNS" ] && [ -n "$DNS1" ]; then
+$hostsd_client --add-name-servers "$DNS1" --tag "dhcp-$interface"
+fi
+if [ "$USEPEERDNS" ] && [ -n "$DNS2" ]; then
+$hostsd_client --add-name-servers "$DNS2" --tag "dhcp-$interface"
+fi
+
+$hostsd_client --apply
diff --git a/src/helpers/system-versions-foot.py b/src/helpers/system-versions-foot.py
index 2aa687221..9614f0d28 100755
--- a/src/helpers/system-versions-foot.py
+++ b/src/helpers/system-versions-foot.py
@@ -1,6 +1,6 @@
#!/usr/bin/python3
-# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2019, 2022 VyOS maintainers and contributors <maintainers@vyos.io>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -16,24 +16,13 @@
# along with this library. If not, see <http://www.gnu.org/licenses/>.
import sys
-import vyos.formatversions as formatversions
-import vyos.systemversions as systemversions
import vyos.defaults
-import vyos.version
-
-sys_versions = systemversions.get_system_component_version()
-
-component_string = formatversions.format_versions_string(sys_versions)
-
-os_version_string = vyos.version.get_version()
+from vyos.component_version import write_system_footer
sys.stdout.write("\n\n")
if vyos.defaults.cfg_vintage == 'vyos':
- formatversions.write_vyos_versions_foot(None, component_string,
- os_version_string)
+ write_system_footer(None, vintage='vyos')
elif vyos.defaults.cfg_vintage == 'vyatta':
- formatversions.write_vyatta_versions_foot(None, component_string,
- os_version_string)
+ write_system_footer(None, vintage='vyatta')
else:
- formatversions.write_vyatta_versions_foot(None, component_string,
- os_version_string)
+ write_system_footer(None, vintage='vyos')
diff --git a/src/helpers/vyos-domain-group-resolve.py b/src/helpers/vyos-domain-group-resolve.py
deleted file mode 100755
index 6b677670b..000000000
--- a/src/helpers/vyos-domain-group-resolve.py
+++ /dev/null
@@ -1,60 +0,0 @@
-#!/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(f'D_{set_name}', elements)
- time.sleep(timeout)
diff --git a/src/helpers/vyos-domain-resolver.py b/src/helpers/vyos-domain-resolver.py
new file mode 100755
index 000000000..e31d9238e
--- /dev/null
+++ b/src/helpers/vyos-domain-resolver.py
@@ -0,0 +1,183 @@
+#!/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
+import os
+import time
+
+from vyos.configdict import dict_merge
+from vyos.configquery import ConfigTreeQuery
+from vyos.firewall import fqdn_config_parse
+from vyos.firewall import fqdn_resolve
+from vyos.util import cmd
+from vyos.util import commit_in_progress
+from vyos.util import dict_search_args
+from vyos.util import run
+from vyos.xml import defaults
+
+base = ['firewall']
+timeout = 300
+cache = False
+
+domain_state = {}
+
+ipv4_tables = {
+ 'ip vyos_mangle',
+ 'ip vyos_filter',
+ 'ip vyos_nat'
+}
+
+ipv6_tables = {
+ 'ip6 vyos_mangle',
+ 'ip6 vyos_filter'
+}
+
+def get_config(conf):
+ firewall = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ default_values = defaults(base)
+ for tmp in ['name', 'ipv6_name']:
+ if tmp in default_values:
+ del default_values[tmp]
+
+ if 'zone' in default_values:
+ del default_values['zone']
+
+ firewall = dict_merge(default_values, firewall)
+
+ global timeout, cache
+
+ if 'resolver_interval' in firewall:
+ timeout = int(firewall['resolver_interval'])
+
+ if 'resolver_cache' in firewall:
+ cache = True
+
+ fqdn_config_parse(firewall)
+
+ return firewall
+
+def resolve(domains, ipv6=False):
+ global domain_state
+
+ ip_list = set()
+
+ for domain in domains:
+ resolved = fqdn_resolve(domain, ipv6=ipv6)
+
+ if resolved and cache:
+ domain_state[domain] = resolved
+ elif not resolved:
+ if domain not in domain_state:
+ continue
+ resolved = domain_state[domain]
+
+ ip_list = ip_list | resolved
+ return ip_list
+
+def nft_output(table, set_name, ip_list):
+ output = [f'flush set {table} {set_name}']
+ if ip_list:
+ ip_str = ','.join(ip_list)
+ output.append(f'add element {table} {set_name} {{ {ip_str} }}')
+ return output
+
+def nft_valid_sets():
+ try:
+ valid_sets = []
+ sets_json = cmd('nft -j list sets')
+ sets_obj = json.loads(sets_json)
+
+ for obj in sets_obj['nftables']:
+ if 'set' in obj:
+ family = obj['set']['family']
+ table = obj['set']['table']
+ name = obj['set']['name']
+ valid_sets.append((f'{family} {table}', name))
+
+ return valid_sets
+ except:
+ return []
+
+def update(firewall):
+ conf_lines = []
+ count = 0
+
+ valid_sets = nft_valid_sets()
+
+ domain_groups = dict_search_args(firewall, 'group', 'domain_group')
+ if domain_groups:
+ for set_name, domain_config in domain_groups.items():
+ if 'address' not in domain_config:
+ continue
+
+ nft_set_name = f'D_{set_name}'
+ domains = domain_config['address']
+
+ ip_list = resolve(domains, ipv6=False)
+ for table in ipv4_tables:
+ if (table, nft_set_name) in valid_sets:
+ conf_lines += nft_output(table, nft_set_name, ip_list)
+
+ ip6_list = resolve(domains, ipv6=True)
+ for table in ipv6_tables:
+ if (table, nft_set_name) in valid_sets:
+ conf_lines += nft_output(table, nft_set_name, ip6_list)
+ count += 1
+
+ for set_name, domain in firewall['ip_fqdn'].items():
+ table = 'ip vyos_filter'
+ nft_set_name = f'FQDN_{set_name}'
+
+ ip_list = resolve([domain], ipv6=False)
+
+ if (table, nft_set_name) in valid_sets:
+ conf_lines += nft_output(table, nft_set_name, ip_list)
+ count += 1
+
+ for set_name, domain in firewall['ip6_fqdn'].items():
+ table = 'ip6 vyos_filter'
+ nft_set_name = f'FQDN_{set_name}'
+
+ ip_list = resolve([domain], ipv6=True)
+ if (table, nft_set_name) in valid_sets:
+ conf_lines += nft_output(table, nft_set_name, ip_list)
+ count += 1
+
+ nft_conf_str = "\n".join(conf_lines) + "\n"
+ code = run(f'nft -f -', input=nft_conf_str)
+
+ print(f'Updated {count} sets - result: {code}')
+
+if __name__ == '__main__':
+ print(f'VyOS domain resolver')
+
+ count = 1
+ while commit_in_progress():
+ if ( count % 60 == 0 ):
+ print(f'Commit still in progress after {count}s - waiting')
+ count += 1
+ time.sleep(1)
+
+ conf = ConfigTreeQuery()
+ firewall = get_config(conf)
+
+ print(f'interval: {timeout}s - cache: {cache}')
+
+ while True:
+ update(firewall)
+ time.sleep(timeout)
diff --git a/src/helpers/vyos-failover.py b/src/helpers/vyos-failover.py
new file mode 100755
index 000000000..1ac193423
--- /dev/null
+++ b/src/helpers/vyos-failover.py
@@ -0,0 +1,184 @@
+#!/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 json
+import subprocess
+import socket
+import time
+
+from vyos.util import rc_cmd
+from pathlib import Path
+from systemd import journal
+
+
+my_name = Path(__file__).stem
+
+
+def get_best_route_options(route, debug=False):
+ """
+ Return current best route ('gateway, interface, metric)
+
+ % get_best_route_options('203.0.113.1')
+ ('192.168.0.1', 'eth1', 1)
+
+ % get_best_route_options('203.0.113.254')
+ (None, None, None)
+ """
+ rc, data = rc_cmd(f'ip --detail --json route show protocol failover {route}')
+ if rc == 0:
+ data = json.loads(data)
+ if len(data) == 0:
+ print(f'\nRoute {route} for protocol failover was not found')
+ return None, None, None
+ # Fake metric 999 by default
+ # Search route with the lowest metric
+ best_metric = 999
+ for entry in data:
+ if debug: print('\n', entry)
+ metric = entry.get('metric')
+ gateway = entry.get('gateway')
+ iface = entry.get('dev')
+ if metric < best_metric:
+ best_metric = metric
+ best_gateway = gateway
+ best_interface = iface
+ if debug:
+ print(f'### Best_route exists: {route}, best_gateway: {best_gateway}, '
+ f'best_metric: {best_metric}, best_iface: {best_interface}')
+ return best_gateway, best_interface, best_metric
+
+def is_port_open(ip, port):
+ """
+ Check connection to remote host and port
+ Return True if host alive
+
+ % is_port_open('example.com', 8080)
+ True
+ """
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP)
+ s.settimeout(2)
+ try:
+ s.connect((ip, int(port)))
+ s.shutdown(socket.SHUT_RDWR)
+ return True
+ except:
+ return False
+ finally:
+ s.close()
+
+def is_target_alive(target=None, iface='', proto='icmp', port=None, debug=False):
+ """
+ Host availability check by ICMP, ARP, TCP
+ Return True if target checks is successful
+
+ % is_target_alive('192.0.2.1', 'eth1', proto='arp')
+ True
+ """
+ if iface != '':
+ iface = f'-I {iface}'
+ if proto == 'icmp':
+ command = f'/usr/bin/ping -q {target} {iface} -n -c 2 -W 1'
+ rc, response = rc_cmd(command)
+ if debug: print(f' [ CHECK-TARGET ]: [{command}] -- return-code [RC: {rc}]')
+ if rc == 0:
+ return True
+ elif proto == 'arp':
+ command = f'/usr/bin/arping -b -c 2 -f -w 1 -i 1 {iface} {target}'
+ rc, response = rc_cmd(command)
+ if debug: print(f' [ CHECK-TARGET ]: [{command}] -- return-code [RC: {rc}]')
+ if rc == 0:
+ return True
+ elif proto == 'tcp' and port is not None:
+ return True if is_port_open(target, port) else False
+ else:
+ return False
+
+
+if __name__ == '__main__':
+ # Parse command arguments and get config
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-c',
+ '--config',
+ action='store',
+ help='Path to protocols failover configuration',
+ required=True,
+ type=Path)
+
+ args = parser.parse_args()
+ try:
+ config_path = Path(args.config)
+ config = json.loads(config_path.read_text())
+ except Exception as err:
+ print(
+ f'Configuration file "{config_path}" does not exist or malformed: {err}'
+ )
+ exit(1)
+
+ # Useful debug info to console, use debug = True
+ # sudo systemctl stop vyos-failover.service
+ # sudo /usr/libexec/vyos/vyos-failover.py --config /run/vyos-failover.conf
+ debug = False
+
+ while(True):
+
+ for route, route_config in config.get('route').items():
+
+ exists_route = exists_gateway, exists_iface, exists_metric = get_best_route_options(route, debug=debug)
+
+ for next_hop, nexthop_config in route_config.get('next_hop').items():
+ conf_iface = nexthop_config.get('interface')
+ conf_metric = int(nexthop_config.get('metric'))
+ port = nexthop_config.get('check').get('port')
+ port_opt = f'port {port}' if port else ''
+ proto = nexthop_config.get('check').get('type')
+ target = nexthop_config.get('check').get('target')
+ timeout = nexthop_config.get('check').get('timeout')
+
+ # Best route not fonund in the current routing table
+ if exists_route == (None, None, None):
+ if debug: print(f" [NEW_ROUTE_DETECTED] route: [{route}]")
+ # Add route if check-target alive
+ if is_target_alive(target, conf_iface, proto, port, debug=debug):
+ if debug: print(f' [ ADD ] -- ip route add {route} via {next_hop} dev {conf_iface} '
+ f'metric {conf_metric} proto failover\n###')
+ rc, command = rc_cmd(f'ip route add {route} via {next_hop} dev {conf_iface} '
+ f'metric {conf_metric} proto failover')
+ # If something is wrong and gateway not added
+ # Example: Error: Next-hop has invalid gateway.
+ if rc !=0:
+ if debug: print(f'{command} -- return-code [RC: {rc}] {next_hop} dev {conf_iface}')
+ else:
+ journal.send(f'ip route add {route} via {next_hop} dev {conf_iface} '
+ f'metric {conf_metric} proto failover', SYSLOG_IDENTIFIER=my_name)
+ else:
+ if debug: print(f' [ TARGET_FAIL ] target checks fails for [{target}], do nothing')
+ journal.send(f'Check fail for route {route} target {target} proto {proto} '
+ f'{port_opt}', SYSLOG_IDENTIFIER=my_name)
+
+ # Route was added, check if the target is alive
+ # We should delete route if check fails only if route exists in the routing table
+ if not is_target_alive(target, conf_iface, proto, port, debug=debug) and \
+ exists_route != (None, None, None):
+ if debug:
+ print(f'Nexh_hop {next_hop} fail, target not response')
+ print(f' [ DEL ] -- ip route del {route} via {next_hop} dev {conf_iface} '
+ f'metric {conf_metric} proto failover [DELETE]')
+ rc_cmd(f'ip route del {route} via {next_hop} dev {conf_iface} metric {conf_metric} proto failover')
+ journal.send(f'ip route del {route} via {next_hop} dev {conf_iface} '
+ f'metric {conf_metric} proto failover', SYSLOG_IDENTIFIER=my_name)
+
+ time.sleep(int(timeout))
diff --git a/src/migration-scripts/https/3-to-4 b/src/migration-scripts/https/3-to-4
new file mode 100755
index 000000000..5ee528b31
--- /dev/null
+++ b/src/migration-scripts/https/3-to-4
@@ -0,0 +1,53 @@
+#!/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/>.
+
+# T4768 rename node 'gql' to 'graphql'.
+
+import sys
+
+from vyos.configtree import ConfigTree
+
+if (len(sys.argv) < 2):
+ print("Must specify file name!")
+ sys.exit(1)
+
+file_name = sys.argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+
+old_base = ['service', 'https', 'api', 'gql']
+if not config.exists(old_base):
+ # Nothing to do
+ sys.exit(0)
+
+new_base = ['service', 'https', 'api', 'graphql']
+config.set(new_base)
+
+nodes = config.list_nodes(old_base)
+for node in nodes:
+ config.copy(old_base + [node], new_base + [node])
+
+config.delete(old_base)
+
+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))
+ sys.exit(1)
diff --git a/src/migration-scripts/policy/4-to-5 b/src/migration-scripts/policy/4-to-5
new file mode 100755
index 000000000..33c9e6ade
--- /dev/null
+++ b/src/migration-scripts/policy/4-to-5
@@ -0,0 +1,92 @@
+#!/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/>.
+
+# T2199: Migrate interface policy nodes to policy route <name> interface <ifname>
+
+import re
+
+from sys import argv
+from sys import exit
+
+from vyos.configtree import ConfigTree
+from vyos.ifconfig import Section
+
+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()
+
+base4 = ['policy', 'route']
+base6 = ['policy', 'route6']
+config = ConfigTree(config_file)
+
+if not config.exists(base4) and not config.exists(base6):
+ # Nothing to do
+ exit(0)
+
+def migrate_interface(config, iftype, ifname, vif=None, vifs=None, vifc=None):
+ if_path = ['interfaces', iftype, ifname]
+ ifname_full = ifname
+
+ if vif:
+ if_path += ['vif', vif]
+ ifname_full = f'{ifname}.{vif}'
+ elif vifs:
+ if_path += ['vif-s', vifs]
+ ifname_full = f'{ifname}.{vifs}'
+ if vifc:
+ if_path += ['vif-c', vifc]
+ ifname_full = f'{ifname}.{vifs}.{vifc}'
+
+ if not config.exists(if_path + ['policy']):
+ return
+
+ if config.exists(if_path + ['policy', 'route']):
+ route_name = config.return_value(if_path + ['policy', 'route'])
+ config.set(base4 + [route_name, 'interface'], value=ifname_full, replace=False)
+
+ if config.exists(if_path + ['policy', 'route6']):
+ route_name = config.return_value(if_path + ['policy', 'route6'])
+ config.set(base6 + [route_name, 'interface'], value=ifname_full, replace=False)
+
+ config.delete(if_path + ['policy'])
+
+for iftype in config.list_nodes(['interfaces']):
+ for ifname in config.list_nodes(['interfaces', iftype]):
+ migrate_interface(config, iftype, ifname)
+
+ if config.exists(['interfaces', iftype, ifname, 'vif']):
+ for vif in config.list_nodes(['interfaces', iftype, ifname, 'vif']):
+ migrate_interface(config, iftype, ifname, vif=vif)
+
+ if config.exists(['interfaces', iftype, ifname, 'vif-s']):
+ for vifs in config.list_nodes(['interfaces', iftype, ifname, 'vif-s']):
+ migrate_interface(config, iftype, ifname, vifs=vifs)
+
+ if config.exists(['interfaces', iftype, ifname, 'vif-s', vifs, 'vif-c']):
+ for vifc in config.list_nodes(['interfaces', iftype, ifname, 'vif-s', vifs, 'vif-c']):
+ migrate_interface(config, iftype, ifname, vifs=vifs, vifc=vifc)
+
+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/accelppp.py b/src/op_mode/accelppp.py
new file mode 100755
index 000000000..2fd045dc3
--- /dev/null
+++ b/src/op_mode/accelppp.py
@@ -0,0 +1,133 @@
+#!/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 sys
+
+import vyos.accel_ppp
+import vyos.opmode
+
+from vyos.configquery import ConfigTreeQuery
+from vyos.util import rc_cmd
+
+
+accel_dict = {
+ 'ipoe': {
+ 'port': 2002,
+ 'path': 'service ipoe-server'
+ },
+ 'pppoe': {
+ 'port': 2001,
+ 'path': 'service pppoe-server'
+ },
+ 'pptp': {
+ 'port': 2003,
+ 'path': 'vpn pptp'
+ },
+ 'l2tp': {
+ 'port': 2004,
+ 'path': 'vpn l2tp'
+ },
+ 'sstp': {
+ 'port': 2005,
+ 'path': 'vpn sstp'
+ }
+}
+
+
+def _get_raw_statistics(accel_output, pattern):
+ return vyos.accel_ppp.get_server_statistics(accel_output, pattern, sep=':')
+
+
+def _get_raw_sessions(port):
+ cmd_options = 'show sessions ifname,username,ip,ip6,ip6-dp,type,state,' \
+ 'uptime-raw,calling-sid,called-sid,sid,comp,rx-bytes-raw,' \
+ 'tx-bytes-raw,rx-pkts,tx-pkts'
+ output = vyos.accel_ppp.accel_cmd(port, cmd_options)
+ parsed_data: list[dict[str, str]] = vyos.accel_ppp.accel_out_parse(
+ output.splitlines())
+ return parsed_data
+
+
+def _verify(func):
+ """Decorator checks if accel-ppp protocol
+ ipoe/pppoe/pptp/l2tp/sstp is configured
+
+ for example:
+ service ipoe-server
+ vpn sstp
+ """
+ from functools import wraps
+
+ @wraps(func)
+ def _wrapper(*args, **kwargs):
+ config = ConfigTreeQuery()
+ protocol_list = accel_dict.keys()
+ protocol = kwargs.get('protocol')
+ # unknown or incorrect protocol query
+ if protocol not in protocol_list:
+ unconf_message = f'unknown protocol "{protocol}"'
+ raise vyos.opmode.UnconfiguredSubsystem(unconf_message)
+ # Check if config does not exist
+ config_protocol_path = accel_dict[protocol]['path']
+ if not config.exists(config_protocol_path):
+ unconf_message = f'"{config_protocol_path}" is not configured'
+ raise vyos.opmode.UnconfiguredSubsystem(unconf_message)
+ return func(*args, **kwargs)
+
+ return _wrapper
+
+
+@_verify
+def show_statistics(raw: bool, protocol: str):
+ """show accel-cmd statistics
+ CPU utilization and amount of sessions
+
+ protocol: ipoe/pppoe/ppptp/l2tp/sstp
+ """
+ pattern = f'{protocol}:'
+ port = accel_dict[protocol]['port']
+ rc, output = rc_cmd(f'/usr/bin/accel-cmd -p {port} show stat')
+
+ if raw:
+ return _get_raw_statistics(output, pattern)
+
+ return output
+
+
+@_verify
+def show_sessions(raw: bool, protocol: str):
+ """show accel-cmd sessions
+
+ protocol: ipoe/pppoe/ppptp/l2tp/sstp
+ """
+ port = accel_dict[protocol]['port']
+ if raw:
+ return _get_raw_sessions(port)
+
+ return vyos.accel_ppp.accel_cmd(port,
+ 'show sessions ifname,username,ip,ip6,ip6-dp,'
+ 'calling-sid,rate-limit,state,uptime,rx-bytes,tx-bytes')
+
+
+if __name__ == '__main__':
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except (ValueError, vyos.opmode.Error) as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/op_mode/bgp.py b/src/op_mode/bgp.py
new file mode 100755
index 000000000..23001a9d7
--- /dev/null
+++ b/src/op_mode/bgp.py
@@ -0,0 +1,120 @@
+#!/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/>.
+#
+# Purpose:
+# Displays bgp neighbors information.
+# Used by the "show bgp (vrf <tag>) ipv4|ipv6 neighbors" commands.
+
+import re
+import sys
+import typing
+
+import jmespath
+from jinja2 import Template
+from humps import decamelize
+
+from vyos.configquery import ConfigTreeQuery
+
+import vyos.opmode
+
+
+frr_command_template = Template("""
+{% if family %}
+ show bgp
+ {{ 'vrf ' ~ vrf if vrf else '' }}
+ {{ 'ipv6' if family == 'inet6' else 'ipv4'}}
+ {{ 'neighbor ' ~ peer if peer else 'summary' }}
+{% endif %}
+
+{% if raw %}
+ json
+{% endif %}
+""")
+
+
+def _verify(func):
+ """Decorator checks if BGP config exists
+ BGP configuration can be present under vrf <tag>
+ If we do npt get arg 'peer' then it can be 'bgp summary'
+ """
+ from functools import wraps
+
+ @wraps(func)
+ def _wrapper(*args, **kwargs):
+ config = ConfigTreeQuery()
+ afi = 'ipv6' if kwargs.get('family') == 'inet6' else 'ipv4'
+ global_vrfs = ['all', 'default']
+ peer = kwargs.get('peer')
+ vrf = kwargs.get('vrf')
+ unconf_message = f'BGP or neighbor is not configured'
+ # Add option to check the specific neighbor if we have arg 'peer'
+ peer_opt = f'neighbor {peer} address-family {afi}-unicast' if peer else ''
+ vrf_opt = ''
+ if vrf and vrf not in global_vrfs:
+ vrf_opt = f'vrf name {vrf}'
+ # Check if config does not exist
+ if not config.exists(f'{vrf_opt} protocols bgp {peer_opt}'):
+ raise vyos.opmode.UnconfiguredSubsystem(unconf_message)
+ return func(*args, **kwargs)
+
+ return _wrapper
+
+
+@_verify
+def show_neighbors(raw: bool,
+ family: str,
+ peer: typing.Optional[str],
+ vrf: typing.Optional[str]):
+ kwargs = dict(locals())
+ frr_command = frr_command_template.render(kwargs)
+ frr_command = re.sub(r'\s+', ' ', frr_command)
+
+ from vyos.util import cmd
+ output = cmd(f"vtysh -c '{frr_command}'")
+
+ if raw:
+ from json import loads
+ data = loads(output)
+ # Get list of the peers
+ peers = jmespath.search('*.peers | [0]', data)
+ if peers:
+ # Create new dict, delete old key 'peers'
+ # add key 'peers' neighbors to the list
+ list_peers = []
+ new_dict = jmespath.search('* | [0]', data)
+ if 'peers' in new_dict:
+ new_dict.pop('peers')
+
+ for neighbor, neighbor_options in peers.items():
+ neighbor_options['neighbor'] = neighbor
+ list_peers.append(neighbor_options)
+ new_dict['peers'] = list_peers
+ return decamelize(new_dict)
+ data = jmespath.search('* | [0]', data)
+ return decamelize(data)
+
+ else:
+ return output
+
+
+if __name__ == '__main__':
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except (ValueError, vyos.opmode.Error) as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/op_mode/bridge.py b/src/op_mode/bridge.py
index 5a821a287..d6098c158 100755
--- a/src/op_mode/bridge.py
+++ b/src/op_mode/bridge.py
@@ -32,7 +32,7 @@ def _get_json_data():
"""
Get bridge data format JSON
"""
- return cmd(f'sudo bridge --json link show')
+ return cmd(f'bridge --json link show')
def _get_raw_data_summary():
@@ -48,7 +48,7 @@ def _get_raw_data_vlan():
"""
:returns dict
"""
- json_data = cmd('sudo bridge --json --compressvlans vlan show')
+ json_data = cmd('bridge --json --compressvlans vlan show')
data_dict = json.loads(json_data)
return data_dict
@@ -57,7 +57,7 @@ def _get_raw_data_fdb(bridge):
"""Get MAC-address for the bridge brX
:returns list
"""
- code, json_data = rc_cmd(f'sudo bridge --json fdb show br {bridge}')
+ code, json_data = rc_cmd(f'bridge --json fdb show br {bridge}')
# From iproute2 fdb.c, fdb_show() will only exit(-1) in case of
# non-existent bridge device; raise error.
if code == 255:
diff --git a/src/op_mode/connect_disconnect.py b/src/op_mode/connect_disconnect.py
index 936c20bcb..d39e88bf3 100755
--- a/src/op_mode/connect_disconnect.py
+++ b/src/op_mode/connect_disconnect.py
@@ -41,7 +41,7 @@ def check_ppp_running(interface):
def connect(interface):
""" Connect dialer interface """
- if interface.startswith('ppp'):
+ if interface.startswith('pppoe') or interface.startswith('sstpc'):
check_ppp_interface(interface)
# Check if interface is already dialed
if os.path.isdir(f'/sys/class/net/{interface}'):
@@ -62,7 +62,7 @@ def connect(interface):
def disconnect(interface):
""" Disconnect dialer interface """
- if interface.startswith('ppp'):
+ if interface.startswith('pppoe') or interface.startswith('sstpc'):
check_ppp_interface(interface)
# Check if interface is already down
diff --git a/src/op_mode/dhcp.py b/src/op_mode/dhcp.py
new file mode 100755
index 000000000..07e9b7d6c
--- /dev/null
+++ b/src/op_mode/dhcp.py
@@ -0,0 +1,278 @@
+#!/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 sys
+from ipaddress import ip_address
+import typing
+
+from datetime import datetime
+from sys import exit
+from tabulate import tabulate
+from isc_dhcp_leases import IscDhcpLeases
+
+from vyos.base import Warning
+from vyos.configquery import ConfigTreeQuery
+
+from vyos.util import cmd
+from vyos.util import dict_search
+from vyos.util import is_systemd_service_running
+
+import vyos.opmode
+
+
+config = ConfigTreeQuery()
+pool_key = "shared-networkname"
+
+
+def _in_pool(lease, pool):
+ if pool_key in lease.sets:
+ if lease.sets[pool_key] == pool:
+ return True
+ return False
+
+
+def _utc_to_local(utc_dt):
+ return datetime.fromtimestamp((datetime.fromtimestamp(utc_dt) - datetime(1970, 1, 1)).total_seconds())
+
+
+def _format_hex_string(in_str):
+ out_str = ""
+ # if input is divisible by 2, add : every 2 chars
+ if len(in_str) > 0 and len(in_str) % 2 == 0:
+ out_str = ':'.join(a+b for a,b in zip(in_str[::2], in_str[1::2]))
+ else:
+ out_str = in_str
+
+ return out_str
+
+
+def _find_list_of_dict_index(lst, key='ip', value='') -> int:
+ """
+ Find the index entry of list of dict matching the dict value
+ Exampe:
+ % lst = [{'ip': '192.0.2.1'}, {'ip': '192.0.2.2'}]
+ % _find_list_of_dict_index(lst, key='ip', value='192.0.2.2')
+ % 1
+ """
+ idx = next((index for (index, d) in enumerate(lst) if d[key] == value), None)
+ return idx
+
+
+def _get_raw_server_leases(family, pool=None) -> list:
+ """
+ Get DHCP server leases
+ :return list
+ """
+ lease_file = '/config/dhcpdv6.leases' if family == 'inet6' else '/config/dhcpd.leases'
+ data = []
+ leases = IscDhcpLeases(lease_file).get()
+ if pool is not None:
+ if config.exists(f'service dhcp-server shared-network-name {pool}'):
+ leases = list(filter(lambda x: _in_pool(x, pool), leases))
+ for lease in leases:
+ data_lease = {}
+ data_lease['ip'] = lease.ip
+ data_lease['state'] = lease.binding_state
+ data_lease['pool'] = lease.sets.get('shared-networkname', '')
+ data_lease['end'] = lease.end.timestamp()
+
+ if family == 'inet':
+ data_lease['hardware'] = lease.ethernet
+ data_lease['start'] = lease.start.timestamp()
+ data_lease['hostname'] = lease.hostname
+
+ if family == 'inet6':
+ data_lease['last_communication'] = lease.last_communication.timestamp()
+ data_lease['iaid_duid'] = _format_hex_string(lease.host_identifier_string)
+ lease_types_long = {'na': 'non-temporary', 'ta': 'temporary', 'pd': 'prefix delegation'}
+ data_lease['type'] = lease_types_long[lease.type]
+
+ data_lease['remaining'] = lease.end - datetime.utcnow()
+
+ if data_lease['remaining'].days >= 0:
+ # substraction gives us a timedelta object which can't be formatted with strftime
+ # so we use str(), split gets rid of the microseconds
+ data_lease['remaining'] = str(data_lease["remaining"]).split('.')[0]
+ else:
+ data_lease['remaining'] = ''
+
+ # Do not add old leases
+ if data_lease['remaining'] != '':
+ data.append(data_lease)
+
+ # deduplicate
+ checked = []
+ for entry in data:
+ addr = entry.get('ip')
+ if addr not in checked:
+ checked.append(addr)
+ else:
+ idx = _find_list_of_dict_index(data, key='ip', value=addr)
+ data.pop(idx)
+
+ return data
+
+
+def _get_formatted_server_leases(raw_data, family):
+ data_entries = []
+ if family == 'inet':
+ for lease in raw_data:
+ ipaddr = lease.get('ip')
+ hw_addr = lease.get('hardware')
+ state = lease.get('state')
+ start = lease.get('start')
+ start = _utc_to_local(start).strftime('%Y/%m/%d %H:%M:%S')
+ end = lease.get('end')
+ end = _utc_to_local(end).strftime('%Y/%m/%d %H:%M:%S')
+ remain = lease.get('remaining')
+ pool = lease.get('pool')
+ hostname = lease.get('hostname')
+ data_entries.append([ipaddr, hw_addr, state, start, end, remain, pool, hostname])
+
+ headers = ['IP Address', 'Hardware address', 'State', 'Lease start', 'Lease expiration', 'Remaining', 'Pool',
+ 'Hostname']
+
+ if family == 'inet6':
+ for lease in raw_data:
+ ipaddr = lease.get('ip')
+ state = lease.get('state')
+ start = lease.get('last_communication')
+ start = _utc_to_local(start).strftime('%Y/%m/%d %H:%M:%S')
+ end = lease.get('end')
+ end = _utc_to_local(end).strftime('%Y/%m/%d %H:%M:%S')
+ remain = lease.get('remaining')
+ lease_type = lease.get('type')
+ pool = lease.get('pool')
+ host_identifier = lease.get('iaid_duid')
+ data_entries.append([ipaddr, state, start, end, remain, lease_type, pool, host_identifier])
+
+ headers = ['IPv6 address', 'State', 'Last communication', 'Lease expiration', 'Remaining', 'Type', 'Pool',
+ 'IAID_DUID']
+
+ output = tabulate(data_entries, headers, numalign='left')
+ return output
+
+
+def _get_dhcp_pools(family='inet') -> list:
+ v = 'v6' if family == 'inet6' else ''
+ pools = config.list_nodes(f'service dhcp{v}-server shared-network-name')
+ return pools
+
+
+def _get_pool_size(pool, family='inet'):
+ v = 'v6' if family == 'inet6' else ''
+ base = f'service dhcp{v}-server shared-network-name {pool}'
+ size = 0
+ subnets = config.list_nodes(f'{base} subnet')
+ for subnet in subnets:
+ if family == 'inet6':
+ ranges = config.list_nodes(f'{base} subnet {subnet} address-range start')
+ else:
+ ranges = config.list_nodes(f'{base} subnet {subnet} range')
+ for range in ranges:
+ if family == 'inet6':
+ start = config.list_nodes(f'{base} subnet {subnet} address-range start')[0]
+ stop = config.value(f'{base} subnet {subnet} address-range start {start} stop')
+ else:
+ start = config.value(f'{base} subnet {subnet} range {range} start')
+ stop = config.value(f'{base} subnet {subnet} range {range} stop')
+ # Add +1 because both range boundaries are inclusive
+ size += int(ip_address(stop)) - int(ip_address(start)) + 1
+ return size
+
+
+def _get_raw_pool_statistics(family='inet', pool=None):
+ if pool is None:
+ pool = _get_dhcp_pools(family=family)
+ else:
+ pool = [pool]
+
+ v = 'v6' if family == 'inet6' else ''
+ stats = []
+ for p in pool:
+ subnet = config.list_nodes(f'service dhcp{v}-server shared-network-name {p} subnet')
+ size = _get_pool_size(family=family, pool=p)
+ leases = len(_get_raw_server_leases(family=family, pool=p))
+ use_percentage = round(leases / size * 100) if size != 0 else 0
+ pool_stats = {'pool': p, 'size': size, 'leases': leases,
+ 'available': (size - leases), 'use_percentage': use_percentage, 'subnet': subnet}
+ stats.append(pool_stats)
+ return stats
+
+
+def _get_formatted_pool_statistics(pool_data, family='inet'):
+ data_entries = []
+ for entry in pool_data:
+ pool = entry.get('pool')
+ size = entry.get('size')
+ leases = entry.get('leases')
+ available = entry.get('available')
+ use_percentage = entry.get('use_percentage')
+ use_percentage = f'{use_percentage}%'
+ data_entries.append([pool, size, leases, available, use_percentage])
+
+ headers = ['Pool', 'Size','Leases', 'Available', 'Usage']
+ output = tabulate(data_entries, headers, numalign='left')
+ return output
+
+
+def _verify(func):
+ """Decorator checks if DHCP(v6) config exists"""
+ from functools import wraps
+
+ @wraps(func)
+ def _wrapper(*args, **kwargs):
+ config = ConfigTreeQuery()
+ family = kwargs.get('family')
+ v = 'v6' if family == 'inet6' else ''
+ unconf_message = f'DHCP{v} server is not configured'
+ # Check if config does not exist
+ if not config.exists(f'service dhcp{v}-server'):
+ raise vyos.opmode.UnconfiguredSubsystem(unconf_message)
+ return func(*args, **kwargs)
+ return _wrapper
+
+
+@_verify
+def show_pool_statistics(raw: bool, family: str, pool: typing.Optional[str]):
+ pool_data = _get_raw_pool_statistics(family=family, pool=pool)
+ if raw:
+ return pool_data
+ else:
+ return _get_formatted_pool_statistics(pool_data, family=family)
+
+
+@_verify
+def show_server_leases(raw: bool, family: str):
+ # if dhcp server is down, inactive leases may still be shown as active, so warn the user.
+ if not is_systemd_service_running('isc-dhcp-server.service'):
+ Warning('DHCP server is configured but not started. Data may be stale.')
+
+ leases = _get_raw_server_leases(family)
+ if raw:
+ return leases
+ else:
+ return _get_formatted_server_leases(leases, family)
+
+
+if __name__ == '__main__':
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except (ValueError, vyos.opmode.Error) as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/op_mode/dns.py b/src/op_mode/dns.py
index 9e5b1040c..a0e47d7ad 100755
--- a/src/op_mode/dns.py
+++ b/src/op_mode/dns.py
@@ -54,10 +54,10 @@ def _data_to_dict(data, sep="\t") -> dict:
def _get_raw_forwarding_statistics() -> dict:
- command = cmd('sudo /usr/bin/rec_control --socket-dir=/run/powerdns get-all')
+ command = cmd('rec_control --socket-dir=/run/powerdns get-all')
data = _data_to_dict(command)
data['cache-size'] = "{0:.2f}".format( int(
- cmd('sudo /usr/bin/rec_control --socket-dir=/run/powerdns get cache-bytes')) / 1024 )
+ cmd('rec_control --socket-dir=/run/powerdns get cache-bytes')) / 1024 )
return data
diff --git a/src/op_mode/firewall.py b/src/op_mode/firewall.py
index 950feb625..46bda5f7e 100755
--- a/src/op_mode/firewall.py
+++ b/src/op_mode/firewall.py
@@ -63,7 +63,7 @@ def get_config_firewall(conf, name=None, ipv6=False, interfaces=True):
get_first_key=True, no_tag_node_value_mangle=True)
if firewall and interfaces:
if name:
- firewall['interface'] = []
+ firewall['interface'] = {}
else:
if 'name' in firewall:
for fw_name, name_conf in firewall['name'].items():
diff --git a/src/op_mode/generate_ipsec_debug_archive.py b/src/op_mode/generate_ipsec_debug_archive.py
new file mode 100755
index 000000000..1422559a8
--- /dev/null
+++ b/src/op_mode/generate_ipsec_debug_archive.py
@@ -0,0 +1,89 @@
+#!/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 datetime import datetime
+from pathlib import Path
+from shutil import rmtree
+from socket import gethostname
+from sys import exit
+from tarfile import open as tar_open
+from vyos.util import rc_cmd
+
+# define a list of commands that needs to be executed
+CMD_LIST: list[str] = [
+ 'ipsec status',
+ 'swanctl -L',
+ 'swanctl -l',
+ 'swanctl -P',
+ 'ip x sa show',
+ 'ip x policy show',
+ 'ip tunnel show',
+ 'ip address',
+ 'ip rule show',
+ 'ip route | head -100',
+ 'ip route show table 220'
+]
+JOURNALCTL_CMD: str = 'journalctl -b -n 10000 /usr/lib/ipsec/charon'
+
+# execute a command and save the output to a file
+def save_stdout(command: str, file: Path) -> None:
+ rc, stdout = rc_cmd(command)
+ body: str = f'''### {command} ###
+Command: {command}
+Exit code: {rc}
+Stdout:
+{stdout}
+
+'''
+ with file.open(mode='a') as f:
+ f.write(body)
+
+
+# get local host name
+hostname: str = gethostname()
+# get current time
+time_now: str = datetime.now().isoformat(timespec='seconds')
+
+# define a temporary directory for logs and collected data
+tmp_dir: Path = Path(f'/tmp/ipsec_debug_{time_now}')
+# set file paths
+ipsec_status_file: Path = Path(f'{tmp_dir}/ipsec_status.txt')
+journalctl_charon_file: Path = Path(f'{tmp_dir}/journalctl_charon.txt')
+archive_file: str = f'/tmp/ipsec_debug_{time_now}.tar.bz2'
+
+# create files
+tmp_dir.mkdir()
+ipsec_status_file.touch()
+journalctl_charon_file.touch()
+
+try:
+ # execute all commands
+ for command in CMD_LIST:
+ save_stdout(command, ipsec_status_file)
+ save_stdout(JOURNALCTL_CMD, journalctl_charon_file)
+
+ # create an archive
+ with tar_open(name=archive_file, mode='x:bz2') as tar_file:
+ tar_file.add(tmp_dir)
+
+ # inform user about success
+ print(f'Debug file is generated and located in {archive_file}')
+except Exception as err:
+ print(f'Error during generating a debug file: {err}')
+finally:
+ # cleanup
+ rmtree(tmp_dir)
+ exit()
diff --git a/src/op_mode/generate_ipsec_debug_archive.sh b/src/op_mode/generate_ipsec_debug_archive.sh
deleted file mode 100755
index 53d0a6eaa..000000000
--- a/src/op_mode/generate_ipsec_debug_archive.sh
+++ /dev/null
@@ -1,36 +0,0 @@
-#!/usr/bin/env bash
-
-# Collecting IPSec Debug Information
-
-DATE=`date +%d-%m-%Y`
-
-a_CMD=(
- "sudo ipsec status"
- "sudo swanctl -L"
- "sudo swanctl -l"
- "sudo swanctl -P"
- "sudo ip x sa show"
- "sudo ip x policy show"
- "sudo ip tunnel show"
- "sudo ip address"
- "sudo ip rule show"
- "sudo ip route"
- "sudo ip route show table 220"
- )
-
-
-echo "DEBUG: ${DATE} on host \"$(hostname)\"" > /tmp/ipsec-status-${DATE}.txt
-date >> /tmp/ipsec-status-${DATE}.txt
-
-# Execute all DEBUG commands and save it to file
-for cmd in "${a_CMD[@]}"; do
- echo -e "\n### ${cmd} ###" >> /tmp/ipsec-status-${DATE}.txt
- ${cmd} >> /tmp/ipsec-status-${DATE}.txt 2>/dev/null
-done
-
-# Collect charon logs, build .tgz archive
-sudo journalctl /usr/lib/ipsec/charon > /tmp/journalctl-charon-${DATE}.txt && \
-sudo tar -zcvf /tmp/ipsec-debug-${DATE}.tgz /tmp/journalctl-charon-${DATE}.txt /tmp/ipsec-status-${DATE}.txt >& /dev/null
-sudo rm -f /tmp/journalctl-charon-${DATE}.txt /tmp/ipsec-status-${DATE}.txt
-
-echo "Debug file is generated and located in /tmp/ipsec-debug-${DATE}.tgz"
diff --git a/src/op_mode/ipsec.py b/src/op_mode/ipsec.py
index 7ec35d7bd..e0d204a0a 100755
--- a/src/op_mode/ipsec.py
+++ b/src/op_mode/ipsec.py
@@ -14,13 +14,16 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+import os
import re
import sys
+import typing
from collections import OrderedDict
from hurry import filesize
from re import split as re_split
from tabulate import tabulate
+from subprocess import TimeoutExpired
from vyos.util import call
from vyos.util import convert_data
@@ -43,7 +46,10 @@ def _alphanum_key(key):
def _get_vici_sas():
from vici import Session as vici_session
- session = vici_session()
+ try:
+ session = vici_session()
+ except Exception:
+ raise vyos.opmode.UnconfiguredSubsystem("IPsec not initialized")
sas = list(session.list_sas())
return sas
@@ -132,41 +138,305 @@ def _get_formatted_output_sas(sas):
return output
-def get_peer_connections(peer, tunnel, return_all = False):
+# Connections block
+def _get_vici_connections():
+ from vici import Session as vici_session
+
+ try:
+ session = vici_session()
+ except Exception:
+ raise vyos.opmode.UnconfiguredSubsystem("IPsec not initialized")
+ connections = list(session.list_conns())
+ return connections
+
+
+def _get_convert_data_connections():
+ get_connections = _get_vici_connections()
+ connections = convert_data(get_connections)
+ return connections
+
+
+def _get_parent_sa_proposal(connection_name: str, data: list) -> dict:
+ """Get parent SA proposals by connection name
+ if connections not in the 'down' state
+
+ Args:
+ connection_name (str): Connection name
+ data (list): List of current SAs from vici
+
+ Returns:
+ str: Parent SA connection proposal
+ AES_CBC/256/HMAC_SHA2_256_128/MODP_1024
+ """
+ if not data:
+ return {}
+ for sa in data:
+ # check if parent SA exist
+ if connection_name not in sa.keys():
+ return {}
+ if 'encr-alg' in sa[connection_name]:
+ encr_alg = sa.get(connection_name, '').get('encr-alg')
+ cipher = encr_alg.split('_')[0]
+ mode = encr_alg.split('_')[1]
+ encr_keysize = sa.get(connection_name, '').get('encr-keysize')
+ integ_alg = sa.get(connection_name, '').get('integ-alg')
+ # prf_alg = sa.get(connection_name, '').get('prf-alg')
+ dh_group = sa.get(connection_name, '').get('dh-group')
+ proposal = {
+ 'cipher': cipher,
+ 'mode': mode,
+ 'key_size': encr_keysize,
+ 'hash': integ_alg,
+ 'dh': dh_group
+ }
+ return proposal
+ return {}
+
+
+def _get_parent_sa_state(connection_name: str, data: list) -> str:
+ """Get parent SA state by connection name
+
+ Args:
+ connection_name (str): Connection name
+ data (list): List of current SAs from vici
+
+ Returns:
+ Parent SA connection state
+ """
+ if not data:
+ return 'down'
+ for sa in data:
+ # check if parent SA exist
+ if connection_name not in sa.keys():
+ return 'down'
+ if sa[connection_name]['state'].lower() == 'established':
+ return 'up'
+ else:
+ return 'down'
+
+
+def _get_child_sa_state(connection_name: str, tunnel_name: str,
+ data: list) -> str:
+ """Get child SA state by connection and tunnel name
+
+ Args:
+ connection_name (str): Connection name
+ tunnel_name (str): Tunnel name
+ data (list): List of current SAs from vici
+
+ Returns:
+ str: `up` if child SA state is 'installed' otherwise `down`
+ """
+ if not data:
+ return 'down'
+ for sa in data:
+ # check if parent SA exist
+ if connection_name not in sa.keys():
+ return 'down'
+ child_sas = sa[connection_name]['child-sas']
+ # Get all child SA states
+ # there can be multiple SAs per tunnel
+ child_sa_states = [
+ v['state'] for k, v in child_sas.items() if v['name'] == tunnel_name
+ ]
+ return 'up' if 'INSTALLED' in child_sa_states else 'down'
+
+
+def _get_child_sa_info(connection_name: str, tunnel_name: str,
+ data: list) -> dict:
+ """Get child SA installed info by connection and tunnel name
+
+ Args:
+ connection_name (str): Connection name
+ tunnel_name (str): Tunnel name
+ data (list): List of current SAs from vici
+
+ Returns:
+ dict: Info of the child SA in the dictionary format
+ """
+ for sa in data:
+ # check if parent SA exist
+ if connection_name not in sa.keys():
+ return {}
+ child_sas = sa[connection_name]['child-sas']
+ # Get all child SA data
+ # Skip temp SA name (first key), get only SA values as dict
+ # {'OFFICE-B-tunnel-0-46': {'name': 'OFFICE-B-tunnel-0'}...}
+ # i.e get all data after 'OFFICE-B-tunnel-0-46'
+ child_sa_info = [
+ v for k, v in child_sas.items() if 'name' in v and
+ v['name'] == tunnel_name and v['state'] == 'INSTALLED'
+ ]
+ return child_sa_info[-1] if child_sa_info else {}
+
+
+def _get_child_sa_proposal(child_sa_data: dict) -> dict:
+ if child_sa_data and 'encr-alg' in child_sa_data:
+ encr_alg = child_sa_data.get('encr-alg')
+ cipher = encr_alg.split('_')[0]
+ mode = encr_alg.split('_')[1]
+ key_size = child_sa_data.get('encr-keysize')
+ integ_alg = child_sa_data.get('integ-alg')
+ dh_group = child_sa_data.get('dh-group')
+ proposal = {
+ 'cipher': cipher,
+ 'mode': mode,
+ 'key_size': key_size,
+ 'hash': integ_alg,
+ 'dh': dh_group
+ }
+ return proposal
+ return {}
+
+
+def _get_raw_data_connections(list_connections: list, list_sas: list) -> list:
+ """Get configured VPN IKE connections and IPsec states
+
+ Args:
+ list_connections (list): List of configured connections from vici
+ list_sas (list): List of current SAs from vici
+
+ Returns:
+ list: List and status of IKE/IPsec connections/tunnels
+ """
+ base_dict = []
+ for connections in list_connections:
+ base_list = {}
+ for connection, conn_conf in connections.items():
+ base_list['ike_connection_name'] = connection
+ base_list['ike_connection_state'] = _get_parent_sa_state(
+ connection, list_sas)
+ base_list['ike_remote_address'] = conn_conf['remote_addrs']
+ base_list['ike_proposal'] = _get_parent_sa_proposal(
+ connection, list_sas)
+ base_list['local_id'] = conn_conf.get('local-1', '').get('id')
+ base_list['remote_id'] = conn_conf.get('remote-1', '').get('id')
+ base_list['version'] = conn_conf.get('version', 'IKE')
+ base_list['children'] = []
+ children = conn_conf['children']
+ for tunnel, tun_options in children.items():
+ state = _get_child_sa_state(connection, tunnel, list_sas)
+ local_ts = tun_options.get('local-ts')
+ remote_ts = tun_options.get('remote-ts')
+ dpd_action = tun_options.get('dpd_action')
+ close_action = tun_options.get('close_action')
+ sa_info = _get_child_sa_info(connection, tunnel, list_sas)
+ esp_proposal = _get_child_sa_proposal(sa_info)
+ base_list['children'].append({
+ 'name': tunnel,
+ 'state': state,
+ 'local_ts': local_ts,
+ 'remote_ts': remote_ts,
+ 'dpd_action': dpd_action,
+ 'close_action': close_action,
+ 'sa': sa_info,
+ 'esp_proposal': esp_proposal
+ })
+ base_dict.append(base_list)
+ return base_dict
+
+
+def _get_raw_connections_summary(list_conn, list_sas):
+ import jmespath
+ data = _get_raw_data_connections(list_conn, list_sas)
+ match = '[*].children[]'
+ child = jmespath.search(match, data)
+ tunnels_down = len([k for k in child if k['state'] == 'down'])
+ tunnels_up = len([k for k in child if k['state'] == 'up'])
+ tun_dict = {
+ 'tunnels': child,
+ 'total': len(child),
+ 'down': tunnels_down,
+ 'up': tunnels_up
+ }
+ return tun_dict
+
+
+def _get_formatted_output_conections(data):
+ from tabulate import tabulate
+ data_entries = ''
+ connections = []
+ for entry in data:
+ tunnels = []
+ ike_name = entry['ike_connection_name']
+ ike_state = entry['ike_connection_state']
+ conn_type = entry.get('version', 'IKE')
+ remote_addrs = ','.join(entry['ike_remote_address'])
+ local_ts, remote_ts = '-', '-'
+ local_id = entry['local_id']
+ remote_id = entry['remote_id']
+ proposal = '-'
+ if entry.get('ike_proposal'):
+ proposal = (f'{entry["ike_proposal"]["cipher"]}_'
+ f'{entry["ike_proposal"]["mode"]}/'
+ f'{entry["ike_proposal"]["key_size"]}/'
+ f'{entry["ike_proposal"]["hash"]}/'
+ f'{entry["ike_proposal"]["dh"]}')
+ connections.append([
+ ike_name, ike_state, conn_type, remote_addrs, local_ts, remote_ts,
+ local_id, remote_id, proposal
+ ])
+ for tun in entry['children']:
+ tun_name = tun.get('name')
+ tun_state = tun.get('state')
+ conn_type = 'IPsec'
+ local_ts = '\n'.join(tun.get('local_ts'))
+ remote_ts = '\n'.join(tun.get('remote_ts'))
+ proposal = '-'
+ if tun.get('esp_proposal'):
+ proposal = (f'{tun["esp_proposal"]["cipher"]}_'
+ f'{tun["esp_proposal"]["mode"]}/'
+ f'{tun["esp_proposal"]["key_size"]}/'
+ f'{tun["esp_proposal"]["hash"]}/'
+ f'{tun["esp_proposal"]["dh"]}')
+ connections.append([
+ tun_name, tun_state, conn_type, remote_addrs, local_ts,
+ remote_ts, local_id, remote_id, proposal
+ ])
+ connection_headers = [
+ 'Connection', 'State', 'Type', 'Remote address', 'Local TS',
+ 'Remote TS', 'Local id', 'Remote id', 'Proposal'
+ ]
+ output = tabulate(connections, connection_headers, numalign='left')
+ return output
+
+
+# Connections block end
+
+
+def get_peer_connections(peer, tunnel):
search = rf'^[\s]*({peer}-(tunnel-[\d]+|vti)).*'
matches = []
+ if not os.path.exists(SWANCTL_CONF):
+ raise vyos.opmode.UnconfiguredSubsystem("IPsec not initialized")
+ suffix = None if tunnel is None else (f'tunnel-{tunnel}' if
+ tunnel.isnumeric() else tunnel)
with open(SWANCTL_CONF, 'r') as f:
for line in f.readlines():
result = re.match(search, line)
if result:
- suffix = f'tunnel-{tunnel}' if tunnel.isnumeric() else tunnel
- if return_all or (result[2] == suffix):
+ if tunnel is None:
matches.append(result[1])
+ else:
+ if result[2] == suffix:
+ matches.append(result[1])
return matches
-def reset_peer(peer: str, tunnel:str):
- if not peer:
- print('Invalid peer, aborting')
- return
-
- conns = get_peer_connections(peer, tunnel, return_all = (not tunnel or tunnel == 'all'))
+def reset_peer(peer: str, tunnel:typing.Optional[str]):
+ conns = get_peer_connections(peer, tunnel)
if not conns:
- print('Tunnel(s) not found, aborting')
- return
+ raise vyos.opmode.IncorrectValue('Peer or tunnel(s) not found, aborting')
- result = True
for conn in conns:
try:
call(f'sudo /usr/sbin/ipsec down {conn}{{*}}', timeout = 10)
call(f'sudo /usr/sbin/ipsec up {conn}', timeout = 10)
except TimeoutExpired as e:
- print(f'Timed out while resetting {conn}')
- result = False
-
+ raise vyos.opmode.InternalError(f'Timed out while resetting {conn}')
- print('Peer reset result: ' + ('success' if result else 'failed'))
+ print('Peer reset result: success')
def show_sa(raw: bool):
@@ -176,6 +446,23 @@ def show_sa(raw: bool):
return _get_formatted_output_sas(sa_data)
+def show_connections(raw: bool):
+ list_conns = _get_convert_data_connections()
+ list_sas = _get_raw_data_sas()
+ if raw:
+ return _get_raw_data_connections(list_conns, list_sas)
+
+ connections = _get_raw_data_connections(list_conns, list_sas)
+ return _get_formatted_output_conections(connections)
+
+
+def show_connections_summary(raw: bool):
+ list_conns = _get_convert_data_connections()
+ list_sas = _get_raw_data_sas()
+ if raw:
+ return _get_raw_connections_summary(list_conns, list_sas)
+
+
if __name__ == '__main__':
try:
res = vyos.opmode.run(sys.modules[__name__])
diff --git a/src/op_mode/log.py b/src/op_mode/log.py
new file mode 100755
index 000000000..b0abd6191
--- /dev/null
+++ b/src/op_mode/log.py
@@ -0,0 +1,94 @@
+#!/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
+import re
+import sys
+import typing
+
+from jinja2 import Template
+
+from vyos.util import rc_cmd
+
+import vyos.opmode
+
+journalctl_command_template = Template("""
+--no-hostname
+--quiet
+
+{% if boot %}
+ --boot
+{% endif %}
+
+{% if count %}
+ --lines={{ count }}
+{% endif %}
+
+{% if reverse %}
+ --reverse
+{% endif %}
+
+{% if since %}
+ --since={{ since }}
+{% endif %}
+
+{% if unit %}
+ --unit={{ unit }}
+{% endif %}
+
+{% if utc %}
+ --utc
+{% endif %}
+
+{% if raw %}
+{# By default show 100 only lines for raw option if count does not set #}
+{# Protection from parsing the full log by default #}
+{% if not boot %}
+ --lines={{ '' ~ count if count else '100' }}
+{% endif %}
+ --no-pager
+ --output=json
+{% endif %}
+""")
+
+
+def show(raw: bool,
+ boot: typing.Optional[bool],
+ count: typing.Optional[int],
+ facility: typing.Optional[str],
+ reverse: typing.Optional[bool],
+ utc: typing.Optional[bool],
+ unit: typing.Optional[str]):
+ kwargs = dict(locals())
+
+ journalctl_options = journalctl_command_template.render(kwargs)
+ journalctl_options = re.sub(r'\s+', ' ', journalctl_options)
+ rc, output = rc_cmd(f'journalctl {journalctl_options}')
+ if raw:
+ # Each 'journalctl --output json' line is a separate JSON object
+ # So we should return list of dict
+ return [json.loads(line) for line in output.split('\n')]
+ return output
+
+
+if __name__ == '__main__':
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except (ValueError, vyos.opmode.Error) as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/op_mode/memory.py b/src/op_mode/memory.py
index 178544be4..7666de646 100755
--- a/src/op_mode/memory.py
+++ b/src/op_mode/memory.py
@@ -20,7 +20,7 @@ import sys
import vyos.opmode
-def _get_system_memory():
+def _get_raw_data():
from re import search as re_search
def find_value(keyword, mem_data):
@@ -38,7 +38,7 @@ def _get_system_memory():
used = total - available
- res = {
+ mem_data = {
"total": total,
"free": available,
"used": used,
@@ -46,24 +46,21 @@ def _get_system_memory():
"cached": cached
}
- return res
-
-def _get_system_memory_human():
- from vyos.util import bytes_to_human
-
- mem = _get_system_memory()
-
- for key in mem:
+ for key in mem_data:
# The Linux kernel exposes memory values in kilobytes,
# so we need to normalize them
- mem[key] = bytes_to_human(mem[key], initial_exponent=10)
+ mem_data[key] = mem_data[key] * 1024
- return mem
-
-def _get_raw_data():
- return _get_system_memory_human()
+ return mem_data
def _get_formatted_output(mem):
+ from vyos.util import bytes_to_human
+
+ # For human-readable outputs, we convert bytes to more convenient units
+ # (100M, 1.3G...)
+ for key in mem:
+ mem[key] = bytes_to_human(mem[key])
+
out = "Total: {}\n".format(mem["total"])
out += "Free: {}\n".format(mem["free"])
out += "Used: {}".format(mem["used"])
diff --git a/src/op_mode/nat.py b/src/op_mode/nat.py
index 845dbbb2c..f899eb3dc 100755
--- a/src/op_mode/nat.py
+++ b/src/op_mode/nat.py
@@ -22,12 +22,18 @@ import xmltodict
from sys import exit
from tabulate import tabulate
+from vyos.configquery import ConfigTreeQuery
+
from vyos.util import cmd
from vyos.util import dict_search
import vyos.opmode
+base = 'nat'
+unconf_message = 'NAT is not configured'
+
+
def _get_xml_translation(direction, family):
"""
Get conntrack XML output --src-nat|--dst-nat
@@ -277,6 +283,20 @@ def _get_formatted_translation(dict_data, nat_direction, family):
return output
+def _verify(func):
+ """Decorator checks if NAT config exists"""
+ from functools import wraps
+
+ @wraps(func)
+ def _wrapper(*args, **kwargs):
+ config = ConfigTreeQuery()
+ if not config.exists(base):
+ raise vyos.opmode.UnconfiguredSubsystem(unconf_message)
+ return func(*args, **kwargs)
+ return _wrapper
+
+
+@_verify
def show_rules(raw: bool, direction: str, family: str):
nat_rules = _get_raw_data_rules(direction, family)
if raw:
@@ -285,6 +305,7 @@ def show_rules(raw: bool, direction: str, family: str):
return _get_formatted_output_rules(nat_rules, direction, family)
+@_verify
def show_statistics(raw: bool, direction: str, family: str):
nat_statistics = _get_raw_data_rules(direction, family)
if raw:
@@ -293,6 +314,7 @@ def show_statistics(raw: bool, direction: str, family: str):
return _get_formatted_output_statistics(nat_statistics, direction)
+@_verify
def show_translations(raw: bool, direction: str, family: str):
family = 'ipv6' if family == 'inet6' else 'ipv4'
nat_translation = _get_raw_translation(direction, family)
diff --git a/src/op_mode/openconnect.py b/src/op_mode/openconnect.py
index 00992c66a..b21890728 100755
--- a/src/op_mode/openconnect.py
+++ b/src/op_mode/openconnect.py
@@ -31,14 +31,7 @@ occtl_socket = '/run/ocserv/occtl.socket'
def _get_raw_data_sessions():
rc, out = rc_cmd(f'sudo {occtl} --json --socket-file {occtl_socket} show users')
if rc != 0:
- output = {'openconnect':
- {
- 'configured': False,
- 'return_code': rc,
- 'reason': out
- }
- }
- return output
+ raise vyos.opmode.DataUnavailable(out)
sessions = json.loads(out)
return sessions
@@ -61,9 +54,8 @@ def _get_formatted_sessions(data):
def show_sessions(raw: bool):
config = ConfigTreeQuery()
- if not config.exists('vpn openconnect') and not raw:
- print('Openconnect is not configured')
- exit(0)
+ if not config.exists('vpn openconnect'):
+ raise vyos.opmode.UnconfiguredSubsystem('Openconnect is not configured')
openconnect_data = _get_raw_data_sessions()
if raw:
diff --git a/src/op_mode/openvpn.py b/src/op_mode/openvpn.py
new file mode 100755
index 000000000..3797a7153
--- /dev/null
+++ b/src/op_mode/openvpn.py
@@ -0,0 +1,220 @@
+#!/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
+import sys
+from tabulate import tabulate
+
+import vyos.opmode
+from vyos.util import bytes_to_human
+from vyos.util import commit_in_progress
+from vyos.util import call
+from vyos.config import Config
+
+def _get_tunnel_address(peer_host, peer_port, status_file):
+ peer = peer_host + ':' + peer_port
+ lst = []
+
+ with open(status_file, 'r') as f:
+ lines = f.readlines()
+ for line in lines:
+ if peer in line:
+ lst.append(line)
+
+ # filter out subnet entries if iroute:
+ # in the case that one sets, say:
+ # [ ..., 'vtun10', 'server', 'client', 'client1', 'subnet','10.10.2.0/25']
+ # the status file will have an entry:
+ # 10.10.2.0/25,client1,...
+ lst = [l for l in lst[1:] if '/' not in l.split(',')[0]]
+
+ tunnel_ip = lst[0].split(',')[0]
+
+ return tunnel_ip
+
+def _get_interface_status(mode: str, interface: str) -> dict:
+ status_file = f'/run/openvpn/{interface}.status'
+
+ data = {
+ 'mode': mode,
+ 'intf': interface,
+ 'local_host': '',
+ 'local_port': '',
+ 'date': '',
+ 'clients': [],
+ }
+
+ if not os.path.exists(status_file):
+ raise vyos.opmode.DataUnavailable('No information for interface {interface}')
+
+ with open(status_file, 'r') as f:
+ lines = f.readlines()
+ for line_no, line in enumerate(lines):
+ # remove trailing newline character first
+ line = line.rstrip('\n')
+
+ # check first line header
+ if line_no == 0:
+ if mode == 'server':
+ if not line == 'OpenVPN CLIENT LIST':
+ raise vyos.opmode.InternalError('Expected "OpenVPN CLIENT LIST"')
+ else:
+ if not line == 'OpenVPN STATISTICS':
+ raise vyos.opmode.InternalError('Expected "OpenVPN STATISTICS"')
+
+ continue
+
+ # second line informs us when the status file has been last updated
+ if line_no == 1:
+ data['date'] = line.lstrip('Updated,').rstrip('\n')
+ continue
+
+ if mode == 'server':
+ # for line_no > 1, lines appear as follows:
+ #
+ # Common Name,Real Address,Bytes Received,Bytes Sent,Connected Since
+ # client1,172.18.202.10:55904,2880587,2882653,Fri Aug 23 16:25:48 2019
+ # client3,172.18.204.10:41328,2850832,2869729,Fri Aug 23 16:25:43 2019
+ # client2,172.18.203.10:48987,2856153,2871022,Fri Aug 23 16:25:45 2019
+ # ...
+ # ROUTING TABLE
+ # ...
+ if line_no >= 3:
+ # indicator that there are no more clients
+ if line == 'ROUTING TABLE':
+ break
+ # otherwise, get client data
+ remote = (line.split(',')[1]).rsplit(':', maxsplit=1)
+
+ client = {
+ 'name': line.split(',')[0],
+ 'remote_host': remote[0],
+ 'remote_port': remote[1],
+ 'tunnel': 'N/A',
+ 'rx_bytes': bytes_to_human(int(line.split(',')[2]),
+ precision=1),
+ 'tx_bytes': bytes_to_human(int(line.split(',')[3]),
+ precision=1),
+ 'online_since': line.split(',')[4]
+ }
+ client['tunnel'] = _get_tunnel_address(client['remote_host'],
+ client['remote_port'],
+ status_file)
+ data['clients'].append(client)
+ continue
+ else: # mode == 'client' or mode == 'site-to-site'
+ if line_no == 2:
+ client = {
+ 'name': 'N/A',
+ 'remote_host': 'N/A',
+ 'remote_port': 'N/A',
+ 'tunnel': 'N/A',
+ 'rx_bytes': bytes_to_human(int(line.split(',')[1]),
+ precision=1),
+ 'tx_bytes': '',
+ 'online_since': 'N/A'
+ }
+ continue
+
+ if line_no == 3:
+ client['tx_bytes'] = bytes_to_human(int(line.split(',')[1]),
+ precision=1)
+ data['clients'].append(client)
+ break
+
+ return data
+
+def _get_raw_data(mode: str) -> dict:
+ data = {}
+ conf = Config()
+ conf_dict = conf.get_config_dict(['interfaces', 'openvpn'],
+ get_first_key=True)
+ if not conf_dict:
+ return data
+
+ interfaces = [x for x in list(conf_dict) if conf_dict[x]['mode'] == mode]
+ for intf in interfaces:
+ data[intf] = _get_interface_status(mode, intf)
+ d = data[intf]
+ d['local_host'] = conf_dict[intf].get('local-host', '')
+ d['local_port'] = conf_dict[intf].get('local-port', '')
+ if mode in ['client', 'site-to-site']:
+ for client in d['clients']:
+ if 'shared-secret-key-file' in list(conf_dict[intf]):
+ client['name'] = 'None (PSK)'
+ client['remote_host'] = conf_dict[intf].get('remote-host', [''])[0]
+ client['remote_port'] = conf_dict[intf].get('remote-port', '1194')
+
+ return data
+
+def _format_openvpn(data: dict) -> str:
+ if not data:
+ out = 'No OpenVPN interfaces configured'
+ return out
+
+ headers = ['Client CN', 'Remote Host', 'Tunnel IP', 'Local Host',
+ 'TX bytes', 'RX bytes', 'Connected Since']
+
+ out = ''
+ data_out = []
+ for intf in list(data):
+ l_host = data[intf]['local_host']
+ l_port = data[intf]['local_port']
+ for client in list(data[intf]['clients']):
+ r_host = client['remote_host']
+ r_port = client['remote_port']
+
+ out += f'\nOpenVPN status on {intf}\n\n'
+ name = client['name']
+ remote = r_host + ':' + r_port if r_host and r_port else 'N/A'
+ tunnel = client['tunnel']
+ local = l_host + ':' + l_port if l_host and l_port else 'N/A'
+ tx_bytes = client['tx_bytes']
+ rx_bytes = client['rx_bytes']
+ online_since = client['online_since']
+ data_out.append([name, remote, tunnel, local, tx_bytes,
+ rx_bytes, online_since])
+
+ out += tabulate(data_out, headers)
+
+ return out
+
+def show(raw: bool, mode: str) -> str:
+ openvpn_data = _get_raw_data(mode)
+
+ if raw:
+ return openvpn_data
+
+ return _format_openvpn(openvpn_data)
+
+def reset(interface: str):
+ if os.path.isfile(f'/run/openvpn/{interface}.conf'):
+ if commit_in_progress():
+ raise vyos.opmode.CommitInProgress('Retry OpenVPN reset: commit in progress.')
+ call(f'systemctl restart openvpn@{interface}.service')
+ else:
+ raise vyos.opmode.IncorrectValue(f'OpenVPN interface "{interface}" does not exist!')
+
+if __name__ == '__main__':
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except (ValueError, vyos.opmode.Error) as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/op_mode/ping.py b/src/op_mode/ping.py
index 60bbc0c78..610e63cb3 100755
--- a/src/op_mode/ping.py
+++ b/src/op_mode/ping.py
@@ -18,6 +18,25 @@ import os
import sys
import socket
import ipaddress
+from vyos.util import get_all_vrfs
+from vyos.ifconfig import Section
+
+
+def interface_list() -> list:
+ """
+ Get list of interfaces in system
+ :rtype: list
+ """
+ return Section.interfaces()
+
+
+def vrf_list() -> list:
+ """
+ Get list of VRFs in system
+ :rtype: list
+ """
+ return list(get_all_vrfs().keys())
+
options = {
'audible': {
@@ -63,6 +82,7 @@ options = {
'interface': {
'ping': '{command} -I {value}',
'type': '<interface>',
+ 'helpfunction': interface_list,
'help': 'Source interface'
},
'interval': {
@@ -128,6 +148,7 @@ options = {
'ping': 'sudo ip vrf exec {value} {command}',
'type': '<vrf>',
'help': 'Use specified VRF table',
+ 'helpfunction': vrf_list,
'dflt': 'default',
},
'verbose': {
@@ -142,20 +163,33 @@ ping = {
}
-class List (list):
- def first (self):
+class List(list):
+ def first(self):
return self.pop(0) if self else ''
def last(self):
return self.pop() if self else ''
- def prepend(self,value):
- self.insert(0,value)
+ def prepend(self, value):
+ self.insert(0, value)
+
+
+def completion_failure(option: str) -> None:
+ """
+ Shows failure message after TAB when option is wrong
+ :param option: failure option
+ :type str:
+ """
+ sys.stderr.write('\n\n Invalid option: {}\n\n'.format(option))
+ sys.stdout.write('<nocomps>')
+ sys.exit(1)
def expension_failure(option, completions):
reason = 'Ambiguous' if completions else 'Invalid'
- sys.stderr.write('\n\n {} command: {} [{}]\n\n'.format(reason,' '.join(sys.argv), option))
+ sys.stderr.write(
+ '\n\n {} command: {} [{}]\n\n'.format(reason, ' '.join(sys.argv),
+ option))
if completions:
sys.stderr.write(' Possible completions:\n ')
sys.stderr.write('\n '.join(completions))
@@ -196,28 +230,44 @@ if __name__ == '__main__':
if host == '--get-options':
args.first() # pop ping
args.first() # pop IP
+ usedoptionslist = []
while args:
- option = args.first()
-
- matched = complete(option)
+ option = args.first() # pop option
+ matched = complete(option) # get option parameters
+ usedoptionslist.append(option) # list of used options
+ # Select options
if not args:
+ # remove from Possible completions used options
+ for o in usedoptionslist:
+ if o in matched:
+ matched.remove(o)
sys.stdout.write(' '.join(matched))
sys.exit(0)
- if len(matched) > 1 :
+ if len(matched) > 1:
sys.stdout.write(' '.join(matched))
sys.exit(0)
+ # If option doesn't have value
+ if matched:
+ if options[matched[0]]['type'] == 'noarg':
+ continue
+ else:
+ # Unexpected option
+ completion_failure(option)
- if options[matched[0]]['type'] == 'noarg':
- continue
-
- value = args.first()
+ value = args.first() # pop option's value
if not args:
matched = complete(option)
- sys.stdout.write(options[matched[0]]['type'])
+ helplines = options[matched[0]]['type']
+ # Run helpfunction to get list of possible values
+ if 'helpfunction' in options[matched[0]]:
+ result = options[matched[0]]['helpfunction']()
+ if result:
+ helplines = '\n' + ' '.join(result)
+ sys.stdout.write(helplines)
sys.exit(0)
- for name,option in options.items():
+ for name, option in options.items():
if 'dflt' in option and name not in args:
args.append(name)
args.append(option['dflt'])
@@ -234,8 +284,7 @@ if __name__ == '__main__':
except ValueError:
sys.exit(f'ping: Unknown host: {host}')
- command = convert(ping[version],args)
+ command = convert(ping[version], args)
# print(f'{command} {host}')
os.system(f'{command} {host}')
-
diff --git a/src/op_mode/policy_route.py b/src/op_mode/policy_route.py
index 5be40082f..5953786f3 100755
--- a/src/op_mode/policy_route.py
+++ b/src/op_mode/policy_route.py
@@ -22,53 +22,13 @@ from vyos.config import Config
from vyos.util import cmd
from vyos.util import dict_search_args
-def get_policy_interfaces(conf, policy, name=None, ipv6=False):
- interfaces = conf.get_config_dict(['interfaces'], key_mangling=('-', '_'),
- get_first_key=True, no_tag_node_value_mangle=True)
-
- routes = ['route', 'route6']
-
- def parse_if(ifname, if_conf):
- if 'policy' in if_conf:
- for route in routes:
- if route in if_conf['policy']:
- route_name = if_conf['policy'][route]
- name_str = f'({ifname},{route})'
-
- if not name:
- policy[route][route_name]['interface'].append(name_str)
- elif not ipv6 and name == route_name:
- policy['interface'].append(name_str)
-
- for iftype in ['vif', 'vif_s', 'vif_c']:
- if iftype in if_conf:
- for vifname, vif_conf in if_conf[iftype].items():
- parse_if(f'{ifname}.{vifname}', vif_conf)
-
- for iftype, iftype_conf in interfaces.items():
- for ifname, if_conf in iftype_conf.items():
- parse_if(ifname, if_conf)
-
-def get_config_policy(conf, name=None, ipv6=False, interfaces=True):
+def get_config_policy(conf, name=None, ipv6=False):
config_path = ['policy']
if name:
config_path += ['route6' if ipv6 else 'route', name]
policy = conf.get_config_dict(config_path, key_mangling=('-', '_'),
get_first_key=True, no_tag_node_value_mangle=True)
- if policy and interfaces:
- if name:
- policy['interface'] = []
- else:
- if 'route' in policy:
- for route_name, route_conf in policy['route'].items():
- route_conf['interface'] = []
-
- if 'route6' in policy:
- for route_name, route_conf in policy['route6'].items():
- route_conf['interface'] = []
-
- get_policy_interfaces(conf, policy, name, ipv6)
return policy
diff --git a/src/op_mode/route.py b/src/op_mode/route.py
index e1eee5bbf..d07a34180 100755
--- a/src/op_mode/route.py
+++ b/src/op_mode/route.py
@@ -54,6 +54,18 @@ frr_command_template = Template("""
{% endif %}
""")
+def show_summary(raw: bool):
+ from vyos.util import cmd
+
+ if raw:
+ from json import loads
+
+ output = cmd(f"vtysh -c 'show ip route summary json'")
+ return loads(output)
+ else:
+ output = cmd(f"vtysh -c 'show ip route summary'")
+ return output
+
def show(raw: bool,
family: str,
net: typing.Optional[str],
@@ -83,7 +95,12 @@ def show(raw: bool,
if raw:
from json import loads
- return loads(output)
+ d = loads(output)
+ collect = []
+ for k,_ in d.items():
+ for l in d[k]:
+ collect.append(l)
+ return collect
else:
return output
diff --git a/src/op_mode/show_openvpn.py b/src/op_mode/show_openvpn.py
index 9a5adcffb..e29e594a5 100755
--- a/src/op_mode/show_openvpn.py
+++ b/src/op_mode/show_openvpn.py
@@ -59,7 +59,11 @@ def get_vpn_tunnel_address(peer, interface):
for line in lines:
if peer in line:
lst.append(line)
- tunnel_ip = lst[1].split(',')[0]
+
+ # filter out subnet entries
+ lst = [l for l in lst[1:] if '/' not in l.split(',')[0]]
+
+ tunnel_ip = lst[0].split(',')[0]
return tunnel_ip
diff --git a/src/op_mode/storage.py b/src/op_mode/storage.py
index 75964c493..d16e271bd 100755
--- a/src/op_mode/storage.py
+++ b/src/op_mode/storage.py
@@ -20,6 +20,16 @@ import sys
import vyos.opmode
from vyos.util import cmd
+# FIY: As of coreutils from Debian Buster and Bullseye,
+# the outpt looks like this:
+#
+# $ df -h -t ext4 --output=source,size,used,avail,pcent
+# Filesystem Size Used Avail Use%
+# /dev/sda1 16G 7.6G 7.3G 51%
+#
+# Those field names are automatically normalized by vyos.opmode.run,
+# so we don't touch them here,
+# and only normalize values.
def _get_system_storage(only_persistent=False):
if not only_persistent:
@@ -32,11 +42,19 @@ def _get_system_storage(only_persistent=False):
return res
def _get_raw_data():
+ from re import sub as re_sub
+ from vyos.util import human_to_bytes
+
out = _get_system_storage(only_persistent=True)
lines = out.splitlines()
lists = [l.split() for l in lines]
res = {lists[0][i]: lists[1][i] for i in range(len(lists[0]))}
+ res["Size"] = human_to_bytes(res["Size"])
+ res["Used"] = human_to_bytes(res["Used"])
+ res["Avail"] = human_to_bytes(res["Avail"])
+ res["Use%"] = re_sub(r'%', '', res["Use%"])
+
return res
def _get_formatted_output():
diff --git a/src/op_mode/traceroute.py b/src/op_mode/traceroute.py
index 4299d6e5f..6c7030ea0 100755
--- a/src/op_mode/traceroute.py
+++ b/src/op_mode/traceroute.py
@@ -18,6 +18,25 @@ import os
import sys
import socket
import ipaddress
+from vyos.util import get_all_vrfs
+from vyos.ifconfig import Section
+
+
+def interface_list() -> list:
+ """
+ Get list of interfaces in system
+ :rtype: list
+ """
+ return Section.interfaces()
+
+
+def vrf_list() -> list:
+ """
+ Get list of VRFs in system
+ :rtype: list
+ """
+ return list(get_all_vrfs().keys())
+
options = {
'backward-hops': {
@@ -48,6 +67,7 @@ options = {
'interface': {
'traceroute': '{command} -i {value}',
'type': '<interface>',
+ 'helpfunction': interface_list,
'help': 'Source interface'
},
'lookup-as': {
@@ -99,6 +119,7 @@ options = {
'traceroute': 'sudo ip vrf exec {value} {command}',
'type': '<vrf>',
'help': 'Use specified VRF table',
+ 'helpfunction': vrf_list,
'dflt': 'default'}
}
@@ -108,20 +129,33 @@ traceroute = {
}
-class List (list):
- def first (self):
+class List(list):
+ def first(self):
return self.pop(0) if self else ''
def last(self):
return self.pop() if self else ''
- def prepend(self,value):
- self.insert(0,value)
+ def prepend(self, value):
+ self.insert(0, value)
+
+
+def completion_failure(option: str) -> None:
+ """
+ Shows failure message after TAB when option is wrong
+ :param option: failure option
+ :type str:
+ """
+ sys.stderr.write('\n\n Invalid option: {}\n\n'.format(option))
+ sys.stdout.write('<nocomps>')
+ sys.exit(1)
def expension_failure(option, completions):
reason = 'Ambiguous' if completions else 'Invalid'
- sys.stderr.write('\n\n {} command: {} [{}]\n\n'.format(reason,' '.join(sys.argv), option))
+ sys.stderr.write(
+ '\n\n {} command: {} [{}]\n\n'.format(reason, ' '.join(sys.argv),
+ option))
if completions:
sys.stderr.write(' Possible completions:\n ')
sys.stderr.write('\n '.join(completions))
@@ -160,30 +194,46 @@ if __name__ == '__main__':
sys.exit("traceroute: Missing host")
if host == '--get-options':
- args.first() # pop traceroute
+ args.first() # pop ping
args.first() # pop IP
+ usedoptionslist = []
while args:
- option = args.first()
-
- matched = complete(option)
+ option = args.first() # pop option
+ matched = complete(option) # get option parameters
+ usedoptionslist.append(option) # list of used options
+ # Select options
if not args:
+ # remove from Possible completions used options
+ for o in usedoptionslist:
+ if o in matched:
+ matched.remove(o)
sys.stdout.write(' '.join(matched))
sys.exit(0)
- if len(matched) > 1 :
+ if len(matched) > 1:
sys.stdout.write(' '.join(matched))
sys.exit(0)
+ # If option doesn't have value
+ if matched:
+ if options[matched[0]]['type'] == 'noarg':
+ continue
+ else:
+ # Unexpected option
+ completion_failure(option)
- if options[matched[0]]['type'] == 'noarg':
- continue
-
- value = args.first()
+ value = args.first() # pop option's value
if not args:
matched = complete(option)
- sys.stdout.write(options[matched[0]]['type'])
+ helplines = options[matched[0]]['type']
+ # Run helpfunction to get list of possible values
+ if 'helpfunction' in options[matched[0]]:
+ result = options[matched[0]]['helpfunction']()
+ if result:
+ helplines = '\n' + ' '.join(result)
+ sys.stdout.write(helplines)
sys.exit(0)
- for name,option in options.items():
+ for name, option in options.items():
if 'dflt' in option and name not in args:
args.append(name)
args.append(option['dflt'])
@@ -200,8 +250,7 @@ if __name__ == '__main__':
except ValueError:
sys.exit(f'traceroute: Unknown host: {host}')
- command = convert(traceroute[version],args)
+ command = convert(traceroute[version], args)
# print(f'{command} {host}')
os.system(f'{command} {host}')
-
diff --git a/src/op_mode/vrf.py b/src/op_mode/vrf.py
index aeb50fe6e..a9a416761 100755
--- a/src/op_mode/vrf.py
+++ b/src/op_mode/vrf.py
@@ -31,14 +31,14 @@ def _get_raw_data(name=None):
If vrf name is set - get only this name data
If vrf name set and not found - return []
"""
- output = cmd('sudo ip --json --brief link show type vrf')
+ output = cmd('ip --json --brief link show type vrf')
data = json.loads(output)
if not data:
return []
if name:
is_vrf_exists = True if [vrf for vrf in data if vrf.get('ifname') == name] else False
if is_vrf_exists:
- output = cmd(f'sudo ip --json --brief link show dev {name}')
+ output = cmd(f'ip --json --brief link show dev {name}')
data = json.loads(output)
return data
return []
@@ -51,7 +51,7 @@ def _get_vrf_members(vrf: str) -> list:
:param vrf: str
:return: list
"""
- output = cmd(f'sudo ip --json --brief link show master {vrf}')
+ output = cmd(f'ip --json --brief link show master {vrf}')
answer = json.loads(output)
interfaces = []
for data in answer:
diff --git a/src/op_mode/webproxy_update_blacklist.sh b/src/op_mode/webproxy_update_blacklist.sh
index 43a4b79fc..4fb9a54c6 100755
--- a/src/op_mode/webproxy_update_blacklist.sh
+++ b/src/op_mode/webproxy_update_blacklist.sh
@@ -18,6 +18,23 @@ blacklist_url='ftp://ftp.univ-tlse1.fr/pub/reseau/cache/squidguard_contrib/black
data_dir="/opt/vyatta/etc/config/url-filtering"
archive="${data_dir}/squidguard/archive"
db_dir="${data_dir}/squidguard/db"
+conf_file="/etc/squidguard/squidGuard.conf"
+tmp_conf_file="/tmp/sg_update_db.conf"
+
+#$1-category
+#$2-type
+#$3-list
+create_sg_db ()
+{
+ FILE=$db_dir/$1/$2
+ if test -f "$FILE"; then
+ rm -f ${tmp_conf_file}
+ printf "dbhome $db_dir\ndest $1 {\n $3 $1/$2\n}\nacl {\n default {\n pass any\n }\n}" >> ${tmp_conf_file}
+ /usr/bin/squidGuard -b -c ${tmp_conf_file} -C $FILE
+ rm -f ${tmp_conf_file}
+ fi
+
+}
while [ $# -gt 0 ]
do
@@ -88,7 +105,17 @@ if [[ -n $update ]] && [[ $update -eq "yes" ]]; then
# fix permissions
chown -R proxy:proxy ${db_dir}
- chmod 2770 ${db_dir}
+
+ #create db
+ category_list=(`find $db_dir -type d -exec basename {} \; `)
+ for category in ${category_list[@]}
+ do
+ create_sg_db $category "domains" "domainlist"
+ create_sg_db $category "urls" "urllist"
+ create_sg_db $category "expressions" "expressionlist"
+ done
+ chown -R proxy:proxy ${db_dir}
+ chmod 755 ${db_dir}
logger --priority WARNING "webproxy blacklist entries updated (${count_before}/${count_after})"
diff --git a/src/services/api/graphql/__init__.py b/src/services/api/graphql/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/services/api/graphql/__init__.py
diff --git a/src/services/api/graphql/bindings.py b/src/services/api/graphql/bindings.py
index 0b1260912..aa1ba0eb0 100644
--- a/src/services/api/graphql/bindings.py
+++ b/src/services/api/graphql/bindings.py
@@ -18,16 +18,26 @@ from . graphql.queries import query
from . graphql.mutations import mutation
from . graphql.directives import directives_dict
from . graphql.errors import op_mode_error
-from . utils.schema_from_op_mode import generate_op_mode_definitions
+from . graphql.auth_token_mutation import auth_token_mutation
+from . generate.schema_from_op_mode import generate_op_mode_definitions
+from . generate.schema_from_config_session import generate_config_session_definitions
+from . generate.schema_from_composite import generate_composite_definitions
+from . libs.token_auth import init_secret
+from . import state
from ariadne import make_executable_schema, load_schema_from_path, snake_case_fallback_resolvers
def generate_schema():
api_schema_dir = vyos.defaults.directories['api_schema']
generate_op_mode_definitions()
+ generate_config_session_definitions()
+ generate_composite_definitions()
+
+ if state.settings['app'].state.vyos_auth_type == 'token':
+ init_secret()
type_defs = load_schema_from_path(api_schema_dir)
- schema = make_executable_schema(type_defs, query, op_mode_error, mutation, snake_case_fallback_resolvers, directives=directives_dict)
+ schema = make_executable_schema(type_defs, query, op_mode_error, mutation, auth_token_mutation, snake_case_fallback_resolvers, directives=directives_dict)
return schema
diff --git a/src/services/api/graphql/generate/composite_function.py b/src/services/api/graphql/generate/composite_function.py
new file mode 100644
index 000000000..bc9d80fbb
--- /dev/null
+++ b/src/services/api/graphql/generate/composite_function.py
@@ -0,0 +1,11 @@
+# typing information for composite functions: those that invoke several
+# elementary requests, and return the result as a single dict
+import typing
+
+def system_status():
+ pass
+
+queries = {'system_status': system_status}
+
+mutations = {}
+
diff --git a/src/services/api/graphql/utils/config_session_function.py b/src/services/api/graphql/generate/config_session_function.py
index fc0dd7a87..fc0dd7a87 100644
--- a/src/services/api/graphql/utils/config_session_function.py
+++ b/src/services/api/graphql/generate/config_session_function.py
diff --git a/src/services/api/graphql/generate/schema_from_composite.py b/src/services/api/graphql/generate/schema_from_composite.py
new file mode 100755
index 000000000..61a08cb2f
--- /dev/null
+++ b/src/services/api/graphql/generate/schema_from_composite.py
@@ -0,0 +1,165 @@
+#!/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/>.
+#
+#
+# A utility to generate GraphQL schema defintions from typing information of
+# composite functions comprising several requests.
+
+import os
+import sys
+import json
+from inspect import signature, getmembers, isfunction, isclass, getmro
+from jinja2 import Template
+
+from vyos.defaults import directories
+if __package__ is None or __package__ == '':
+ sys.path.append("/usr/libexec/vyos/services/api")
+ from graphql.libs.op_mode import snake_to_pascal_case, map_type_name
+ from composite_function import queries, mutations
+ from vyos.config import Config
+ from vyos.configdict import dict_merge
+ from vyos.xml import defaults
+else:
+ from .. libs.op_mode import snake_to_pascal_case, map_type_name
+ from . composite_function import queries, mutations
+ from .. import state
+
+SCHEMA_PATH = directories['api_schema']
+
+if __package__ is None or __package__ == '':
+ # allow running stand-alone
+ conf = Config()
+ base = ['service', 'https', 'api']
+ graphql_dict = conf.get_config_dict(base, key_mangling=('-', '_'),
+ no_tag_node_value_mangle=True,
+ get_first_key=True)
+ if 'graphql' not in graphql_dict:
+ exit("graphql is not configured")
+
+ graphql_dict = dict_merge(defaults(base), graphql_dict)
+ auth_type = graphql_dict['graphql']['authentication']['type']
+else:
+ auth_type = state.settings['app'].state.vyos_auth_type
+
+schema_data: dict = {'auth_type': auth_type,
+ 'schema_name': '',
+ 'schema_fields': []}
+
+query_template = """
+{%- if auth_type == 'key' %}
+input {{ schema_name }}Input {
+ key: String!
+ {%- for field_entry in schema_fields %}
+ {{ field_entry }}
+ {%- endfor %}
+}
+{%- elif schema_fields %}
+input {{ schema_name }}Input {
+ {%- for field_entry in schema_fields %}
+ {{ field_entry }}
+ {%- endfor %}
+}
+{%- endif %}
+
+type {{ schema_name }} {
+ result: Generic
+}
+
+type {{ schema_name }}Result {
+ data: {{ schema_name }}
+ success: Boolean!
+ errors: [String]
+}
+
+extend type Query {
+{%- if auth_type == 'key' or schema_fields %}
+ {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @compositequery
+{%- else %}
+ {{ schema_name }} : {{ schema_name }}Result @compositequery
+{%- endif %}
+}
+"""
+
+mutation_template = """
+{%- if auth_type == 'key' %}
+input {{ schema_name }}Input {
+ key: String!
+ {%- for field_entry in schema_fields %}
+ {{ field_entry }}
+ {%- endfor %}
+}
+{%- elif schema_fields %}
+input {{ schema_name }}Input {
+ {%- for field_entry in schema_fields %}
+ {{ field_entry }}
+ {%- endfor %}
+}
+{%- endif %}
+
+type {{ schema_name }} {
+ result: Generic
+}
+
+type {{ schema_name }}Result {
+ data: {{ schema_name }}
+ success: Boolean!
+ errors: [String]
+}
+
+extend type Mutation {
+{%- if auth_type == 'key' or schema_fields %}
+ {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @compositemutation
+{%- else %}
+ {{ schema_name }} : {{ schema_name }}Result @compositemutation
+{%- endif %}
+}
+"""
+
+def create_schema(func_name: str, func: callable, template: str) -> str:
+ sig = signature(func)
+
+ field_dict = {}
+ for k in sig.parameters:
+ field_dict[sig.parameters[k].name] = map_type_name(sig.parameters[k].annotation)
+
+ schema_fields = []
+ for k,v in field_dict.items():
+ schema_fields.append(k+': '+v)
+
+ schema_data['schema_name'] = snake_to_pascal_case(func_name)
+ schema_data['schema_fields'] = schema_fields
+
+ j2_template = Template(template)
+ res = j2_template.render(schema_data)
+
+ return res
+
+def generate_composite_definitions():
+ results = []
+ for name,func in queries.items():
+ res = create_schema(name, func, query_template)
+ results.append(res)
+
+ for name,func in mutations.items():
+ res = create_schema(name, func, mutation_template)
+ results.append(res)
+
+ out = '\n'.join(results)
+ with open(f'{SCHEMA_PATH}/composite.graphql', 'w') as f:
+ f.write(out)
+
+if __name__ == '__main__':
+ generate_composite_definitions()
diff --git a/src/services/api/graphql/utils/schema_from_config_session.py b/src/services/api/graphql/generate/schema_from_config_session.py
index ea78aaf88..49bf2440e 100755
--- a/src/services/api/graphql/utils/schema_from_config_session.py
+++ b/src/services/api/graphql/generate/schema_from_config_session.py
@@ -19,28 +19,60 @@
# (wrappers of) native configsession functions.
import os
+import sys
import json
from inspect import signature, getmembers, isfunction, isclass, getmro
from jinja2 import Template
+from vyos.defaults import directories
if __package__ is None or __package__ == '':
- from util import snake_to_pascal_case, map_type_name
+ sys.path.append("/usr/libexec/vyos/services/api")
+ from graphql.libs.op_mode import snake_to_pascal_case, map_type_name
+ from config_session_function import queries, mutations
+ from vyos.config import Config
+ from vyos.configdict import dict_merge
+ from vyos.xml import defaults
else:
- from . util import snake_to_pascal_case, map_type_name
+ from .. libs.op_mode import snake_to_pascal_case, map_type_name
+ from . config_session_function import queries, mutations
+ from .. import state
+
+SCHEMA_PATH = directories['api_schema']
-# this will be run locally before the build
-SCHEMA_PATH = '../graphql/schema'
+if __package__ is None or __package__ == '':
+ # allow running stand-alone
+ conf = Config()
+ base = ['service', 'https', 'api']
+ graphql_dict = conf.get_config_dict(base, key_mangling=('-', '_'),
+ no_tag_node_value_mangle=True,
+ get_first_key=True)
+ if 'graphql' not in graphql_dict:
+ exit("graphql is not configured")
+
+ graphql_dict = dict_merge(defaults(base), graphql_dict)
+ auth_type = graphql_dict['graphql']['authentication']['type']
+else:
+ auth_type = state.settings['app'].state.vyos_auth_type
-schema_data: dict = {'schema_name': '',
+schema_data: dict = {'auth_type': auth_type,
+ 'schema_name': '',
'schema_fields': []}
query_template = """
+{%- if auth_type == 'key' %}
input {{ schema_name }}Input {
key: String!
{%- for field_entry in schema_fields %}
{{ field_entry }}
{%- endfor %}
}
+{%- elif schema_fields %}
+input {{ schema_name }}Input {
+ {%- for field_entry in schema_fields %}
+ {{ field_entry }}
+ {%- endfor %}
+}
+{%- endif %}
type {{ schema_name }} {
result: Generic
@@ -53,17 +85,29 @@ type {{ schema_name }}Result {
}
extend type Query {
+{%- if auth_type == 'key' or schema_fields %}
{{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @configsessionquery
+{%- else %}
+ {{ schema_name }} : {{ schema_name }}Result @configsessionquery
+{%- endif %}
}
"""
mutation_template = """
+{%- if auth_type == 'key' %}
input {{ schema_name }}Input {
key: String!
{%- for field_entry in schema_fields %}
{{ field_entry }}
{%- endfor %}
}
+{%- elif schema_fields %}
+input {{ schema_name }}Input {
+ {%- for field_entry in schema_fields %}
+ {{ field_entry }}
+ {%- endfor %}
+}
+{%- endif %}
type {{ schema_name }} {
result: Generic
@@ -76,7 +120,11 @@ type {{ schema_name }}Result {
}
extend type Mutation {
+{%- if auth_type == 'key' or schema_fields %}
{{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @configsessionmutation
+{%- else %}
+ {{ schema_name }} : {{ schema_name }}Result @configsessionmutation
+{%- endif %}
}
"""
@@ -100,8 +148,6 @@ def create_schema(func_name: str, func: callable, template: str) -> str:
return res
def generate_config_session_definitions():
- from config_session_function import queries, mutations
-
results = []
for name,func in queries.items():
res = create_schema(name, func, query_template)
diff --git a/src/services/api/graphql/utils/schema_from_op_mode.py b/src/services/api/graphql/generate/schema_from_op_mode.py
index 57d63628b..fc63b0100 100755
--- a/src/services/api/graphql/utils/schema_from_op_mode.py
+++ b/src/services/api/graphql/generate/schema_from_op_mode.py
@@ -19,17 +19,24 @@
# scripts.
import os
+import sys
import json
from inspect import signature, getmembers, isfunction, isclass, getmro
from jinja2 import Template
from vyos.defaults import directories
+from vyos.util import load_as_module
if __package__ is None or __package__ == '':
- from util import load_as_module, is_op_mode_function_name, is_show_function_name
- from util import snake_to_pascal_case, map_type_name
+ sys.path.append("/usr/libexec/vyos/services/api")
+ from graphql.libs.op_mode import is_op_mode_function_name, is_show_function_name
+ from graphql.libs.op_mode import snake_to_pascal_case, map_type_name
+ from vyos.config import Config
+ from vyos.configdict import dict_merge
+ from vyos.xml import defaults
else:
- from . util import load_as_module, is_op_mode_function_name, is_show_function_name
- from . util import snake_to_pascal_case, map_type_name
+ from .. libs.op_mode import is_op_mode_function_name, is_show_function_name
+ from .. libs.op_mode import snake_to_pascal_case, map_type_name
+ from .. import state
OP_MODE_PATH = directories['op_mode']
SCHEMA_PATH = directories['api_schema']
@@ -38,16 +45,40 @@ DATA_DIR = directories['data']
op_mode_include_file = os.path.join(DATA_DIR, 'op-mode-standardized.json')
op_mode_error_schema = 'op_mode_error.graphql'
-schema_data: dict = {'schema_name': '',
+if __package__ is None or __package__ == '':
+ # allow running stand-alone
+ conf = Config()
+ base = ['service', 'https', 'api']
+ graphql_dict = conf.get_config_dict(base, key_mangling=('-', '_'),
+ no_tag_node_value_mangle=True,
+ get_first_key=True)
+ if 'graphql' not in graphql_dict:
+ exit("graphql is not configured")
+
+ graphql_dict = dict_merge(defaults(base), graphql_dict)
+ auth_type = graphql_dict['graphql']['authentication']['type']
+else:
+ auth_type = state.settings['app'].state.vyos_auth_type
+
+schema_data: dict = {'auth_type': auth_type,
+ 'schema_name': '',
'schema_fields': []}
query_template = """
+{%- if auth_type == 'key' %}
input {{ schema_name }}Input {
key: String!
{%- for field_entry in schema_fields %}
{{ field_entry }}
{%- endfor %}
}
+{%- elif schema_fields %}
+input {{ schema_name }}Input {
+ {%- for field_entry in schema_fields %}
+ {{ field_entry }}
+ {%- endfor %}
+}
+{%- endif %}
type {{ schema_name }} {
result: Generic
@@ -61,17 +92,29 @@ type {{ schema_name }}Result {
}
extend type Query {
+{%- if auth_type == 'key' or schema_fields %}
{{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @genopquery
+{%- else %}
+ {{ schema_name }} : {{ schema_name }}Result @genopquery
+{%- endif %}
}
"""
mutation_template = """
+{%- if auth_type == 'key' %}
input {{ schema_name }}Input {
key: String!
{%- for field_entry in schema_fields %}
{{ field_entry }}
{%- endfor %}
}
+{%- elif schema_fields %}
+input {{ schema_name }}Input {
+ {%- for field_entry in schema_fields %}
+ {{ field_entry }}
+ {%- endfor %}
+}
+{%- endif %}
type {{ schema_name }} {
result: Generic
@@ -85,7 +128,11 @@ type {{ schema_name }}Result {
}
extend type Mutation {
+{%- if auth_type == 'key' or schema_fields %}
{{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @genopmutation
+{%- else %}
+ {{ schema_name }} : {{ schema_name }}Result @genopquery
+{%- endif %}
}
"""
diff --git a/src/services/api/graphql/graphql/auth_token_mutation.py b/src/services/api/graphql/graphql/auth_token_mutation.py
new file mode 100644
index 000000000..21ac40094
--- /dev/null
+++ b/src/services/api/graphql/graphql/auth_token_mutation.py
@@ -0,0 +1,49 @@
+# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import jwt
+import datetime
+from typing import Any, Dict
+from ariadne import ObjectType, UnionType
+from graphql import GraphQLResolveInfo
+
+from .. libs.token_auth import generate_token
+from .. import state
+
+auth_token_mutation = ObjectType("Mutation")
+
+@auth_token_mutation.field('AuthToken')
+def auth_token_resolver(obj: Any, info: GraphQLResolveInfo, data: Dict):
+ # non-nullable fields
+ user = data['username']
+ passwd = data['password']
+
+ secret = state.settings['secret']
+ exp_interval = int(state.settings['app'].state.vyos_token_exp)
+ expiration = (datetime.datetime.now(tz=datetime.timezone.utc) +
+ datetime.timedelta(seconds=exp_interval))
+
+ res = generate_token(user, passwd, secret, expiration)
+ if res:
+ data['result'] = res
+ return {
+ "success": True,
+ "data": data
+ }
+
+ return {
+ "success": False,
+ "errors": ['token generation failed']
+ }
diff --git a/src/services/api/graphql/graphql/directives.py b/src/services/api/graphql/graphql/directives.py
index d75d72582..a7919854a 100644
--- a/src/services/api/graphql/graphql/directives.py
+++ b/src/services/api/graphql/graphql/directives.py
@@ -63,16 +63,25 @@ class GenOpMutationDirective(VyosDirective):
super().visit_field_definition(field, object_type,
make_resolver=make_gen_op_mutation_resolver)
-class SystemStatusDirective(VyosDirective):
+class CompositeQueryDirective(VyosDirective):
"""
Class providing implementation of 'system_status' directive in schema.
"""
def visit_field_definition(self, field, object_type):
super().visit_field_definition(field, object_type,
- make_resolver=make_system_status_resolver)
+ make_resolver=make_composite_query_resolver)
+
+class CompositeMutationDirective(VyosDirective):
+ """
+ Class providing implementation of 'system_status' directive in schema.
+ """
+ def visit_field_definition(self, field, object_type):
+ super().visit_field_definition(field, object_type,
+ make_resolver=make_composite_mutation_resolver)
directives_dict = {"configsessionquery": ConfigSessionQueryDirective,
"configsessionmutation": ConfigSessionMutationDirective,
"genopquery": GenOpQueryDirective,
"genopmutation": GenOpMutationDirective,
- "systemstatus": SystemStatusDirective}
+ "compositequery": CompositeQueryDirective,
+ "compositemutation": CompositeMutationDirective}
diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py
index f7d285a77..87ea59c43 100644
--- a/src/services/api/graphql/graphql/mutations.py
+++ b/src/services/api/graphql/graphql/mutations.py
@@ -14,13 +14,13 @@
# along with this library. If not, see <http://www.gnu.org/licenses/>.
from importlib import import_module
-from typing import Any, Dict
+from typing import Any, Dict, Optional
from ariadne import ObjectType, convert_kwargs_to_snake_case, convert_camel_case_to_snake
from graphql import GraphQLResolveInfo
from makefun import with_signature
from .. import state
-from .. import key_auth
+from .. libs import key_auth
from api.graphql.session.session import Session
from api.graphql.session.errors.op_mode_errors import op_mode_err_msg, op_mode_err_code
from vyos.opmode import Error as OpModeError
@@ -42,32 +42,52 @@ def make_mutation_resolver(mutation_name, class_name, session_func):
func_base_name = convert_camel_case_to_snake(class_name)
resolver_name = f'resolve_{func_base_name}'
- func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Dict)'
+ func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Optional[Dict]=None)'
@mutation.field(mutation_name)
@convert_kwargs_to_snake_case
@with_signature(func_sig, func_name=resolver_name)
async def func_impl(*args, **kwargs):
try:
- if 'data' not in kwargs:
- return {
- "success": False,
- "errors": ['missing data']
- }
-
- data = kwargs['data']
- key = data['key']
-
- auth = key_auth.auth_required(key)
- if auth is None:
- return {
- "success": False,
- "errors": ['invalid API key']
- }
-
- # We are finished with the 'key' entry, and may remove so as to
- # pass the rest of data (if any) to function.
- del data['key']
+ auth_type = state.settings['app'].state.vyos_auth_type
+
+ if auth_type == 'key':
+ data = kwargs['data']
+ key = data['key']
+
+ auth = key_auth.auth_required(key)
+ if auth is None:
+ return {
+ "success": False,
+ "errors": ['invalid API key']
+ }
+
+ # We are finished with the 'key' entry, and may remove so as to
+ # pass the rest of data (if any) to function.
+ del data['key']
+
+ elif auth_type == 'token':
+ data = kwargs['data']
+ if data is None:
+ data = {}
+ info = kwargs['info']
+ user = info.context.get('user')
+ if user is None:
+ error = info.context.get('error')
+ if error is not None:
+ return {
+ "success": False,
+ "errors": [error]
+ }
+ return {
+ "success": False,
+ "errors": ['not authenticated']
+ }
+ else:
+ # AtrributeError will have already been raised if no
+ # vyos_auth_type; validation and defaultValue ensure it is
+ # one of the previous cases, so this is never reached.
+ pass
session = state.settings['app'].state.vyos_session
@@ -112,3 +132,7 @@ def make_config_session_mutation_resolver(mutation_name):
def make_gen_op_mutation_resolver(mutation_name):
return make_mutation_resolver(mutation_name, mutation_name, 'gen_op_mutation')
+
+def make_composite_mutation_resolver(mutation_name):
+ return make_mutation_resolver(mutation_name, mutation_name,
+ convert_camel_case_to_snake(mutation_name))
diff --git a/src/services/api/graphql/graphql/queries.py b/src/services/api/graphql/graphql/queries.py
index 5f3a7d005..1ad586428 100644
--- a/src/services/api/graphql/graphql/queries.py
+++ b/src/services/api/graphql/graphql/queries.py
@@ -14,13 +14,13 @@
# along with this library. If not, see <http://www.gnu.org/licenses/>.
from importlib import import_module
-from typing import Any, Dict
+from typing import Any, Dict, Optional
from ariadne import ObjectType, convert_kwargs_to_snake_case, convert_camel_case_to_snake
from graphql import GraphQLResolveInfo
from makefun import with_signature
from .. import state
-from .. import key_auth
+from .. libs import key_auth
from api.graphql.session.session import Session
from api.graphql.session.errors.op_mode_errors import op_mode_err_msg, op_mode_err_code
from vyos.opmode import Error as OpModeError
@@ -42,32 +42,52 @@ def make_query_resolver(query_name, class_name, session_func):
func_base_name = convert_camel_case_to_snake(class_name)
resolver_name = f'resolve_{func_base_name}'
- func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Dict)'
+ func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Optional[Dict]=None)'
@query.field(query_name)
@convert_kwargs_to_snake_case
@with_signature(func_sig, func_name=resolver_name)
async def func_impl(*args, **kwargs):
try:
- if 'data' not in kwargs:
- return {
- "success": False,
- "errors": ['missing data']
- }
-
- data = kwargs['data']
- key = data['key']
-
- auth = key_auth.auth_required(key)
- if auth is None:
- return {
- "success": False,
- "errors": ['invalid API key']
- }
-
- # We are finished with the 'key' entry, and may remove so as to
- # pass the rest of data (if any) to function.
- del data['key']
+ auth_type = state.settings['app'].state.vyos_auth_type
+
+ if auth_type == 'key':
+ data = kwargs['data']
+ key = data['key']
+
+ auth = key_auth.auth_required(key)
+ if auth is None:
+ return {
+ "success": False,
+ "errors": ['invalid API key']
+ }
+
+ # We are finished with the 'key' entry, and may remove so as to
+ # pass the rest of data (if any) to function.
+ del data['key']
+
+ elif auth_type == 'token':
+ data = kwargs['data']
+ if data is None:
+ data = {}
+ info = kwargs['info']
+ user = info.context.get('user')
+ if user is None:
+ error = info.context.get('error')
+ if error is not None:
+ return {
+ "success": False,
+ "errors": [error]
+ }
+ return {
+ "success": False,
+ "errors": ['not authenticated']
+ }
+ else:
+ # AtrributeError will have already been raised if no
+ # vyos_auth_type; validation and defaultValue ensure it is
+ # one of the previous cases, so this is never reached.
+ pass
session = state.settings['app'].state.vyos_session
@@ -113,5 +133,6 @@ def make_config_session_query_resolver(query_name):
def make_gen_op_query_resolver(query_name):
return make_query_resolver(query_name, query_name, 'gen_op_query')
-def make_system_status_resolver(query_name):
- return make_query_resolver(query_name, query_name, 'system_status')
+def make_composite_query_resolver(query_name):
+ return make_query_resolver(query_name, query_name,
+ convert_camel_case_to_snake(query_name))
diff --git a/src/services/api/graphql/graphql/schema/auth_token.graphql b/src/services/api/graphql/graphql/schema/auth_token.graphql
new file mode 100644
index 000000000..af53a293a
--- /dev/null
+++ b/src/services/api/graphql/graphql/schema/auth_token.graphql
@@ -0,0 +1,19 @@
+
+input AuthTokenInput {
+ username: String!
+ password: String!
+}
+
+type AuthToken {
+ result: Generic
+}
+
+type AuthTokenResult {
+ data: AuthToken
+ success: Boolean!
+ errors: [String]
+}
+
+extend type Mutation {
+ AuthToken(data: AuthTokenInput) : AuthTokenResult
+}
diff --git a/src/services/api/graphql/graphql/schema/configsession.graphql b/src/services/api/graphql/graphql/schema/configsession.graphql
deleted file mode 100644
index b1deac4b3..000000000
--- a/src/services/api/graphql/graphql/schema/configsession.graphql
+++ /dev/null
@@ -1,115 +0,0 @@
-
-input ShowConfigInput {
- key: String!
- path: [String!]!
- configFormat: String = null
-}
-
-type ShowConfig {
- result: Generic
-}
-
-type ShowConfigResult {
- data: ShowConfig
- success: Boolean!
- errors: [String]
-}
-
-extend type Query {
- ShowConfig(data: ShowConfigInput) : ShowConfigResult @configsessionquery
-}
-
-input ShowInput {
- key: String!
- path: [String!]!
-}
-
-type Show {
- result: Generic
-}
-
-type ShowResult {
- data: Show
- success: Boolean!
- errors: [String]
-}
-
-extend type Query {
- Show(data: ShowInput) : ShowResult @configsessionquery
-}
-
-input SaveConfigFileInput {
- key: String!
- fileName: String = null
-}
-
-type SaveConfigFile {
- result: Generic
-}
-
-type SaveConfigFileResult {
- data: SaveConfigFile
- success: Boolean!
- errors: [String]
-}
-
-extend type Mutation {
- SaveConfigFile(data: SaveConfigFileInput) : SaveConfigFileResult @configsessionmutation
-}
-
-input LoadConfigFileInput {
- key: String!
- fileName: String!
-}
-
-type LoadConfigFile {
- result: Generic
-}
-
-type LoadConfigFileResult {
- data: LoadConfigFile
- success: Boolean!
- errors: [String]
-}
-
-extend type Mutation {
- LoadConfigFile(data: LoadConfigFileInput) : LoadConfigFileResult @configsessionmutation
-}
-
-input AddSystemImageInput {
- key: String!
- location: String!
-}
-
-type AddSystemImage {
- result: Generic
-}
-
-type AddSystemImageResult {
- data: AddSystemImage
- success: Boolean!
- errors: [String]
-}
-
-extend type Mutation {
- AddSystemImage(data: AddSystemImageInput) : AddSystemImageResult @configsessionmutation
-}
-
-input DeleteSystemImageInput {
- key: String!
- name: String!
-}
-
-type DeleteSystemImage {
- result: Generic
-}
-
-type DeleteSystemImageResult {
- data: DeleteSystemImage
- success: Boolean!
- errors: [String]
-}
-
-extend type Mutation {
- DeleteSystemImage(data: DeleteSystemImageInput) : DeleteSystemImageResult @configsessionmutation
-} \ No newline at end of file
diff --git a/src/services/api/graphql/graphql/schema/schema.graphql b/src/services/api/graphql/graphql/schema/schema.graphql
index 2acecade4..62b0d30bb 100644
--- a/src/services/api/graphql/graphql/schema/schema.graphql
+++ b/src/services/api/graphql/graphql/schema/schema.graphql
@@ -3,7 +3,8 @@ schema {
mutation: Mutation
}
-directive @systemstatus on FIELD_DEFINITION
+directive @compositequery on FIELD_DEFINITION
+directive @compositemutation on FIELD_DEFINITION
directive @configsessionquery on FIELD_DEFINITION
directive @configsessionmutation on FIELD_DEFINITION
directive @genopquery on FIELD_DEFINITION
@@ -11,8 +12,5 @@ directive @genopmutation on FIELD_DEFINITION
scalar Generic
-type Query {
- SystemStatus(data: SystemStatusInput) : SystemStatusResult @systemstatus
-}
-
+type Query
type Mutation
diff --git a/src/services/api/graphql/graphql/schema/system_status.graphql b/src/services/api/graphql/graphql/schema/system_status.graphql
deleted file mode 100644
index be8d87535..000000000
--- a/src/services/api/graphql/graphql/schema/system_status.graphql
+++ /dev/null
@@ -1,18 +0,0 @@
-"""
-Use 'scalar Generic' for system status output, to avoid attempts to
-JSON-serialize in case of JSON output.
-"""
-
-input SystemStatusInput {
- key: String!
-}
-
-type SystemStatus {
- result: Generic
-}
-
-type SystemStatusResult {
- data: SystemStatus
- success: Boolean!
- errors: [String]
-}
diff --git a/src/services/api/graphql/key_auth.py b/src/services/api/graphql/libs/key_auth.py
index f756ed6d8..2db0f7d48 100644
--- a/src/services/api/graphql/key_auth.py
+++ b/src/services/api/graphql/libs/key_auth.py
@@ -1,5 +1,5 @@
-from . import state
+from .. import state
def check_auth(key_list, key):
if not key_list:
diff --git a/src/services/api/graphql/utils/util.py b/src/services/api/graphql/libs/op_mode.py
index da2bcdb5b..6939ed5d6 100644
--- a/src/services/api/graphql/utils/util.py
+++ b/src/services/api/graphql/libs/op_mode.py
@@ -17,14 +17,12 @@ import os
import re
import typing
import importlib.util
+from typing import Union
+from humps import decamelize
from vyos.defaults import directories
-
-def load_as_module(name: str, path: str):
- spec = importlib.util.spec_from_file_location(name, path)
- mod = importlib.util.module_from_spec(spec)
- spec.loader.exec_module(mod)
- return mod
+from vyos.util import load_as_module
+from vyos.opmode import _normalize_field_names
def load_op_mode_as_module(name: str):
path = os.path.join(directories['op_mode'], name)
@@ -98,3 +96,6 @@ def map_type_name(type_name: type, optional: bool = False) -> str:
# scalar 'Generic' is defined in schema.graphql
return 'Generic'
+
+def normalize_output(result: Union[dict, list]) -> Union[dict, list]:
+ return _normalize_field_names(decamelize(result))
diff --git a/src/services/api/graphql/libs/token_auth.py b/src/services/api/graphql/libs/token_auth.py
new file mode 100644
index 000000000..2100eba7f
--- /dev/null
+++ b/src/services/api/graphql/libs/token_auth.py
@@ -0,0 +1,71 @@
+import jwt
+import uuid
+import pam
+from secrets import token_hex
+
+from .. import state
+
+def _check_passwd_pam(username: str, passwd: str) -> bool:
+ if pam.authenticate(username, passwd):
+ return True
+ return False
+
+def init_secret():
+ length = int(state.settings['app'].state.vyos_secret_len)
+ secret = token_hex(length)
+ state.settings['secret'] = secret
+
+def generate_token(user: str, passwd: str, secret: str, exp: int) -> dict:
+ if user is None or passwd is None:
+ return {}
+ if _check_passwd_pam(user, passwd):
+ app = state.settings['app']
+ try:
+ users = app.state.vyos_token_users
+ except AttributeError:
+ app.state.vyos_token_users = {}
+ users = app.state.vyos_token_users
+ user_id = uuid.uuid1().hex
+ payload_data = {'iss': user, 'sub': user_id, 'exp': exp}
+ secret = state.settings.get('secret')
+ if secret is None:
+ return {
+ "success": False,
+ "errors": ['failed secret generation']
+ }
+ token = jwt.encode(payload=payload_data, key=secret, algorithm="HS256")
+
+ users |= {user_id: user}
+ return {'token': token}
+
+def get_user_context(request):
+ context = {}
+ context['request'] = request
+ context['user'] = None
+ if 'Authorization' in request.headers:
+ auth = request.headers['Authorization']
+ scheme, token = auth.split()
+ if scheme.lower() != 'bearer':
+ return context
+
+ try:
+ secret = state.settings.get('secret')
+ payload = jwt.decode(token, secret, algorithms=["HS256"])
+ user_id: str = payload.get('sub')
+ if user_id is None:
+ return context
+ except jwt.exceptions.ExpiredSignatureError:
+ context['error'] = 'expired token'
+ return context
+ except jwt.PyJWTError:
+ return context
+ try:
+ users = state.settings['app'].state.vyos_token_users
+ except AttributeError:
+ return context
+
+ user = users.get(user_id)
+ if user is not None:
+ context['user'] = user
+
+ return context
diff --git a/src/services/api/graphql/session/composite/system_status.py b/src/services/api/graphql/session/composite/system_status.py
index 3c1a3d45b..d809f32e3 100755
--- a/src/services/api/graphql/session/composite/system_status.py
+++ b/src/services/api/graphql/session/composite/system_status.py
@@ -23,7 +23,7 @@ import importlib.util
from vyos.defaults import directories
-from api.graphql.utils.util import load_op_mode_as_module
+from api.graphql.libs.op_mode import load_op_mode_as_module
def get_system_version() -> dict:
show_version = load_op_mode_as_module('version.py')
diff --git a/src/services/api/graphql/session/errors/op_mode_errors.py b/src/services/api/graphql/session/errors/op_mode_errors.py
index 7ba75455d..7bc1d1d81 100644
--- a/src/services/api/graphql/session/errors/op_mode_errors.py
+++ b/src/services/api/graphql/session/errors/op_mode_errors.py
@@ -3,11 +3,13 @@
op_mode_err_msg = {
"UnconfiguredSubsystem": "subsystem is not configured or not running",
"DataUnavailable": "data currently unavailable",
- "PermissionDenied": "client does not have permission"
+ "PermissionDenied": "client does not have permission",
+ "IncorrectValue": "argument value is incorrect"
}
op_mode_err_code = {
"UnconfiguredSubsystem": 2000,
"DataUnavailable": 2001,
- "PermissionDenied": 1003
+ "PermissionDenied": 1003,
+ "IncorrectValue": 1002
}
diff --git a/src/services/api/graphql/session/session.py b/src/services/api/graphql/session/session.py
index f990e63d0..0b77b1433 100644
--- a/src/services/api/graphql/session/session.py
+++ b/src/services/api/graphql/session/session.py
@@ -24,7 +24,8 @@ from vyos.defaults import directories
from vyos.template import render
from vyos.opmode import Error as OpModeError
-from api.graphql.utils.util import load_op_mode_as_module, split_compound_op_mode_name
+from api.graphql.libs.op_mode import load_op_mode_as_module, split_compound_op_mode_name
+from api.graphql.libs.op_mode import normalize_output
op_mode_include_file = os.path.join(directories['data'], 'op-mode-standardized.json')
@@ -149,6 +150,8 @@ class Session:
except OpModeError as e:
raise e
+ res = normalize_output(res)
+
return res
def gen_op_mutation(self):
diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd
index 9ae7b1ea9..a380f2e66 100755
--- a/src/services/vyos-hostsd
+++ b/src/services/vyos-hostsd
@@ -406,8 +406,7 @@ def validate_schema(data):
def pdns_rec_control(command):
- # pdns-r process name is NOT equal to the name shown in ps
- if not process_named_running('pdns-r/worker'):
+ if not process_named_running('pdns_recursor'):
logger.info(f'pdns_recursor not running, not sending "{command}"')
return
diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server
index 4ace981ca..60ea9a5ee 100755
--- a/src/services/vyos-http-api-server
+++ b/src/services/vyos-http-api-server
@@ -647,21 +647,30 @@ def reset_op(data: ResetModel):
###
def graphql_init(fast_api_app):
- from api.graphql.bindings import generate_schema
-
+ from api.graphql.libs.token_auth import get_user_context
api.graphql.state.init()
api.graphql.state.settings['app'] = app
+ # import after initializaion of state
+ from api.graphql.bindings import generate_schema
schema = generate_schema()
in_spec = app.state.vyos_introspection
if app.state.vyos_origins:
origins = app.state.vyos_origins
- app.add_route('/graphql', CORSMiddleware(GraphQL(schema, debug=True, introspection=in_spec), allow_origins=origins, allow_methods=("GET", "POST", "OPTIONS")))
+ app.add_route('/graphql', CORSMiddleware(GraphQL(schema,
+ context_value=get_user_context,
+ debug=True,
+ introspection=in_spec),
+ allow_origins=origins,
+ allow_methods=("GET", "POST", "OPTIONS"),
+ allow_headers=("Authorization",)))
else:
- app.add_route('/graphql', GraphQL(schema, debug=True, introspection=in_spec))
-
+ app.add_route('/graphql', GraphQL(schema,
+ context_value=get_user_context,
+ debug=True,
+ introspection=in_spec))
###
if __name__ == '__main__':
@@ -688,16 +697,21 @@ if __name__ == '__main__':
app.state.vyos_debug = server_config['debug']
app.state.vyos_strict = server_config['strict']
app.state.vyos_origins = server_config.get('cors', {}).get('allow_origin', [])
- if 'gql' in server_config:
- app.state.vyos_gql = True
- if isinstance(server_config['gql'], dict) and 'introspection' in server_config['gql']:
- app.state.vyos_introspection = True
- else:
- app.state.vyos_introspection = False
+ if 'graphql' in server_config:
+ app.state.vyos_graphql = True
+ if isinstance(server_config['graphql'], dict):
+ if 'introspection' in server_config['graphql']:
+ app.state.vyos_introspection = True
+ else:
+ app.state.vyos_introspection = False
+ # default value is merged in conf_mode http-api.py, if not set
+ app.state.vyos_auth_type = server_config['graphql']['authentication']['type']
+ app.state.vyos_token_exp = server_config['graphql']['authentication']['expiration']
+ app.state.vyos_secret_len = server_config['graphql']['authentication']['secret_length']
else:
- app.state.vyos_gql = False
+ app.state.vyos_graphql = False
- if app.state.vyos_gql:
+ if app.state.vyos_graphql:
graphql_init(app)
try:
diff --git a/src/system/keepalived-fifo.py b/src/system/keepalived-fifo.py
index a0fccd1d0..864ee8419 100755
--- a/src/system/keepalived-fifo.py
+++ b/src/system/keepalived-fifo.py
@@ -67,13 +67,13 @@ class KeepalivedFifo:
# For VRRP configuration to be read, the commit must be finished
count = 1
while commit_in_progress():
- if ( count <= 40 ):
- logger.debug(f'commit in progress try: {count}')
+ if ( count <= 20 ):
+ logger.debug(f'Attempt to load keepalived configuration aborted due to a commit in progress (attempt {count}/20)')
else:
- logger.error(f'commit still in progress after {count} continuing anyway')
+ logger.error(f'Forced keepalived configuration loading despite a commit in progress ({count} wait time expired, not waiting further)')
break
count += 1
- time.sleep(0.5)
+ time.sleep(1)
try:
base = ['high-availability', 'vrrp']
diff --git a/src/systemd/vyos-domain-group-resolve.service b/src/systemd/vyos-domain-group-resolve.service
deleted file mode 100644
index 29628fddb..000000000
--- a/src/systemd/vyos-domain-group-resolve.service
+++ /dev/null
@@ -1,11 +0,0 @@
-[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-domain-resolver.service b/src/systemd/vyos-domain-resolver.service
new file mode 100644
index 000000000..c56b51f0c
--- /dev/null
+++ b/src/systemd/vyos-domain-resolver.service
@@ -0,0 +1,13 @@
+[Unit]
+Description=VyOS firewall domain resolver
+After=vyos-router.service
+
+[Service]
+Type=simple
+Restart=always
+ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/vyos-domain-resolver.py
+StandardError=journal
+StandardOutput=journal
+
+[Install]
+WantedBy=multi-user.target
diff --git a/src/tests/test_op_mode.py b/src/tests/test_op_mode.py
new file mode 100644
index 000000000..90963b3c5
--- /dev/null
+++ b/src/tests/test_op_mode.py
@@ -0,0 +1,65 @@
+#!/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 unittest import TestCase
+
+import vyos.opmode
+
+class TestVyOSOpMode(TestCase):
+ def test_field_name_normalization(self):
+ from vyos.opmode import _normalize_field_name
+
+ self.assertEqual(_normalize_field_name(" foo bar "), "foo_bar")
+ self.assertEqual(_normalize_field_name("foo-bar"), "foo_bar")
+ self.assertEqual(_normalize_field_name("foo (bar) baz"), "foo_bar_baz")
+ self.assertEqual(_normalize_field_name("load%"), "load_percentage")
+
+ def test_dict_fields_normalization_non_unique(self):
+ from vyos.opmode import _normalize_field_names
+
+ # Space and dot are both replaced by an underscore,
+ # so dicts like this cannor be normalized uniquely
+ data = {"foo bar": True, "foo.bar": False}
+
+ with self.assertRaises(vyos.opmode.InternalError):
+ _normalize_field_names(data)
+
+ def test_dict_fields_normalization_simple_dict(self):
+ from vyos.opmode import _normalize_field_names
+
+ data = {"foo bar": True, "Bar-Baz": False}
+ self.assertEqual(_normalize_field_names(data), {"foo_bar": True, "bar_baz": False})
+
+ def test_dict_fields_normalization_nested_dict(self):
+ from vyos.opmode import _normalize_field_names
+
+ data = {"foo bar": True, "bar-baz": {"baz-quux": {"quux-xyzzy": False}}}
+ self.assertEqual(_normalize_field_names(data),
+ {"foo_bar": True, "bar_baz": {"baz_quux": {"quux_xyzzy": False}}})
+
+ def test_dict_fields_normalization_mixed(self):
+ from vyos.opmode import _normalize_field_names
+
+ data = [{"foo bar": True, "bar-baz": [{"baz-quux": {"quux-xyzzy": [False]}}]}]
+ self.assertEqual(_normalize_field_names(data),
+ [{"foo_bar": True, "bar_baz": [{"baz_quux": {"quux_xyzzy": [False]}}]}])
+
+ def test_dict_fields_normalization_primitive(self):
+ from vyos.opmode import _normalize_field_names
+
+ data = [1, False, "foo"]
+ self.assertEqual(_normalize_field_names(data), [1, False, "foo"])
+
diff --git a/src/tests/test_util.py b/src/tests/test_util.py
index 8ac9a500a..d8b2b7940 100644
--- a/src/tests/test_util.py
+++ b/src/tests/test_util.py
@@ -26,3 +26,17 @@ class TestVyOSUtil(TestCase):
def test_sysctl_read(self):
self.assertEqual(sysctl_read('net.ipv4.conf.lo.forwarding'), '1')
+
+ def test_camel_to_snake_case(self):
+ self.assertEqual(camel_to_snake_case('ConnectionTimeout'),
+ 'connection_timeout')
+ self.assertEqual(camel_to_snake_case('connectionTimeout'),
+ 'connection_timeout')
+ self.assertEqual(camel_to_snake_case('TCPConnectionTimeout'),
+ 'tcp_connection_timeout')
+ self.assertEqual(camel_to_snake_case('TCPPort'),
+ 'tcp_port')
+ self.assertEqual(camel_to_snake_case('UseHTTPProxy'),
+ 'use_http_proxy')
+ self.assertEqual(camel_to_snake_case('CustomerID'),
+ 'customer_id')
diff --git a/src/validators/allowed-vlan b/src/validators/allowed-vlan
deleted file mode 100755
index 11389390b..000000000
--- a/src/validators/allowed-vlan
+++ /dev/null
@@ -1,19 +0,0 @@
-#! /usr/bin/python3
-
-import sys
-import re
-
-if __name__ == '__main__':
- if len(sys.argv)>1:
- allowed_vlan = sys.argv[1]
- if re.search('[0-9]{1,4}-[0-9]{1,4}', allowed_vlan):
- for tmp in allowed_vlan.split('-'):
- if int(tmp) not in range(1, 4095):
- sys.exit(1)
- else:
- if int(allowed_vlan) not in range(1, 4095):
- sys.exit(1)
- else:
- sys.exit(2)
-
- sys.exit(0)
diff --git a/src/validators/dotted-decimal b/src/validators/dotted-decimal
deleted file mode 100755
index 652110346..000000000
--- a/src/validators/dotted-decimal
+++ /dev/null
@@ -1,33 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2020 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 re
-import sys
-
-area = sys.argv[1]
-
-res = re.match(r'^(\d+)\.(\d+)\.(\d+)\.(\d+)$', area)
-if not res:
- print("\'{0}\' is not a valid dotted decimal value".format(area))
- sys.exit(1)
-else:
- components = res.groups()
- for n in range(0, 4):
- if (int(components[n]) > 255):
- print("Invalid component of a dotted decimal value: {0} exceeds 255".format(components[n]))
- sys.exit(1)
-
-sys.exit(0)
diff --git a/src/validators/file-exists b/src/validators/file-exists
deleted file mode 100755
index 5cef6b199..000000000
--- a/src/validators/file-exists
+++ /dev/null
@@ -1,61 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2019 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/>.
-#
-# Description:
-# Check if a given file exists on the system. Used for files that
-# are referenced from the CLI and need to be preserved during an image upgrade.
-# Warn the user if these aren't under /config
-
-import os
-import sys
-import argparse
-
-
-def exit(strict, message):
- if strict:
- sys.exit(f'ERROR: {message}')
- print(f'WARNING: {message}', file=sys.stderr)
- sys.exit()
-
-
-if __name__ == '__main__':
- parser = argparse.ArgumentParser()
- parser.add_argument("-d", "--directory", type=str, help="File must be present in this directory.")
- parser.add_argument("-e", "--error", action="store_true", help="Tread warnings as errors - change exit code to '1'")
- parser.add_argument("file", type=str, help="Path of file to validate")
-
- args = parser.parse_args()
-
- #
- # Always check if the given file exists
- #
- if not os.path.exists(args.file):
- exit(args.error, f"File '{args.file}' not found")
-
- #
- # Optional check if the file is under a certain directory path
- #
- if args.directory:
- # remove directory path from path to verify
- rel_filename = args.file.replace(args.directory, '').lstrip('/')
-
- if not os.path.exists(args.directory + '/' + rel_filename):
- exit(args.error,
- f"'{args.file}' lies outside of '{args.directory}' directory.\n"
- "It will not get preserved during image upgrade!"
- )
-
- sys.exit()
diff --git a/src/validators/fqdn b/src/validators/fqdn
index a4027e4ca..a65d2d5d4 100755
--- a/src/validators/fqdn
+++ b/src/validators/fqdn
@@ -1,27 +1,2 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2020-2021 VyOS maintainers and contributors
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 or later as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-import re
-import sys
-
-pattern = '[A-Za-z0-9][-.A-Za-z0-9]*'
-
-if __name__ == '__main__':
- if len(sys.argv) != 2:
- sys.exit(1)
- if not re.match(pattern, sys.argv[1]):
- sys.exit(1)
- sys.exit(0)
+#!/usr/bin/env sh
+${vyos_libexec_dir}/validate-value --regex "[A-Za-z0-9][-.A-Za-z0-9]*" --value "$1"
diff --git a/src/validators/interface-name b/src/validators/interface-name
deleted file mode 100755
index 105815eee..000000000
--- a/src/validators/interface-name
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2021 VyOS maintainers and contributors
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 or later as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-import os
-import re
-
-from sys import argv
-from sys import exit
-
-pattern = '^(bond|br|dum|en|ersp|eth|gnv|lan|l2tp|l2tpeth|macsec|peth|ppp|pppoe|pptp|sstp|tun|vti|vtun|vxlan|wg|wlan|wwan)[0-9]+(.\d+)?|lo$'
-
-if __name__ == '__main__':
- if len(argv) != 2:
- exit(1)
- interface = argv[1]
-
- if re.match(pattern, interface):
- exit(0)
- if os.path.exists(f'/sys/class/net/{interface}'):
- exit(0)
- exit(1)
diff --git a/src/validators/mac-address b/src/validators/mac-address
index 7d020f387..bb859a603 100755
--- a/src/validators/mac-address
+++ b/src/validators/mac-address
@@ -1,27 +1,2 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2018-2020 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 re
-import sys
-
-pattern = "^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$"
-
-if __name__ == '__main__':
- if len(sys.argv) != 2:
- sys.exit(1)
- if not re.match(pattern, sys.argv[1]):
- sys.exit(1)
- sys.exit(0)
+#!/usr/bin/env sh
+${vyos_libexec_dir}/validate-value --regex "([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})" --value "$1"
diff --git a/src/validators/mac-address-exclude b/src/validators/mac-address-exclude
new file mode 100755
index 000000000..c44913023
--- /dev/null
+++ b/src/validators/mac-address-exclude
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+${vyos_libexec_dir}/validate-value --regex "!([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})" --value "$1"
diff --git a/src/validators/mac-address-firewall b/src/validators/mac-address-firewall
deleted file mode 100755
index 70551f86d..000000000
--- a/src/validators/mac-address-firewall
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/usr/bin/env python3
-#
-# 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
-# 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 re
-import sys
-
-pattern = "^!?([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$"
-
-if __name__ == '__main__':
- if len(sys.argv) != 2:
- sys.exit(1)
- if not re.match(pattern, sys.argv[1]):
- sys.exit(1)
- sys.exit(0)
diff --git a/src/validators/tcp-flag b/src/validators/tcp-flag
deleted file mode 100755
index 1496b904a..000000000
--- a/src/validators/tcp-flag
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/usr/bin/python3
-
-import sys
-import re
-
-if __name__ == '__main__':
- if len(sys.argv)>1:
- flag = sys.argv[1]
- if flag and flag[0] == '!':
- flag = flag[1:]
- if flag not in ['syn', 'ack', 'rst', 'fin', 'urg', 'psh', 'ecn', 'cwr']:
- print(f'Error: {flag} is not a valid TCP flag')
- sys.exit(1)
- else:
- sys.exit(2)
-
- sys.exit(0)
diff --git a/src/xdp/common/common.mk b/src/xdp/common/common.mk
index ebe23a9ed..ffb86a65c 100644
--- a/src/xdp/common/common.mk
+++ b/src/xdp/common/common.mk
@@ -39,7 +39,7 @@ KERN_USER_H ?= $(wildcard common_kern_user.h)
CFLAGS ?= -g -I../include/
BPF_CFLAGS ?= -I../include/
-LIBS = -l:libbpf.a -lelf $(USER_LIBS)
+LIBS = -lbpf -lelf $(USER_LIBS)
all: llvm-check $(USER_TARGETS) $(XDP_OBJ) $(COPY_LOADER) $(COPY_STATS)
diff --git a/src/xdp/common/common_user_bpf_xdp.c b/src/xdp/common/common_user_bpf_xdp.c
index e7ef77174..faf7f4f91 100644
--- a/src/xdp/common/common_user_bpf_xdp.c
+++ b/src/xdp/common/common_user_bpf_xdp.c
@@ -274,7 +274,7 @@ struct bpf_object *load_bpf_and_xdp_attach(struct config *cfg)
exit(EXIT_FAIL_BPF);
}
- strncpy(cfg->progsec, bpf_program__title(bpf_prog, false), sizeof(cfg->progsec));
+ strncpy(cfg->progsec, bpf_program__section_name(bpf_prog), sizeof(cfg->progsec));
prog_fd = bpf_program__fd(bpf_prog);
if (prog_fd <= 0) {