diff --git a/debian/vyos-1x.postinst b/debian/vyos-1x.postinst
index ff5a91e097..fde58651a7 100644
--- a/debian/vyos-1x.postinst
+++ b/debian/vyos-1x.postinst
@@ -272,3 +272,7 @@ update-alternatives --set regulatory.db /lib/firmware/regulatory.db-upstream
if systemctl is-active --quiet vyos-configd; then
systemctl restart vyos-configd
fi
+# Restart vyos-domain-resolver if running
+if systemctl is-active --quiet vyos-domain-resolver; then
+ systemctl restart vyos-domain-resolver
+fi
diff --git a/interface-definitions/interfaces_wireguard.xml.in b/interface-definitions/interfaces_wireguard.xml.in
index ce49de0381..4f8b6c7518 100644
--- a/interface-definitions/interfaces_wireguard.xml.in
+++ b/interface-definitions/interfaces_wireguard.xml.in
@@ -40,6 +40,19 @@
0
+
+
+ DNS retries when resolve fails
+
+ u32:1-15
+ Maximum number of retries
+
+
+
+
+
+ 3
+
Base64 encoded private key
@@ -104,6 +117,18 @@
+
+
+ Hostname of tunnel endpoint
+
+ hostname
+ FQDN of WireGuard endpoint
+
+
+
+
+
+
#include
diff --git a/op-mode-definitions/reset-wireguard.xml.in b/op-mode-definitions/reset-wireguard.xml.in
new file mode 100644
index 0000000000..c2243f5197
--- /dev/null
+++ b/op-mode-definitions/reset-wireguard.xml.in
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+ Reset WireGuard Peers
+
+
+
+
+ WireGuard interface name
+
+ interfaces wireguard
+
+
+ sudo ${vyos_op_scripts_dir}/reset_wireguard.py reset_peer --interface="$4"
+
+
+
+ WireGuard peer name
+
+ interfaces wireguard ${COMP_WORDS[3]} peer
+
+
+ sudo ${vyos_op_scripts_dir}/reset_wireguard.py reset_peer --interface="$4" --peer="$6"
+
+
+
+
+
+
+
+
diff --git a/python/vyos/configquery.py b/python/vyos/configquery.py
index 5d6ca9be9f..4c4ead0a38 100644
--- a/python/vyos/configquery.py
+++ b/python/vyos/configquery.py
@@ -1,4 +1,4 @@
-# Copyright 2021-2024 VyOS maintainers and contributors
+# Copyright 2021-2025 VyOS maintainers and contributors
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -120,11 +120,14 @@ def list_nodes(self, path: list):
def get_config_dict(self, path=[], effective=False, key_mangling=None,
get_first_key=False, no_multi_convert=False,
- no_tag_node_value_mangle=False):
+ no_tag_node_value_mangle=False, with_defaults=False,
+ with_recursive_defaults=False):
return self.config.get_config_dict(path, effective=effective,
key_mangling=key_mangling, get_first_key=get_first_key,
no_multi_convert=no_multi_convert,
- no_tag_node_value_mangle=no_tag_node_value_mangle)
+ no_tag_node_value_mangle=no_tag_node_value_mangle,
+ with_defaults=with_defaults,
+ with_recursive_defaults=with_recursive_defaults)
class VbashOpRun(GenericOpRun):
def __init__(self):
diff --git a/python/vyos/ifconfig/control.py b/python/vyos/ifconfig/control.py
index 7402da55aa..a886c1b9ef 100644
--- a/python/vyos/ifconfig/control.py
+++ b/python/vyos/ifconfig/control.py
@@ -48,7 +48,7 @@ def _debug_msg (self, message):
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 @@ def _cmd(self, command):
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 5190126254..341fd32ff7 100644
--- a/python/vyos/ifconfig/wireguard.py
+++ b/python/vyos/ifconfig/wireguard.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2024 VyOS maintainers and contributors
+# Copyright 2019-2025 VyOS maintainers and contributors
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -22,9 +22,11 @@
from hurry.filesize import size
from hurry.filesize import alternative
+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
class WireGuardOperational(Operational):
def _dump(self):
@@ -80,80 +82,76 @@ def _dump(self):
}
return output
- def show_interface(self):
- from vyos.config import Config
+ def get_latest_handshakes(self):
+ """Get latest handshake time for each peer"""
+ output = {}
- c = Config()
+ # 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')
- wgdump = self._dump().get(self.config['ifname'], None)
+ if len(items) != 2:
+ continue
- c.set_level(['interfaces', 'wireguard', self.config['ifname']])
- description = c.return_effective_value(['description'])
- ips = c.return_effective_values(['address'])
+ output[items[0]] = int(items[1])
- answer = 'interface: {}\n'.format(self.config['ifname'])
- if description:
- answer += ' description: {}\n'.format(description)
- if ips:
- answer += ' address: {}\n'.format(', '.join(ips))
+ return output
+
+ def reset_peer(self, peer_name=None, public_key=None):
+ c = ConfigTreeQuery()
+ tmp = c.get_config_dict(['interfaces', 'wireguard', self.ifname],
+ effective=True, get_first_key=True,
+ key_mangling=('-', '_'), with_defaults=True)
+
+ current_peers = self._dump().get(self.ifname, {}).get('peers', {})
+
+ for peer, peer_config in tmp['peer'].items():
+ peer_public_key = peer_config['public_key']
+ if peer_name is None or peer == peer_name or public_key == peer_public_key:
+ if ('address' not in peer_config and 'host_name' not in peer_config) or 'port' not in peer_config:
+ if peer_name is not None:
+ print(f'WireGuard interface "{self.ifname}" peer "{peer_name}" address/host-name unset!')
+ continue
- answer += ' public key: {}\n'.format(wgdump['public_key'])
- answer += ' private key: (hidden)\n'
- answer += ' listening port: {}\n'.format(wgdump['listen_port'])
- answer += '\n'
+ # As we work with an effective config, a port CLI node is always
+ # available when an address/host-name is defined on the CLI
+ port = peer_config['port']
- for peer in c.list_effective_nodes(['peer']):
- if wgdump['peers']:
- pubkey = c.return_effective_value(['peer', peer, 'public-key'])
- if pubkey in wgdump['peers']:
- wgpeer = wgdump['peers'][pubkey]
+ # address has higher priority than host-name
+ if 'address' in peer_config:
+ address = peer_config['address']
+ new_endpoint = f'{address}:{port}'
+ else:
+ host_name = peer_config['host_name']
+ new_endpoint = f'{host_name}:{port}'
- answer += ' peer: {}\n'.format(peer)
- answer += ' public key: {}\n'.format(pubkey)
+ if 'disable' in peer_config:
+ print(f'WireGuard interface "{self.ifname}" peer "{peer_name}" disabled!')
+ continue
- """ figure out if the tunnel is recently active or not """
- status = 'inactive'
- if wgpeer['latest_handshake'] is None:
- """ no handshake ever """
- status = 'inactive'
+ 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:
- if int(wgpeer['latest_handshake']) > 0:
- delta = timedelta(
- seconds=int(time.time() - wgpeer['latest_handshake'])
- )
- answer += ' latest handshake: {}\n'.format(delta)
- if time.time() - int(wgpeer['latest_handshake']) < (60 * 5):
- """ Five minutes and the tunnel is still active """
- status = 'active'
- else:
- """ it's been longer than 5 minutes """
- status = 'inactive'
- elif int(wgpeer['latest_handshake']) == 0:
- """ no handshake ever """
- status = 'inactive'
- answer += ' status: {}\n'.format(status)
-
- if wgpeer['endpoint'] is not None:
- answer += ' endpoint: {}\n'.format(wgpeer['endpoint'])
-
- if wgpeer['allowed_ips'] is not None:
- answer += ' allowed ips: {}\n'.format(
- ','.join(wgpeer['allowed_ips']).replace(',', ', ')
- )
-
- if wgpeer['transfer_rx'] > 0 or wgpeer['transfer_tx'] > 0:
- rx_size = size(wgpeer['transfer_rx'], system=alternative)
- tx_size = size(wgpeer['transfer_tx'], system=alternative)
- answer += ' transfer: {} received, {} sent\n'.format(
- rx_size, tx_size
- )
-
- if wgpeer['persistent_keepalive'] is not None:
- answer += ' persistent keepalive: every {} seconds\n'.format(
- wgpeer['persistent_keepalive']
- )
- answer += '\n'
- return answer
+ message = f'Resetting {self.ifname} peer {peer_public_key} endpoint to {new_endpoint} ... '
+ print(message, end='')
+
+ self._cmd(cmd, env={'WG_ENDPOINT_RESOLUTION_RETRIES':
+ tmp['max_dns_retry']})
+ print('done')
+ except:
+ print(f'Error\nPlease try to run command manually:\n{cmd}\n')
@Interface.register
@@ -180,22 +178,26 @@ def update(self, config):
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 = 'wg set {ifname}'
+ base_cmd = f'wg set {self.ifname}'
+ 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 +205,60 @@ def update(self, config):
# 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': 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)
# call base class
super().update(config)
diff --git a/python/vyos/utils/kernel.py b/python/vyos/utils/kernel.py
index 847f801080..05eac8a6a4 100644
--- a/python/vyos/utils/kernel.py
+++ b/python/vyos/utils/kernel.py
@@ -15,6 +15,10 @@
import os
+# A list of used Kernel constants
+# https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/drivers/net/wireguard/messages.h?h=linux-6.6.y#n45
+WIREGUARD_REKEY_AFTER_TIME = 120
+
def check_kmod(k_mod):
""" Common utility function to load required kernel modules on demand """
from vyos import ConfigError
diff --git a/smoketest/scripts/cli/test_interfaces_wireguard.py b/smoketest/scripts/cli/test_interfaces_wireguard.py
index 4b994a659b..f8cd18cf2b 100755
--- a/smoketest/scripts/cli/test_interfaces_wireguard.py
+++ b/smoketest/scripts/cli/test_interfaces_wireguard.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-2023 VyOS maintainers and contributors
+# Copyright (C) 2020-2025 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
@@ -17,27 +17,33 @@
import os
import unittest
-from base_vyostest_shim import VyOSUnitTestSHIM
+from base_interfaces_test import BasicInterfaceTest
from vyos.configsession import ConfigSessionError
from vyos.utils.file import read_file
from vyos.utils.process import cmd
+from vyos.utils.process import is_systemd_service_running
base_path = ['interfaces', 'wireguard']
-
-class WireGuardInterfaceTest(VyOSUnitTestSHIM.TestCase):
+domain_resolver = 'vyos-domain-resolver.service'
+class WireGuardInterfaceTest(BasicInterfaceTest.TestCase):
@classmethod
def setUpClass(cls):
- super(WireGuardInterfaceTest, cls).setUpClass()
-
- cls._test_addr = ['192.0.2.1/26', '192.0.2.255/31', '192.0.2.64/32',
- '2001:db8:1::ffff/64', '2001:db8:101::1/112']
- cls._interfaces = ['wg0', 'wg1']
+ cls._base_path = ['interfaces', 'wireguard']
+ cls._options = {
+ 'wg0': ['private-key wBbGJJXYllwDcw63AFjiIR6ZlsvqvAf3eDwog64Dp0Q=',
+ 'peer RED public-key 6hkkfxN4VUQLu36NLZr47I7ST/FkQl2clPWr+9a6ZH8=',
+ 'peer RED allowed-ips 169.254.0.0/16',
+ 'port 5678'],
+ 'wg1': ['private-key QFwnBHlHYspehvpklBKb7cikM+QMkEy2p6gfsg06S08=',
+ 'peer BLUE public-key hRJLmP8SVU9/MLmPmYmpOa+RTB4F/zhDqA+/QDuW1Hg=',
+ 'peer BLUE allowed-ips 169.254.0.0/16',
+ 'port 4567'],
+ }
+ cls._interfaces = list(cls._options)
- def tearDown(self):
- self.cli_delete(base_path)
- self.cli_commit()
+ super(WireGuardInterfaceTest, cls).setUpClass()
- def test_01_wireguard_peer(self):
+ def test_wireguard_peer(self):
# Create WireGuard interfaces with associated peers
for intf in self._interfaces:
peer = 'foo-' + intf
@@ -64,7 +70,7 @@ def test_01_wireguard_peer(self):
self.assertTrue(os.path.isdir(f'/sys/class/net/{intf}'))
- def test_02_wireguard_add_remove_peer(self):
+ def test_wireguard_add_remove_peer(self):
# T2939: Create WireGuard interfaces with associated peers.
# Remove one of the configured peers.
# T4774: Test prevention of duplicate peer public keys
@@ -102,7 +108,7 @@ def test_02_wireguard_add_remove_peer(self):
self.cli_delete(base_path + [interface, 'peer', 'PEER01'])
self.cli_commit()
- def test_03_wireguard_same_public_key(self):
+ def test_wireguard_same_public_key(self):
# T5413: Test prevention of equality interface public key and peer's
# public key
interface = 'wg0'
@@ -115,45 +121,40 @@ def test_03_wireguard_same_public_key(self):
self.cli_set(base_path + [interface, 'private-key', privkey])
self.cli_set(base_path + [interface, 'peer', 'PEER01', 'public-key', pubkey_fail])
- self.cli_set(base_path + [interface, 'peer', 'PEER01', 'port', port])
self.cli_set(base_path + [interface, 'peer', 'PEER01', 'allowed-ips', '10.205.212.10/32'])
self.cli_set(base_path + [interface, 'peer', 'PEER01', 'address', '192.0.2.1'])
# The same pubkey as the interface wg0
with self.assertRaises(ConfigSessionError):
self.cli_commit()
-
self.cli_set(base_path + [interface, 'peer', 'PEER01', 'public-key', pubkey_ok])
+ # If address is defined for a peer, so must be the peer port
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + [interface, 'peer', 'PEER01', 'port', port])
+
# Commit peers
self.cli_commit()
self.assertTrue(os.path.isdir(f'/sys/class/net/{interface}'))
- def test_04_wireguard_threaded(self):
+ def test_wireguard_threaded(self):
# T5409: Test adding threaded option on interface.
- # Test prevention for adding threaded
- # if no enabled peer is configured.
- interface = 'wg0'
- port = '12345'
- privkey = 'OOjcXGfgQlAuM6q8Z9aAYduCua7pxf7UKYvIqoUPoGQ='
- pubkey = 'ebFx/1G0ti8tvuZd94sEIosAZZIznX+dBAKG/8DFm0I='
+ for intf in self._interfaces:
+ for option in self._options.get(intf, []):
+ self.cli_set(self._base_path + [intf] + option.split())
- self.cli_set(base_path + [interface, 'address', '172.16.0.1/24'])
- self.cli_set(base_path + [interface, 'private-key', privkey])
-
- self.cli_set(base_path + [interface, 'peer', 'PEER01', 'port', port])
- self.cli_set(base_path + [interface, 'peer', 'PEER01', 'public-key', pubkey])
- self.cli_set(base_path + [interface, 'peer', 'PEER01', 'allowed-ips', '10.205.212.10/32'])
- self.cli_set(base_path + [interface, 'peer', 'PEER01', 'address', '192.0.2.1'])
- self.cli_set(base_path + [interface, 'per-client-thread'])
+ self.cli_set(base_path + [intf, 'per-client-thread'])
# Commit peers
self.cli_commit()
- tmp = read_file(f'/sys/class/net/{interface}/threaded')
- self.assertTrue(tmp, "1")
- def test_05_wireguard_peer_pubkey_change(self):
+ for intf in self._interfaces:
+ tmp = read_file(f'/sys/class/net/{intf}/threaded')
+ self.assertTrue(tmp, "1")
+
+ def test_wireguard_peer_pubkey_change(self):
# T5707 changing WireGuard CLI public key of a peer - it's not removed
def get_peers(interface) -> list:
@@ -171,7 +172,6 @@ def get_peers(interface) -> list:
peers.append(items[0])
return peers
-
interface = 'wg1337'
port = '1337'
privkey = 'iJi4lb2HhkLx2KSAGOjji2alKkYsJjSPkHkrcpxgEVU='
@@ -200,5 +200,41 @@ def get_peers(interface) -> list:
self.assertNotIn(pubkey_1, peers)
self.assertIn(pubkey_2, peers)
+ def test_wireguard_hostname(self):
+ # T4930: Test dynamic endpoint support
+ interface = 'wg1234'
+ port = '54321'
+ privkey = 'UOWIeZKNzijhgu0bPRy2PB3gnuOBLfQax5GiYfkmU3A='
+ pubkey = '4nG5NfhHBQUq/DnwT0RjRoBCqAh3VrRHqdQgzC/xujk='
+
+ base_interface_path = base_path + [interface]
+ self.cli_set(base_interface_path + ['address', '172.16.0.1/24'])
+ self.cli_set(base_interface_path + ['private-key', privkey])
+
+ peer_base_path = base_interface_path + ['peer', 'dynamic01']
+ self.cli_set(peer_base_path + ['port', port])
+ self.cli_set(peer_base_path + ['public-key', pubkey])
+ self.cli_set(peer_base_path + ['allowed-ips', '169.254.0.0/16'])
+ self.cli_set(peer_base_path + ['address', '192.0.2.1'])
+ self.cli_set(peer_base_path + ['host-name', 'wg.vyos.net'])
+
+ # Peer address and host-name are mutually exclusive
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_delete(peer_base_path + ['address'])
+
+ # Commit peers
+ self.cli_commit()
+
+ # Ensure the service is running which checks for DNS changes
+ self.assertTrue(is_systemd_service_running(domain_resolver))
+
+ self.cli_delete(base_interface_path)
+ self.cli_commit()
+
+ # Ensure the service is no longer running after WireGuard interface is deleted
+ self.assertFalse(is_systemd_service_running(domain_resolver))
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/src/conf_mode/interfaces_wireguard.py b/src/conf_mode/interfaces_wireguard.py
index b6fd6b0b20..877d013cf4 100755
--- a/src/conf_mode/interfaces_wireguard.py
+++ b/src/conf_mode/interfaces_wireguard.py
@@ -29,11 +29,12 @@
from vyos.utils.kernel import check_kmod
from vyos.utils.network import check_port_availability
from vyos.utils.network import is_wireguard_key_pair
+from vyos.utils.process import call
from vyos import ConfigError
from vyos import airbag
+from pathlib import Path
airbag.enable()
-
def get_config(config=None):
"""
Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
@@ -54,6 +55,12 @@ def get_config(config=None):
if is_node_changed(conf, base + [ifname, 'peer']):
wireguard.update({'rebuild_required': {}})
+ wireguard['peers_need_resolve'] = []
+ if 'peer' in wireguard:
+ for peer, peer_config in wireguard['peer'].items():
+ if 'disable' not in peer_config and 'host_name' in peer_config:
+ wireguard['peers_need_resolve'].append(peer)
+
return wireguard
def verify(wireguard):
@@ -82,22 +89,33 @@ def verify(wireguard):
for tmp in wireguard['peer']:
peer = wireguard['peer'][tmp]
+ base_error = f'WireGuard peer "{tmp}":'
+
+ if 'host_name' in peer and 'address' in peer:
+ raise ConfigError(f'{base_error} address/host-name are mutually exclusive!')
+
if 'allowed_ips' not in peer:
- raise ConfigError(f'Wireguard allowed-ips required for peer "{tmp}"!')
+ raise ConfigError(f'{base_error} missing mandatory allowed-ips!')
if 'public_key' not in peer:
- raise ConfigError(f'Wireguard public-key required for peer "{tmp}"!')
-
- if ('address' in peer and 'port' not in peer) or ('port' in peer and 'address' not in peer):
- raise ConfigError('Both Wireguard port and address must be defined '
- f'for peer "{tmp}" if either one of them is set!')
+ raise ConfigError(f'{base_error} missing mandatory public-key!')
if peer['public_key'] in public_keys:
- raise ConfigError(f'Duplicate public-key defined on peer "{tmp}"')
+ raise ConfigError(f'{base_error} duplicate public-key!')
if 'disable' not in peer:
if is_wireguard_key_pair(wireguard['private_key'], peer['public_key']):
- raise ConfigError(f'Peer "{tmp}" has the same public key as the interface "{wireguard["ifname"]}"')
+ tmp = wireguard["ifname"]
+ raise ConfigError(f'{base_error} identical public key as interface "{tmp}"!')
+
+ port_addr_error = f'{base_error} both port and address/host-name must '\
+ 'be defined if either one of them is set!'
+ if 'port' not in peer:
+ if 'host_name' in peer or 'address' in peer:
+ raise ConfigError(port_addr_error)
+ else:
+ if 'host_name' not in peer and 'address' not in peer:
+ raise ConfigError(port_addr_error)
public_keys.append(peer['public_key'])
@@ -122,6 +140,23 @@ def apply(wireguard):
wg = WireGuardIf(**wireguard)
wg.update(wireguard)
+ domain_resolver_usage = '/run/use-vyos-domain-resolver-interfaces-wireguard-' + wireguard['ifname']
+
+ ## DOMAIN RESOLVER
+ domain_action = 'restart'
+ if 'peers_need_resolve' in wireguard and len(wireguard['peers_need_resolve']) > 0 and 'disable' not in wireguard:
+ from vyos.utils.file import write_file
+
+ text = f'# Automatically generated by interfaces_wireguard.py\nThis file indicates that vyos-domain-resolver service is used by the interfaces_wireguard.\n'
+ text += "intefaces:\n" + "".join([f" - {peer}\n" for peer in wireguard['peers_need_resolve']])
+ Path(domain_resolver_usage).write_text(text)
+ write_file(domain_resolver_usage, text)
+ else:
+ Path(domain_resolver_usage).unlink(missing_ok=True)
+ if not Path('/run').glob('use-vyos-domain-resolver*'):
+ domain_action = 'stop'
+ call(f'systemctl {domain_action} vyos-domain-resolver.service')
+
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py
index 98b2f3f293..504b3e82ad 100755
--- a/src/conf_mode/nat.py
+++ b/src/conf_mode/nat.py
@@ -17,6 +17,7 @@
import os
from sys import exit
+from pathlib import Path
from vyos.base import Warning
from vyos.config import Config
@@ -43,7 +44,6 @@
nftables_nat_config = '/run/nftables_nat.conf'
nftables_static_nat_conf = '/run/nftables_static-nat-rules.nft'
domain_resolver_usage = '/run/use-vyos-domain-resolver-nat'
-domain_resolver_usage_firewall = '/run/use-vyos-domain-resolver-firewall'
valid_groups = [
'address_group',
@@ -265,9 +265,9 @@ def apply(nat):
text = f'# Automatically generated by nat.py\nThis file indicates that vyos-domain-resolver service is used by nat.\n'
write_file(domain_resolver_usage, text)
elif os.path.exists(domain_resolver_usage):
- os.unlink(domain_resolver_usage)
- if not os.path.exists(domain_resolver_usage_firewall):
- # Firewall not using domain resolver
+ Path(domain_resolver_usage).unlink(missing_ok=True)
+
+ if not Path('/run').glob('use-vyos-domain-resolver*'):
domain_action = 'stop'
call(f'systemctl {domain_action} vyos-domain-resolver.service')
diff --git a/src/op_mode/reset_wireguard.py b/src/op_mode/reset_wireguard.py
new file mode 100755
index 0000000000..1fcfb31b5d
--- /dev/null
+++ b/src/op_mode/reset_wireguard.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2025 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 .
+
+import sys
+import typing
+
+import vyos.opmode
+
+from vyos.ifconfig import WireGuardIf
+from vyos.configquery import ConfigTreeQuery
+
+
+def _verify(func):
+ """Decorator checks if WireGuard interface config exists"""
+ from functools import wraps
+
+ @wraps(func)
+ def _wrapper(*args, **kwargs):
+ config = ConfigTreeQuery()
+ interface = kwargs.get('interface')
+ if not config.exists(['interfaces', 'wireguard', interface]):
+ unconf_message = f'WireGuard interface {interface} is not configured'
+ raise vyos.opmode.UnconfiguredSubsystem(unconf_message)
+ return func(*args, **kwargs)
+
+ return _wrapper
+
+
+@_verify
+def reset_peer(interface: str, peer: typing.Optional[str] = None):
+ intf = WireGuardIf(interface, create=False, debug=False)
+ return intf.operational.reset_peer(peer)
+
+
+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/services/vyos-domain-resolver b/src/services/vyos-domain-resolver
index bc74a05d15..fe0f40a079 100755
--- a/src/services/vyos-domain-resolver
+++ b/src/services/vyos-domain-resolver
@@ -22,8 +22,10 @@ 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.ifconfig import WireGuardIf
from vyos.utils.commit import commit_in_progress
from vyos.utils.dict import dict_search_args
+from vyos.utils.kernel import WIREGUARD_REKEY_AFTER_TIME
from vyos.utils.process import cmd
from vyos.utils.process import run
from vyos.xml_ref import get_defaults
@@ -33,6 +35,7 @@ timeout = 300
cache = False
base_firewall = ['firewall']
base_nat = ['nat']
+base_interfaces = ['interfaces']
domain_state = {}
@@ -171,8 +174,45 @@ def update_fqdn(config, node):
logger.info(f'Updated {count} sets in {node} - result: {code}')
+def update_interfaces(config, node):
+ if node == 'interfaces':
+ wg_interfaces = dict_search_args(config, 'wireguard')
+
+ peer_public_keys = {}
+ # for each wireguard interfaces
+ for interface, wireguard in wg_interfaces.items():
+ peer_public_keys[interface] = []
+ for peer, peer_config in wireguard['peer'].items():
+ # check peer if peer host-name or address is set
+ if 'host_name' in peer_config or 'address' in peer_config:
+ # check latest handshake
+ peer_public_keys[interface].append(
+ peer_config['public_key']
+ )
+
+ now_time = time.time()
+ for (interface, check_peer_public_keys) in peer_public_keys.items():
+ if len(check_peer_public_keys) == 0:
+ continue
+
+ intf = WireGuardIf(interface, create=False, debug=False)
+ handshakes = intf.operational.get_latest_handshakes()
+
+ # WireGuard performs a handshake every WIREGUARD_REKEY_AFTER_TIME
+ # if data is being transmitted between the peers. If no data is
+ # transmitted, the handshake will not be initiated unless new
+ # data begins to flow. Each handshake generates a new session
+ # key, and the key is rotated at least every 120 seconds or
+ # upon data transmission after a prolonged silence.
+ for public_key, handshake_time in handshakes.items():
+ if public_key in check_peer_public_keys and (
+ handshake_time == 0
+ or (now_time - handshake_time > 3*WIREGUARD_REKEY_AFTER_TIME)
+ ):
+ intf.operational.reset_peer(public_key=public_key)
+
if __name__ == '__main__':
- logger.info(f'VyOS domain resolver')
+ logger.info('VyOS domain resolver')
count = 1
while commit_in_progress():
@@ -184,10 +224,12 @@ if __name__ == '__main__':
conf = ConfigTreeQuery()
firewall = get_config(conf, base_firewall)
nat = get_config(conf, base_nat)
+ interfaces = get_config(conf, base_interfaces)
logger.info(f'interval: {timeout}s - cache: {cache}')
while True:
update_fqdn(firewall, 'firewall')
update_fqdn(nat, 'nat')
+ update_interfaces(interfaces, 'interfaces')
time.sleep(timeout)