diff options
| author | Daniil Baturin <daniil@vyos.io> | 2025-01-24 18:13:08 +0000 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-01-24 18:13:08 +0000 | 
| commit | 2f8d231f4ae16cd49a36bc3d5e11b25db1501240 (patch) | |
| tree | 1a5c6d3fb9acbbb23fa47084c7b4d9676b061b5a /src | |
| parent | f0e05ba825f5f154c570487d1b189a8be6f3b121 (diff) | |
| parent | 98414a69f0018915ac999f51975618dd5fbe817d (diff) | |
| download | vyos-1x-2f8d231f4ae16cd49a36bc3d5e11b25db1501240.tar.gz vyos-1x-2f8d231f4ae16cd49a36bc3d5e11b25db1501240.zip | |
Merge pull request #4200 from sskaje/T4930-1
T4930: Allow WireGuard peers via DNS hostname
Diffstat (limited to 'src')
| -rwxr-xr-x | src/conf_mode/interfaces_wireguard.py | 53 | ||||
| -rwxr-xr-x | src/conf_mode/nat.py | 8 | ||||
| -rwxr-xr-x | src/op_mode/reset_wireguard.py | 55 | ||||
| -rwxr-xr-x | src/services/vyos-domain-resolver | 44 | 
4 files changed, 146 insertions, 14 deletions
| diff --git a/src/conf_mode/interfaces_wireguard.py b/src/conf_mode/interfaces_wireguard.py index b6fd6b0b2..877d013cf 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,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 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 <http://www.gnu.org/licenses/>. + +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..fe0f40a07 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) | 
