diff options
Diffstat (limited to 'python')
-rwxr-xr-x | python/vyos/firewall.py | 21 | ||||
-rw-r--r-- | python/vyos/ifconfig/wireguard.py | 160 | ||||
-rwxr-xr-x | python/vyos/template.py | 2 | ||||
-rw-r--r-- | python/vyos/utils/network.py | 31 | ||||
-rw-r--r-- | python/vyos/utils/process.py | 48 |
5 files changed, 169 insertions, 93 deletions
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/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' |