summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
Diffstat (limited to 'python')
-rw-r--r--python/vyos/configdict.py12
-rwxr-xr-xpython/vyos/firewall.py21
-rw-r--r--python/vyos/ifconfig/wireguard.py160
-rwxr-xr-xpython/vyos/template.py2
-rw-r--r--python/vyos/utils/cpu.py6
-rw-r--r--python/vyos/utils/network.py31
-rw-r--r--python/vyos/utils/process.py48
7 files changed, 186 insertions, 94 deletions
diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
index ff0a15933..a34b0176a 100644
--- a/python/vyos/configdict.py
+++ b/python/vyos/configdict.py
@@ -661,6 +661,7 @@ def get_accel_dict(config, base, chap_secrets, with_pki=False):
Return a dictionary with the necessary interface config keys.
"""
from vyos.utils.cpu import get_core_count
+ from vyos.utils.cpu import get_half_cpus
from vyos.template import is_ipv4
dict = config.get_config_dict(base, key_mangling=('-', '_'),
@@ -670,7 +671,16 @@ def get_accel_dict(config, base, chap_secrets, with_pki=False):
with_pki=with_pki)
# set CPUs cores to process requests
- dict.update({'thread_count' : get_core_count()})
+ match dict.get('thread_count'):
+ case 'all':
+ dict['thread_count'] = get_core_count()
+ case 'half':
+ dict['thread_count'] = get_half_cpus()
+ case str(x) if x.isdigit():
+ dict['thread_count'] = int(x)
+ case _:
+ dict['thread_count'] = get_core_count()
+
# we need to store the path to the secrets file
dict.update({'chap_secrets_file' : chap_secrets})
diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py
index 9c320c82d..64022db84 100755
--- a/python/vyos/firewall.py
+++ b/python/vyos/firewall.py
@@ -319,7 +319,10 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
if group_name[0] == '!':
operator = '!='
group_name = group_name[1:]
- output.append(f'{ip_name} {prefix}addr {operator} @R_{group_name}')
+ if ip_name == 'ip':
+ output.append(f'{ip_name} {prefix}addr {operator} @R_{group_name}')
+ elif ip_name == 'ip6':
+ output.append(f'{ip_name} {prefix}addr {operator} @R6_{group_name}')
if 'mac_group' in group:
group_name = group['mac_group']
operator = ''
@@ -471,14 +474,14 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
output.append('gre version 1')
if gre_key:
- # The offset of the key within the packet shifts depending on the C-flag.
- # nftables cannot handle complex enough expressions to match multiple
+ # The offset of the key within the packet shifts depending on the C-flag.
+ # nftables cannot handle complex enough expressions to match multiple
# offsets based on bitfields elsewhere.
- # We enforce a specific match for the checksum flag in validation, so the
- # gre_flags dict will always have a 'checksum' key when gre_key is populated.
- if not gre_flags['checksum']:
+ # We enforce a specific match for the checksum flag in validation, so the
+ # gre_flags dict will always have a 'checksum' key when gre_key is populated.
+ if not gre_flags['checksum']:
# No "unset" child node means C is set, we offset key lookup +32 bits
- output.append(f'@th,64,32 == {gre_key}')
+ output.append(f'@th,64,32 == {gre_key}')
else:
output.append(f'@th,32,32 == {gre_key}')
@@ -637,7 +640,7 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
return " ".join(output)
def parse_gre_flags(flags, force_keyed=False):
- flag_map = { # nft does not have symbolic names for these.
+ flag_map = { # nft does not have symbolic names for these.
'checksum': 1<<0,
'routing': 1<<1,
'key': 1<<2,
@@ -648,7 +651,7 @@ def parse_gre_flags(flags, force_keyed=False):
include = 0
exclude = 0
for fl_name, fl_state in flags.items():
- if not fl_state:
+ if not fl_state:
include |= flag_map[fl_name]
else: # 'unset' child tag
exclude |= flag_map[fl_name]
diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py
index f5217aecb..3a28723b3 100644
--- a/python/vyos/ifconfig/wireguard.py
+++ b/python/vyos/ifconfig/wireguard.py
@@ -22,12 +22,13 @@ from tempfile import NamedTemporaryFile
from hurry.filesize import size
from hurry.filesize import alternative
+from vyos.base import Warning
from vyos.configquery import ConfigTreeQuery
from vyos.ifconfig import Interface
from vyos.ifconfig import Operational
from vyos.template import is_ipv6
from vyos.template import is_ipv4
-
+from vyos.utils.network import get_wireguard_peers
class WireGuardOperational(Operational):
def _dump(self):
"""Dump wireguard data in a python friendly way."""
@@ -251,92 +252,131 @@ class WireGuardIf(Interface):
"""Get a synthetic MAC address."""
return self.get_mac_synthetic()
+ def get_peer_public_keys(self, config, disabled=False):
+ """Get list of configured peer public keys"""
+ if 'peer' not in config:
+ return []
+
+ public_keys = []
+
+ for _, peer_config in config['peer'].items():
+ if disabled == ('disable' in peer_config):
+ public_keys.append(peer_config['public_key'])
+
+ return public_keys
+
def update(self, config):
"""General helper function which works on a dictionary retrived by
get_config_dict(). It's main intention is to consolidate the scattered
interface setup code and provide a single point of entry when workin
on any interface."""
- tmp_file = NamedTemporaryFile('w')
- tmp_file.write(config['private_key'])
- tmp_file.flush()
# Wireguard base command is identical for every peer
base_cmd = f'wg set {self.ifname}'
+
interface_cmd = base_cmd
if 'port' in config:
interface_cmd += ' listen-port {port}'
if 'fwmark' in config:
interface_cmd += ' fwmark {fwmark}'
- interface_cmd += f' private-key {tmp_file.name}'
- interface_cmd = interface_cmd.format(**config)
- # T6490: execute command to ensure interface configured
- self._cmd(interface_cmd)
+ with NamedTemporaryFile('w') as tmp_file:
+ tmp_file.write(config['private_key'])
+ tmp_file.flush()
- # If no PSK is given remove it by using /dev/null - passing keys via
- # the shell (usually bash) is considered insecure, thus we use a file
- no_psk_file = '/dev/null'
+ interface_cmd += f' private-key {tmp_file.name}'
+ interface_cmd = interface_cmd.format(**config)
+ # T6490: execute command to ensure interface configured
+ self._cmd(interface_cmd)
+
+ current_peer_public_keys = get_wireguard_peers(self.ifname)
+
+ if 'rebuild_required' in config:
+ # Remove all existing peers that no longer exist in config
+ current_public_keys = self.get_peer_public_keys(config)
+ cmd_remove_peers = [f' peer {public_key} remove'
+ for public_key in current_peer_public_keys
+ if public_key not in current_public_keys]
+ if cmd_remove_peers:
+ self._cmd(base_cmd + ''.join(cmd_remove_peers))
if 'peer' in config:
+ # Group removal of disabled peers in one command
+ current_disabled_peers = self.get_peer_public_keys(config, disabled=True)
+ cmd_disabled_peers = [f' peer {public_key} remove'
+ for public_key in current_disabled_peers]
+ if cmd_disabled_peers:
+ self._cmd(base_cmd + ''.join(cmd_disabled_peers))
+
+ peer_cmds = []
+ peer_domain_cmds = []
+ peer_psk_files = []
+
for peer, peer_config in config['peer'].items():
# T4702: No need to configure this peer when it was explicitly
# marked as disabled - also active sessions are terminated as
# the public key was already removed when entering this method!
if 'disable' in peer_config:
- # remove peer if disabled, no error report even if peer not exists
- cmd = base_cmd + ' peer {public_key} remove'
- self._cmd(cmd.format(**peer_config))
continue
- psk_file = no_psk_file
-
# start of with a fresh 'wg' command
- peer_cmd = base_cmd + ' peer {public_key}'
+ peer_cmd = ' peer {public_key}'
- try:
- cmd = peer_cmd
-
- if 'preshared_key' in peer_config:
- psk_file = '/tmp/tmp.wireguard.psk'
- with open(psk_file, 'w') as f:
- f.write(peer_config['preshared_key'])
- cmd += f' preshared-key {psk_file}'
-
- # Persistent keepalive is optional
- if 'persistent_keepalive' in peer_config:
- cmd += ' persistent-keepalive {persistent_keepalive}'
-
- # Multiple allowed-ip ranges can be defined - ensure we are always
- # dealing with a list
- if isinstance(peer_config['allowed_ips'], str):
- peer_config['allowed_ips'] = [peer_config['allowed_ips']]
- cmd += ' allowed-ips ' + ','.join(peer_config['allowed_ips'])
-
- self._cmd(cmd.format(**peer_config))
-
- cmd = peer_cmd
-
- # Ensure peer is created even if dns not working
- if {'address', 'port'} <= set(peer_config):
- if is_ipv6(peer_config['address']):
- cmd += ' endpoint [{address}]:{port}'
- elif is_ipv4(peer_config['address']):
- cmd += ' endpoint {address}:{port}'
- else:
- # don't set endpoint if address uses domain name
- continue
- elif {'host_name', 'port'} <= set(peer_config):
- cmd += ' endpoint {host_name}:{port}'
-
- self._cmd(cmd.format(**peer_config), env={
+ cmd = peer_cmd
+
+ if 'preshared_key' in peer_config:
+ with NamedTemporaryFile(mode='w', delete=False) as tmp_file:
+ tmp_file.write(peer_config['preshared_key'])
+ tmp_file.flush()
+ cmd += f' preshared-key {tmp_file.name}'
+ peer_psk_files.append(tmp_file.name)
+ else:
+ # If no PSK is given remove it by using /dev/null - passing keys via
+ # the shell (usually bash) is considered insecure, thus we use a file
+ cmd += f' preshared-key /dev/null'
+
+ # Persistent keepalive is optional
+ if 'persistent_keepalive' in peer_config:
+ cmd += ' persistent-keepalive {persistent_keepalive}'
+
+ # Multiple allowed-ip ranges can be defined - ensure we are always
+ # dealing with a list
+ if isinstance(peer_config['allowed_ips'], str):
+ peer_config['allowed_ips'] = [peer_config['allowed_ips']]
+ cmd += ' allowed-ips ' + ','.join(peer_config['allowed_ips'])
+
+ peer_cmds.append(cmd.format(**peer_config))
+
+ cmd = peer_cmd
+
+ # Ensure peer is created even if dns not working
+ if {'address', 'port'} <= set(peer_config):
+ if is_ipv6(peer_config['address']):
+ cmd += ' endpoint [{address}]:{port}'
+ elif is_ipv4(peer_config['address']):
+ cmd += ' endpoint {address}:{port}'
+ else:
+ # don't set endpoint if address uses domain name
+ continue
+ elif {'host_name', 'port'} <= set(peer_config):
+ cmd += ' endpoint {host_name}:{port}'
+ else:
+ continue
+
+ peer_domain_cmds.append(cmd.format(**peer_config))
+
+ try:
+ if peer_cmds:
+ self._cmd(base_cmd + ''.join(peer_cmds))
+
+ if peer_domain_cmds:
+ self._cmd(base_cmd + ''.join(peer_domain_cmds), env={
'WG_ENDPOINT_RESOLUTION_RETRIES': config['max_dns_retry']})
- except:
- # todo: logging
- pass
- finally:
- # PSK key file is not required to be stored persistently as its backed by CLI
- if psk_file != no_psk_file and os.path.exists(psk_file):
- os.remove(psk_file)
+ except Exception as e:
+ Warning(f'Failed to apply Wireguard peers on {self.ifname}: {e}')
+ finally:
+ for tmp in peer_psk_files:
+ os.unlink(tmp)
# call base class
super().update(config)
diff --git a/python/vyos/template.py b/python/vyos/template.py
index 11e1cc50f..aa215db95 100755
--- a/python/vyos/template.py
+++ b/python/vyos/template.py
@@ -728,7 +728,7 @@ def conntrack_rule(rule_conf, rule_id, action, ipv6=False):
if port[0] == '!':
operator = '!='
port = port[1:]
- output.append(f'th {prefix}port {operator} {port}')
+ output.append(f'th {prefix}port {operator} {{ {port} }}')
if 'group' in side_conf:
group = side_conf['group']
diff --git a/python/vyos/utils/cpu.py b/python/vyos/utils/cpu.py
index 8ace77d15..6f21eb526 100644
--- a/python/vyos/utils/cpu.py
+++ b/python/vyos/utils/cpu.py
@@ -26,6 +26,7 @@ It has special cases for x86_64 and MAY work correctly on other architectures,
but nothing is certain.
"""
+import os
import re
def _read_cpuinfo():
@@ -114,3 +115,8 @@ def get_available_cpus():
out = json.loads(cmd('lscpu --extended -b --json'))
return out['cpus']
+
+
+def get_half_cpus():
+ """ return 1/2 of the numbers of available CPUs """
+ return max(1, os.cpu_count() // 2)
diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py
index 67d247fba..0a84be478 100644
--- a/python/vyos/utils/network.py
+++ b/python/vyos/utils/network.py
@@ -416,6 +416,21 @@ def is_wireguard_key_pair(private_key: str, public_key:str) -> bool:
else:
return False
+def get_wireguard_peers(ifname: str) -> list:
+ """
+ Return list of configured Wireguard peers for interface
+ :param ifname: Interface name
+ :type ifname: str
+ :return: list of public keys
+ :rtype: list
+ """
+ if not interface_exists(ifname):
+ return []
+
+ from vyos.utils.process import cmd
+ peers = cmd(f'wg show {ifname} peers')
+ return peers.splitlines()
+
def is_subnet_connected(subnet, primary=False):
"""
Verify is the given IPv4/IPv6 subnet is connected to any interface on this
@@ -635,3 +650,19 @@ def is_valid_ipv4_address_or_range(addr: str) -> bool:
return ip_network(addr).version == 4
except:
return False
+
+def is_valid_ipv6_address_or_range(addr: str) -> bool:
+ """
+ Validates if the provided address is a valid IPv4, CIDR or IPv4 range
+ :param addr: address to test
+ :return: bool: True if provided address is valid
+ """
+ from ipaddress import ip_network
+ try:
+ if '-' in addr: # If we are checking a range, validate both address's individually
+ split = addr.split('-')
+ return is_valid_ipv6_address_or_range(split[0]) and is_valid_ipv6_address_or_range(split[1])
+ else:
+ return ip_network(addr).version == 6
+ except:
+ return False
diff --git a/python/vyos/utils/process.py b/python/vyos/utils/process.py
index 121b6e240..21335e6b3 100644
--- a/python/vyos/utils/process.py
+++ b/python/vyos/utils/process.py
@@ -14,6 +14,7 @@
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
import os
+import shlex
from subprocess import Popen
from subprocess import PIPE
@@ -21,20 +22,17 @@ from subprocess import STDOUT
from subprocess import DEVNULL
-def get_wrapper(vrf, netns, auth):
- wrapper = ''
+def get_wrapper(vrf, netns):
+ wrapper = None
if vrf:
- wrapper = f'ip vrf exec {vrf} '
+ wrapper = ['ip', 'vrf', 'exec', vrf]
elif netns:
- wrapper = f'ip netns exec {netns} '
- if auth:
- wrapper = f'{auth} {wrapper}'
+ wrapper = ['ip', 'netns', 'exec', netns]
return wrapper
def popen(command, flag='', shell=None, input=None, timeout=None, env=None,
- stdout=PIPE, stderr=PIPE, decode='utf-8', auth='', vrf=None,
- netns=None):
+ stdout=PIPE, stderr=PIPE, decode='utf-8', vrf=None, netns=None):
"""
popen is a wrapper helper around subprocess.Popen
with it default setting it will return a tuple (out, err)
@@ -75,28 +73,33 @@ def popen(command, flag='', shell=None, input=None, timeout=None, env=None,
if not debug.enabled(flag):
flag = 'command'
+ use_shell = shell
+ stdin = None
+ if shell is None:
+ use_shell = False
+ if ' ' in command:
+ use_shell = True
+ if env:
+ use_shell = True
+
# Must be run as root to execute command in VRF or network namespace
+ wrapper = get_wrapper(vrf, netns)
if vrf or netns:
if os.getuid() != 0:
raise OSError(
'Permission denied: cannot execute commands in VRF and netns contexts as an unprivileged user'
)
- wrapper = get_wrapper(vrf, netns, auth)
- command = f'{wrapper} {command}' if wrapper else command
+ if use_shell:
+ command = f'{shlex.join(wrapper)} {command}'
+ else:
+ if type(command) is not list:
+ command = [command]
+ command = wrapper + command
- cmd_msg = f"cmd '{command}'"
+ cmd_msg = f"cmd '{command}'" if use_shell else f"cmd '{shlex.join(command)}'"
debug.message(cmd_msg, flag)
- use_shell = shell
- stdin = None
- if shell is None:
- use_shell = False
- if ' ' in command:
- use_shell = True
- if env:
- use_shell = True
-
if input:
stdin = PIPE
input = input.encode() if type(input) is str else input
@@ -155,7 +158,7 @@ def run(command, flag='', shell=None, input=None, timeout=None, env=None,
def cmd(command, flag='', shell=None, input=None, timeout=None, env=None,
stdout=PIPE, stderr=PIPE, decode='utf-8', raising=None, message='',
- expect=[0], auth='', vrf=None, netns=None):
+ expect=[0], vrf=None, netns=None):
"""
A wrapper around popen, which returns the stdout and
will raise the error code of a command
@@ -171,12 +174,11 @@ def cmd(command, flag='', shell=None, input=None, timeout=None, env=None,
input=input, timeout=timeout,
env=env, shell=shell,
decode=decode,
- auth=auth,
vrf=vrf,
netns=netns,
)
if code not in expect:
- wrapper = get_wrapper(vrf, netns, auth='')
+ wrapper = get_wrapper(vrf, netns)
command = f'{wrapper} {command}'
feedback = message + '\n' if message else ''
feedback += f'failed to run command: {command}\n'