summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--interface-definitions/interfaces-wireguard.xml.in8
-rw-r--r--python/vyos/ifconfig/wireguard.py136
-rwxr-xr-xsrc/conf_mode/interfaces-wireguard.py300
3 files changed, 139 insertions, 305 deletions
diff --git a/interface-definitions/interfaces-wireguard.xml.in b/interface-definitions/interfaces-wireguard.xml.in
index edf9bf696..981bce826 100644
--- a/interface-definitions/interfaces-wireguard.xml.in
+++ b/interface-definitions/interfaces-wireguard.xml.in
@@ -33,6 +33,7 @@
<validator name="numeric" argument="--range 0-4294967295"/>
</constraint>
</properties>
+ <defaultValue>0</defaultValue>
</leafNode>
<leafNode name="private-key">
<properties>
@@ -41,6 +42,7 @@
<script>${vyos_op_scripts_dir}/wireguard.py --listkdir</script>
</completionHelp>
</properties>
+ <defaultValue>default</defaultValue>
</leafNode>
<tagNode name="peer">
<properties>
@@ -103,7 +105,11 @@
#include <include/port-number.xml.i>
<leafNode name="persistent-keepalive">
<properties>
- <help>how often send keep alives in seconds</help>
+ <help>Interval to send keepalive messages</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Interval in seconds</description>
+ </valueHelp>
<constraint>
<validator name="numeric" argument="--range 1-65535"/>
</constraint>
diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py
index 62ca57ca2..fad4ef282 100644
--- a/python/vyos/ifconfig/wireguard.py
+++ b/python/vyos/ifconfig/wireguard.py
@@ -24,7 +24,7 @@ from hurry.filesize import alternative
from vyos.config import Config
from vyos.ifconfig import Interface
from vyos.ifconfig import Operational
-
+from vyos.validate import is_ipv6
class WireGuardOperational(Operational):
def _dump(self):
@@ -169,65 +169,79 @@ class WireGuardIf(Interface):
['port', 'private_key', 'pubkey', 'psk',
'allowed_ips', 'fwmark', 'endpoint', 'keepalive']
- """
- Wireguard interface class, contains a comnfig dictionary since
- wireguard VPN is being comnfigured via the wg command rather than
- writing the config into a file. Otherwise if a pre-shared key is used
- (symetric enryption key), it would we exposed within multiple files.
- Currently it's only within the config.boot if the config was saved.
-
- Example:
- >>> from vyos.ifconfig import WireGuardIf as wg_if
- >>> wg_intfc = wg_if("wg01")
- >>> print (wg_intfc.wg_config)
- {'private_key': None, 'keepalive': 0, 'endpoint': None, 'port': 0,
- 'allowed_ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'}
- >>> wg_intfc.wg_config['keepalive'] = 100
- >>> print (wg_intfc.wg_config)
- {'private_key': None, 'keepalive': 100, 'endpoint': None, 'port': 0,
- 'allowed_ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'}
- """
-
- def update(self):
- if not self.config['private_key']:
- raise ValueError("private key required")
- else:
- # fmask permission check?
- pass
-
- cmd = 'wg set {ifname}'.format(**self.config)
- cmd += ' listen-port {port}'.format(**self.config)
- cmd += ' fwmark "{fwmark}" '.format(**self.config)
- cmd += ' private-key {private_key}'.format(**self.config)
- cmd += ' peer {pubkey}'.format(**self.config)
- cmd += ' persistent-keepalive {keepalive}'.format(**self.config)
- # allowed-ips must be properly quoted else the interface can't be properly
- # created as the wg utility will tread multiple IP addresses as command
- # parameters
- cmd += ' allowed-ips "{}"'.format(','.join(self.config['allowed-ips']))
-
- if self.config['endpoint']:
- cmd += ' endpoint "{endpoint}"'.format(**self.config)
-
- psk_file = ''
- if self.config['psk']:
- psk_file = '/tmp/{ifname}.psk'.format(**self.config)
- with open(psk_file, 'w') as f:
- f.write(self.config['psk'])
+ 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. """
+
+ # remove no longer associated peers first
+ if 'peer_remove' in config:
+ for tmp in config['peer_remove']:
+ peer = config['peer_remove'][tmp]
+ peer['ifname'] = config['ifname']
+
+ cmd = 'wg set {ifname} peer {pubkey} remove'
+ self._cmd(cmd.format(**peer))
+
+ # Wireguard base command is identical for every peer
+ base_cmd = 'wg set {ifname} private-key {private_key}'
+ if 'port' in config:
+ base_cmd += ' listen-port {port}'
+ if 'fwmark' in config:
+ base_cmd += ' fwmark {fwmark}'
+
+ base_cmd = base_cmd.format(**config)
+
+ for tmp in config['peer']:
+ peer = config['peer'][tmp]
+
+ # start of with a fresh 'wg' command
+ cmd = base_cmd + ' peer {pubkey}'
+
+ # 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:
+ psk_file = '/tmp/tmp.wireguard.psk'
+ with open(psk_file, 'w') as f:
+ f.write(peer['preshared_key'])
cmd += f' preshared-key {psk_file}'
- self._cmd(cmd)
-
- # PSK key file is not required to be stored persistently as its backed by CLI
- if os.path.exists(psk_file):
- os.remove(psk_file)
-
- def remove_peer(self, peerkey):
- """
- Remove a peer of an interface, peers are identified by their public key.
- Giving it a readable name is a vyos feature, to remove a peer the pubkey
- and the interface is needed, to remove the entry.
- """
- cmd = "wg set {0} peer {1} remove".format(
- self.config['ifname'], str(peerkey))
- return self._cmd(cmd)
+ # Persistent keepalive is optional
+ if 'persistent_keepalive'in peer:
+ cmd += ' persistent-keepalive {persistent_keepalive}'
+
+ # Multiple allowed-ip ranges can be defined - ensure we are always
+ # dealing with a list
+ if isinstance(peer['allowed_ips'], str):
+ peer['allowed_ips'] = [peer['allowed_ips']]
+ cmd += ' allowed-ips ' + ','.join(peer['allowed_ips'])
+
+ # Endpoint configuration is optional
+ if {'address', 'port'} <= set(peer):
+ if is_ipv6(config['address']):
+ cmd += ' endpoint [{address}]:{port}'
+ else:
+ cmd += ' endpoint {address}:{port}'
+
+ self._cmd(cmd.format(**peer))
+
+ # 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)
+
+ # Enable/Disable of an interface must always be done at the end of the
+ # derived class to make use of the ref-counting set_admin_state()
+ # function. We will only enable the interface if 'up' was called as
+ # often as 'down'. This is required by some interface implementations
+ # as certain parameters can only be changed when the interface is
+ # in admin-down state. This ensures the link does not flap during
+ # reconfiguration.
+ state = 'down' if 'disable' in config else 'up'
+ self.set_admin_state(state)
+
diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py
index 982aefa5f..6325a8b05 100755
--- a/src/conf_mode/interfaces-wireguard.py
+++ b/src/conf_mode/interfaces-wireguard.py
@@ -15,44 +15,29 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
-import re
from sys import exit
from copy import deepcopy
-from netifaces import interfaces
from vyos.config import Config
-from vyos.configdict import list_diff
+from vyos.configdict import dict_merge
+from vyos.configdict import get_interface_dict
+from vyos.configdict import node_changed
+from vyos.configdict import leaf_node_changed
+from vyos.configverify import verify_vrf
+from vyos.configverify import verify_address
+from vyos.configverify import verify_bridge_delete
from vyos.ifconfig import WireGuardIf
-from vyos.util import chown, chmod_750, call
+from vyos.util import chown, chmod_750
from vyos.util import check_kmod
-from vyos.validate import is_member, is_ipv6
from vyos import ConfigError
-
from vyos import airbag
airbag.enable()
-kdir = r'/config/auth/wireguard'
k_mod = 'wireguard'
-default_config_data = {
- 'intfc': '',
- 'address': [],
- 'address_remove': [],
- 'description': '',
- 'listen_port': '',
- 'deleted': False,
- 'disable': False,
- 'fwmark': 0,
- 'is_bridge_member': False,
- 'mtu': 1420,
- 'peer': [],
- 'peer_remove': [], # stores public keys of peers to remove
- 'pk': f'{kdir}/default/private.key',
- 'vrf': ''
-}
-
def _migrate_default_keys():
+ kdir = r'/config/auth/wireguard'
if os.path.exists(f'{kdir}/private.key') and not os.path.exists(f'{kdir}/default/private.key'):
location = f'{kdir}/default'
if not os.path.exists(location):
@@ -65,246 +50,75 @@ def _migrate_default_keys():
def get_config():
+ """
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+ interface name will be added or a deleted flag
+ """
conf = Config()
base = ['interfaces', 'wireguard']
+ wireguard = get_interface_dict(conf, base)
- # determine tagNode instance
- if 'VYOS_TAGNODE_VALUE' not in os.environ:
- raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
-
- wg = deepcopy(default_config_data)
- wg['intf'] = os.environ['VYOS_TAGNODE_VALUE']
-
- # check if interface is member if a bridge
- wg['is_bridge_member'] = is_member(conf, wg['intf'], 'bridge')
-
- # Check if interface has been removed
- if not conf.exists(base + [wg['intf']]):
- wg['deleted'] = True
- return wg
-
- conf.set_level(base + [wg['intf']])
-
- # retrieve configured interface addresses
- if conf.exists(['address']):
- wg['address'] = conf.return_values(['address'])
-
- # get interface addresses (currently effective) - to determine which
- # address is no longer valid and needs to be removed
- eff_addr = conf.return_effective_values(['address'])
- wg['address_remove'] = list_diff(eff_addr, wg['address'])
-
- # retrieve interface description
- if conf.exists(['description']):
- wg['description'] = conf.return_value(['description'])
-
- # disable interface
- if conf.exists(['disable']):
- wg['disable'] = True
-
- # local port to listen on
- if conf.exists(['port']):
- wg['listen_port'] = conf.return_value(['port'])
-
- # fwmark value
- if conf.exists(['fwmark']):
- wg['fwmark'] = int(conf.return_value(['fwmark']))
-
- # Maximum Transmission Unit (MTU)
- if conf.exists('mtu'):
- wg['mtu'] = int(conf.return_value(['mtu']))
-
- # retrieve VRF instance
- if conf.exists('vrf'):
- wg['vrf'] = conf.return_value('vrf')
-
- # private key
- if conf.exists(['private-key']):
- wg['pk'] = "{0}/{1}/private.key".format(
- kdir, conf.return_value(['private-key']))
-
- # peer removal, wg identifies peers by its pubkey
- peer_eff = conf.list_effective_nodes(['peer'])
- peer_rem = list_diff(peer_eff, conf.list_nodes(['peer']))
- for peer in peer_rem:
- wg['peer_remove'].append(
- conf.return_effective_value(['peer', peer, 'pubkey']))
-
- # peer settings
- if conf.exists(['peer']):
- for p in conf.list_nodes(['peer']):
- # set new config level for this peer
- conf.set_level(base + [wg['intf'], 'peer', p])
- peer = {
- 'allowed-ips': [],
- 'address': '',
- 'name': p,
- 'persistent_keepalive': '',
- 'port': '',
- 'psk': '',
- 'pubkey': ''
- }
-
- # peer allowed-ips
- if conf.exists(['allowed-ips']):
- peer['allowed-ips'] = conf.return_values(['allowed-ips'])
-
- # peer address
- if conf.exists(['address']):
- peer['address'] = conf.return_value(['address'])
-
- # peer port
- if conf.exists(['port']):
- peer['port'] = conf.return_value(['port'])
+ # Wireguard is "special" the default MTU is 1420 - update accordingly
+ # as the config_level is already st in get_interface_dict() - we can use []
+ tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True)
+ if 'mtu' not in tmp:
+ wireguard['mtu'] = '1420'
- # persistent-keepalive
- if conf.exists(['persistent-keepalive']):
- peer['persistent_keepalive'] = conf.return_value(['persistent-keepalive'])
+ # Mangle private key - it has a default so its always valid
+ wireguard['private_key'] = '/config/auth/wireguard/{private_key}/private.key'.format(**wireguard)
- # preshared-key
- if conf.exists(['preshared-key']):
- peer['psk'] = conf.return_value(['preshared-key'])
+ # Determine which Wireguard peer has been removed.
+ # Peers can only be removed with their public key!
+ tmp = node_changed(conf, ['peer'])
+ if tmp:
+ dict = {}
+ for peer in tmp:
+ peer_config = leaf_node_changed(conf, ['peer', peer, 'pubkey'])
+ dict = dict_merge({'peer_remove' : {peer : {'pubkey' : peer_config}}}, dict)
+ wireguard.update(dict)
- # peer pubkeys
- if conf.exists(['pubkey']):
- key_eff = conf.return_effective_value(['pubkey'])
- key_cfg = conf.return_value(['pubkey'])
- peer['pubkey'] = key_cfg
+ return wireguard
- # on a pubkey change we need to remove the pubkey first
- # peers are identified by pubkey, so key update means
- # peer removal and re-add
- if key_eff != key_cfg and key_eff != None:
- wg['peer_remove'].append(key_cfg)
-
- # if a peer is disabled, we have to exec a remove for it's pubkey
- if conf.exists(['disable']):
- wg['peer_remove'].append(peer['pubkey'])
- else:
- wg['peer'].append(peer)
-
- return wg
-
-
-def verify(wg):
- if wg['deleted']:
- if wg['is_bridge_member']:
- raise ConfigError((
- f'Cannot delete interface "{wg["intf"]}" as it is a member '
- f'of bridge "{wg["is_bridge_member"]}"!'))
+def verify(wireguard):
+ if 'deleted' in wireguard:
+ verify_bridge_delete(wireguard)
return None
- if wg['is_bridge_member'] and wg['address']:
- raise ConfigError((
- f'Cannot assign address to interface "{wg["intf"]}" '
- f'as it is a member of bridge "{wg["is_bridge_member"]}"!'))
-
- if wg['vrf']:
- if wg['vrf'] not in interfaces():
- raise ConfigError(f'VRF "{wg["vrf"]}" does not exist')
+ verify_address(wireguard)
+ verify_vrf(wireguard)
- if wg['is_bridge_member']:
- raise ConfigError((
- f'Interface "{wg["intf"]}" cannot be member of VRF '
- f'"{wg["vrf"]}" and bridge {wg["is_bridge_member"]} '
- f'at the same time!'))
+ if not os.path.exists(wireguard['private_key']):
+ raise ConfigError('Wireguard private-key not found! Execute: ' \
+ '"run generate wireguard [default-keypair|named-keypairs]"')
- if not os.path.exists(wg['pk']):
- raise ConfigError('No keys found, generate them by executing:\n' \
- '"run generate wireguard [keypair|named-keypairs]"')
+ if 'address' not in wireguard:
+ raise ConfigError('IP address required!')
- if not wg['address']:
- raise ConfigError(f'IP address required for interface "{wg["intf"]}"!')
-
- if not wg['peer']:
- raise ConfigError(f'Peer required for interface "{wg["intf"]}"!')
+ if 'peer' not in wireguard:
+ raise ConfigError('At least one Wireguard peer is required!')
# run checks on individual configured WireGuard peer
- for peer in wg['peer']:
- if not peer['allowed-ips']:
- raise ConfigError(f'Peer allowed-ips required for peer "{peer["name"]}"!')
-
- if not peer['pubkey']:
- raise ConfigError(f'Peer public-key required for peer "{peer["name"]}"!')
-
- if peer['address'] and not peer['port']:
- raise ConfigError(f'Peer "{peer["name"]}" port must be defined if address is defined!')
+ for tmp in wireguard['peer']:
+ peer = wireguard['peer'][tmp]
- if not peer['address'] and peer['port']:
- raise ConfigError(f'Peer "{peer["name"]}" address must be defined if port is defined!')
+ if 'allowed_ips' not in peer:
+ raise ConfigError(f'Wireguard allowed-ips required for peer "{tmp}"!')
+ if 'pubkey' not in peer:
+ raise ConfigError(f'Wireguard public-key required for peer "{tmp}"!')
-def apply(wg):
- # init wg class
- w = WireGuardIf(wg['intf'])
+ 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!')
- # single interface removal
- if wg['deleted']:
- w.remove()
+def apply(wireguard):
+ if 'deleted' in wireguard:
+ WireGuardIf(wireguard['ifname']).remove()
return None
- # Configure interface address(es)
- # - not longer required addresses get removed first
- # - newly addresses will be added second
- for addr in wg['address_remove']:
- w.del_addr(addr)
- for addr in wg['address']:
- w.add_addr(addr)
-
- # Maximum Transmission Unit (MTU)
- w.set_mtu(wg['mtu'])
-
- # update interface description used e.g. within SNMP
- w.set_alias(wg['description'])
-
- # assign/remove VRF (ONLY when not a member of a bridge,
- # otherwise 'nomaster' removes it from it)
- if not wg['is_bridge_member']:
- w.set_vrf(wg['vrf'])
-
- # remove peers
- for pub_key in wg['peer_remove']:
- w.remove_peer(pub_key)
-
- # peer pubkey
- # setting up the wg interface
- w.config['private_key'] = c['pk']
-
- for peer in wg['peer']:
- # peer pubkey
- w.config['pubkey'] = peer['pubkey']
- # peer allowed-ips
- w.config['allowed-ips'] = peer['allowed-ips']
- # local listen port
- if wg['listen_port']:
- w.config['port'] = wg['listen_port']
- # fwmark
- if c['fwmark']:
- w.config['fwmark'] = wg['fwmark']
-
- # endpoint
- if peer['address'] and peer['port']:
- if is_ipv6(peer['address']):
- w.config['endpoint'] = '[{}]:{}'.format(peer['address'], peer['port'])
- else:
- w.config['endpoint'] = '{}:{}'.format(peer['address'], peer['port'])
-
- # persistent-keepalive
- if peer['persistent_keepalive']:
- w.config['keepalive'] = peer['persistent_keepalive']
-
- if peer['psk']:
- w.config['psk'] = peer['psk']
-
- w.update()
-
- # Enable/Disable interface
- if wg['disable']:
- w.set_admin_state('down')
- else:
- w.set_admin_state('up')
-
+ w = WireGuardIf(wireguard['ifname'])
+ w.update(wireguard)
return None
if __name__ == '__main__':