summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
authorsskaje <sskaje@gmail.com>2024-12-31 10:44:01 +0800
committerChristian Breunig <christian@breunig.cc>2025-01-19 00:17:12 +0100
commit2212a438b234f34f32e08efef2f841ba55a3b6a0 (patch)
tree47528dafb6733efb134a5ceee51e52118be7896f /python
parent4d3e976271e30d70c8b2660d869a220de98d8c59 (diff)
downloadvyos-1x-2212a438b234f34f32e08efef2f841ba55a3b6a0.tar.gz
vyos-1x-2212a438b234f34f32e08efef2f841ba55a3b6a0.zip
wireguard: T4930: allow peers via FQDN
* set interfaces wireguard wgXX peer YY hostname <fqdn>
Diffstat (limited to 'python')
-rw-r--r--python/vyos/ifconfig/control.py4
-rw-r--r--python/vyos/ifconfig/wireguard.py196
2 files changed, 159 insertions, 41 deletions
diff --git a/python/vyos/ifconfig/control.py b/python/vyos/ifconfig/control.py
index 7402da55a..a886c1b9e 100644
--- a/python/vyos/ifconfig/control.py
+++ b/python/vyos/ifconfig/control.py
@@ -48,7 +48,7 @@ class Control(Section):
def _popen(self, command):
return popen(command, self.debug)
- def _cmd(self, command):
+ def _cmd(self, command, env=None):
import re
if 'netns' in self.config:
# This command must be executed from default netns 'ip link set dev X netns X'
@@ -61,7 +61,7 @@ class Control(Section):
command = command
else:
command = f'ip netns exec {self.config["netns"]} {command}'
- return cmd(command, self.debug)
+ return cmd(command, self.debug, env=env)
def _get_command(self, config, name):
"""
diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py
index 519012625..9d0bd1a6c 100644
--- a/python/vyos/ifconfig/wireguard.py
+++ b/python/vyos/ifconfig/wireguard.py
@@ -25,6 +25,7 @@ from hurry.filesize import alternative
from vyos.ifconfig import Interface
from vyos.ifconfig import Operational
from vyos.template import is_ipv6
+from vyos.template import is_ipv4
class WireGuardOperational(Operational):
def _dump(self):
@@ -90,12 +91,15 @@ class WireGuardOperational(Operational):
c.set_level(['interfaces', 'wireguard', self.config['ifname']])
description = c.return_effective_value(['description'])
ips = c.return_effective_values(['address'])
+ hostnames = c.return_effective_values(['host-name'])
answer = 'interface: {}\n'.format(self.config['ifname'])
if description:
answer += ' description: {}\n'.format(description)
if ips:
answer += ' address: {}\n'.format(', '.join(ips))
+ if hostnames:
+ answer += ' hostname: {}\n'.format(', '.join(hostnames))
answer += ' public key: {}\n'.format(wgdump['public_key'])
answer += ' private key: (hidden)\n'
@@ -155,6 +159,94 @@ class WireGuardOperational(Operational):
answer += '\n'
return answer
+ def get_latest_handshakes(self):
+ """Get latest handshake time for each peer"""
+ output = {}
+
+ # Dump wireguard last handshake
+ tmp = self._cmd(f'wg show {self.ifname} latest-handshakes')
+ # Output:
+ # PUBLIC-KEY= 1732812147
+ for line in tmp.split('\n'):
+ if not line:
+ # Skip empty lines and last line
+ continue
+ items = line.split('\t')
+
+ if len(items) != 2:
+ continue
+
+ output[items[0]] = int(items[1])
+
+ return output
+
+ def reset_peer(self, peer_name=None, public_key=None):
+ from vyos.configquery import ConfigTreeQuery
+
+ c = ConfigTreeQuery()
+
+ max_dns_retry = c.value(
+ ['interfaces', 'wireguard', self.ifname, 'max-dns-retry']
+ )
+ if max_dns_retry is None:
+ max_dns_retry = 3
+
+ current_peers = self._dump().get(self.ifname, {}).get('peers', {})
+
+ for peer in c.list_nodes(['interfaces', 'wireguard', self.ifname, 'peer']):
+ peer_public_key = c.value(
+ ['interfaces', 'wireguard', self.ifname, 'peer', peer, 'public-key']
+ )
+ if peer_name is None or peer == peer_name or public_key == peer_public_key:
+ address = c.value(
+ ['interfaces', 'wireguard', self.ifname, 'peer', peer, 'address']
+ )
+ host_name = c.value(
+ ['interfaces', 'wireguard', self.ifname, 'peer', peer, 'host-name']
+ )
+ port = c.value(
+ ['interfaces', 'wireguard', self.ifname, 'peer', peer, 'port']
+ )
+
+ if (not address and not host_name) or not port:
+ if peer_name is not None:
+ print(f'Peer {peer_name} endpoint not set')
+ continue
+
+ # address has higher priority than host-name
+ if address:
+ new_endpoint = f'{address}:{port}'
+ else:
+ new_endpoint = f'{host_name}:{port}'
+
+ if c.exists(
+ ['interfaces', 'wireguard', self.ifname, 'peer', peer, 'disable']
+ ):
+ continue
+
+ cmd = f'wg set {self.ifname} peer {peer_public_key} endpoint {new_endpoint}'
+ try:
+ if (
+ peer_public_key in current_peers
+ and 'endpoint' in current_peers[peer_public_key]
+ and current_peers[peer_public_key]['endpoint'] is not None
+ ):
+ current_endpoint = current_peers[peer_public_key]['endpoint']
+ message = f'Resetting {self.ifname} peer {peer_public_key} from {current_endpoint} endpoint to {new_endpoint} ... '
+ else:
+ message = f'Resetting {self.ifname} peer {peer_public_key} endpoint to {new_endpoint} ... '
+ print(
+ message,
+ end='',
+ )
+
+ self._cmd(
+ cmd, env={'WG_ENDPOINT_RESOLUTION_RETRIES': str(max_dns_retry)}
+ )
+ print('done')
+ except:
+ print(f'Error\nPlease try to run command manually:\n{cmd}\n')
+
@Interface.register
class WireGuardIf(Interface):
@@ -186,16 +278,23 @@ class WireGuardIf(Interface):
tmp_file.flush()
# Wireguard base command is identical for every peer
- base_cmd = 'wg set {ifname}'
+ base_cmd = 'wg set ' + config['ifname']
+ max_dns_retry = config['max_dns_retry'] if 'max_dns_retry' in config else 3
+
+ interface_cmd = base_cmd
if 'port' in config:
- base_cmd += ' listen-port {port}'
+ interface_cmd += ' listen-port {port}'
if 'fwmark' in config:
- base_cmd += ' fwmark {fwmark}'
+ interface_cmd += ' fwmark {fwmark}'
- base_cmd += f' private-key {tmp_file.name}'
- base_cmd = base_cmd.format(**config)
+ interface_cmd += f' private-key {tmp_file.name}'
+ interface_cmd = interface_cmd.format(**config)
# T6490: execute command to ensure interface configured
- self._cmd(base_cmd)
+ self._cmd(interface_cmd)
+
+ # 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'
if 'peer' in config:
for peer, peer_config in config['peer'].items():
@@ -203,43 +302,62 @@ class WireGuardIf(Interface):
# 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
- # start of with a fresh 'wg' command
- cmd = base_cmd + ' peer {public_key}'
-
- # 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'
psk_file = no_psk_file
- 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'])
-
- # Endpoint configuration is optional
- if {'address', 'port'} <= set(peer_config):
- if is_ipv6(peer_config['address']):
- cmd += ' endpoint [{address}]:{port}'
- else:
- cmd += ' endpoint {address}:{port}'
-
- self._cmd(cmd.format(**peer_config))
- # 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)
+ # start of with a fresh 'wg' command
+ peer_cmd = base_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={'WG_ENDPOINT_RESOLUTION_RETRIES': str(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)
# call base class
super().update(config)