From 2212a438b234f34f32e08efef2f841ba55a3b6a0 Mon Sep 17 00:00:00 2001 From: sskaje Date: Tue, 31 Dec 2024 10:44:01 +0800 Subject: wireguard: T4930: allow peers via FQDN * set interfaces wireguard wgXX peer YY hostname --- src/conf_mode/interfaces_wireguard.py | 40 +++++++++++++++++++++---- src/conf_mode/nat.py | 8 ++--- src/op_mode/reset_wireguard.py | 55 +++++++++++++++++++++++++++++++++++ src/services/vyos-domain-resolver | 48 ++++++++++++++++++++++++++++++ 4 files changed, 142 insertions(+), 9 deletions(-) create mode 100755 src/op_mode/reset_wireguard.py (limited to 'src') diff --git a/src/conf_mode/interfaces_wireguard.py b/src/conf_mode/interfaces_wireguard.py index b6fd6b0b2..1dbaa9d4e 100755 --- a/src/conf_mode/interfaces_wireguard.py +++ b/src/conf_mode/interfaces_wireguard.py @@ -29,11 +29,12 @@ from vyos.ifconfig import WireGuardIf 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,16 +89,15 @@ def verify(wireguard): for tmp in wireguard['peer']: peer = wireguard['peer'][tmp] + if 'host_name' in peer and 'address' in peer: + raise ConfigError('"host-name" and "address" are mutually exclusive') + if 'allowed_ips' not in peer: raise ConfigError(f'Wireguard allowed-ips required for peer "{tmp}"!') 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!') - if peer['public_key'] in public_keys: raise ConfigError(f'Duplicate public-key defined on peer "{tmp}"') @@ -99,6 +105,13 @@ def verify(wireguard): 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"]}"') + if 'port' not in peer: + if 'host_name' in peer or 'address' in peer: + raise ConfigError(f'Missing "host-name" or "address" on peer "{tmp}"') + else: + if 'host_name' not in peer and 'address' not in peer: + raise ConfigError(f'Missing "host-name" and "address" on peer "{tmp}"') + public_keys.append(peer['public_key']) def generate(wireguard): @@ -122,6 +135,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 98b2f3f29..504b3e82a 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 @@ k_mod = ['nft_nat', 'nft_chain_nat'] 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 000000000..1fcfb31b5 --- /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 bc74a05d1..6eab7e7e5 100755 --- a/src/services/vyos-domain-resolver +++ b/src/services/vyos-domain-resolver @@ -27,12 +27,14 @@ from vyos.utils.dict import dict_search_args from vyos.utils.process import cmd from vyos.utils.process import run from vyos.xml_ref import get_defaults +from vyos.template import is_ip base = ['firewall'] timeout = 300 cache = False base_firewall = ['firewall'] base_nat = ['nat'] +base_interfaces = ['interfaces'] domain_state = {} @@ -171,6 +173,50 @@ def update_fqdn(config, node): logger.info(f'Updated {count} sets in {node} - result: {code}') +def update_interfaces(config, node): + if node == 'interfaces': + wireguard_interfaces = dict_search_args(config, 'wireguard') + + # WireGuard redo handshake usually every 180 seconds, but not documented officially. + # If peer with domain name in its endpoint didn't get handshake for over 300 seconds, + # we do re-resolv and reset its endpoint from config tree. + handshake_threshold = 300 + + from vyos.ifconfig import WireGuardIf + + check_wireguard_peer_public_keys = {} + # for each wireguard interfaces + for interface, wireguard in wireguard_interfaces.items(): + check_wireguard_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 + check_wireguard_peer_public_keys[interface].append( + peer_config['public_key'] + ) + + now_time = time.time() + for ( + interface, + check_peer_public_keys + ) in check_wireguard_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() + + 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 > handshake_threshold + ): + intf.operational.reset_peer(public_key=public_key) + + print(f'Wireguard: reset {interface} peer {public_key}') + + if __name__ == '__main__': logger.info(f'VyOS domain resolver') @@ -184,10 +230,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) -- cgit v1.2.3 From c4c35d3b7a9de76802663376b82c7decfc878980 Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Sat, 18 Jan 2025 22:44:35 +0100 Subject: wireguard: T4930: use common error message pattern --- src/conf_mode/interfaces_wireguard.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/conf_mode/interfaces_wireguard.py b/src/conf_mode/interfaces_wireguard.py index 1dbaa9d4e..877d013cf 100755 --- a/src/conf_mode/interfaces_wireguard.py +++ b/src/conf_mode/interfaces_wireguard.py @@ -89,28 +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('"host-name" and "address" are mutually exclusive') + 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}"!') + 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(f'Missing "host-name" or "address" on peer "{tmp}"') + raise ConfigError(port_addr_error) else: if 'host_name' not in peer and 'address' not in peer: - raise ConfigError(f'Missing "host-name" and "address" on peer "{tmp}"') + raise ConfigError(port_addr_error) public_keys.append(peer['public_key']) -- cgit v1.2.3 From f01c4d0173bb49bfd5bd4f1ef5675cc8c597595a Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Sat, 18 Jan 2025 23:06:37 +0100 Subject: wireguard: T4930: add mnemonic for WIREGUARD_REKEY_AFTER_TIME 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. --- python/vyos/utils/kernel.py | 4 ++++ src/services/vyos-domain-resolver | 38 +++++++++++++++++--------------------- 2 files changed, 21 insertions(+), 21 deletions(-) (limited to 'src') diff --git a/python/vyos/utils/kernel.py b/python/vyos/utils/kernel.py index 847f80108..05eac8a6a 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/src/services/vyos-domain-resolver b/src/services/vyos-domain-resolver index 6eab7e7e5..a4b0869fa 100755 --- a/src/services/vyos-domain-resolver +++ b/src/services/vyos-domain-resolver @@ -22,12 +22,13 @@ 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 -from vyos.template import is_ip base = ['firewall'] timeout = 300 @@ -175,50 +176,45 @@ def update_fqdn(config, node): def update_interfaces(config, node): if node == 'interfaces': - wireguard_interfaces = dict_search_args(config, 'wireguard') + wg_interfaces = dict_search_args(config, 'wireguard') - # WireGuard redo handshake usually every 180 seconds, but not documented officially. - # If peer with domain name in its endpoint didn't get handshake for over 300 seconds, - # we do re-resolv and reset its endpoint from config tree. - handshake_threshold = 300 - - from vyos.ifconfig import WireGuardIf - - check_wireguard_peer_public_keys = {} + peer_public_keys = {} # for each wireguard interfaces - for interface, wireguard in wireguard_interfaces.items(): - check_wireguard_peer_public_keys[interface] = [] + 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 - check_wireguard_peer_public_keys[interface].append( + peer_public_keys[interface].append( peer_config['public_key'] ) now_time = time.time() - for ( - interface, - check_peer_public_keys - ) in check_wireguard_peer_public_keys.items(): + 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 > handshake_threshold + or (now_time - handshake_time > 3*WIREGUARD_REKEY_AFTER_TIME) ): intf.operational.reset_peer(public_key=public_key) - - print(f'Wireguard: reset {interface} peer {public_key}') + print(f'WireGuard: reset {interface} peer {public_key}') if __name__ == '__main__': - logger.info(f'VyOS domain resolver') + logger.info('VyOS domain resolver') count = 1 while commit_in_progress(): -- cgit v1.2.3 From 47b6afa14210834f5add9a509e594ec2ed4b1e42 Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Sat, 18 Jan 2025 23:33:40 +0100 Subject: wireguard: T4930: fix dict key (-/_) when working with config dict Retrieving the config dict sets key_mangling(), thus we need to look for host_name instead of host-name. --- src/services/vyos-domain-resolver | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'src') diff --git a/src/services/vyos-domain-resolver b/src/services/vyos-domain-resolver index a4b0869fa..fe0f40a07 100755 --- a/src/services/vyos-domain-resolver +++ b/src/services/vyos-domain-resolver @@ -184,7 +184,7 @@ def update_interfaces(config, node): 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: + if 'host_name' in peer_config or 'address' in peer_config: # check latest handshake peer_public_keys[interface].append( peer_config['public_key'] @@ -210,8 +210,6 @@ def update_interfaces(config, node): or (now_time - handshake_time > 3*WIREGUARD_REKEY_AFTER_TIME) ): intf.operational.reset_peer(public_key=public_key) - print(f'WireGuard: reset {interface} peer {public_key}') - if __name__ == '__main__': logger.info('VyOS domain resolver') -- cgit v1.2.3