Skip to content

Commit

Permalink
T4930: code style changes
Browse files Browse the repository at this point in the history
T4930: print previous/current endpoint when resetting peer
T4930: resolve code change requests
T4930: Fix ConfigTreeQuery usages
T4930: resolve code change requests
T4930: make wireguard domain resolver run flag files separated by interface; code style
T4930: Move dns resolution to vyos-domain-resolver
T4930: make wg dns retry configurable through `interfaces wireguard wgX max-dns-retry`
T4930: simplify reset-wireguard.xml.in
T4930: code style changes for python 3.11
T4930: code style changes
T4930: code style changes
T4930: limit wg retry times by using environment variable
T4930: Ensure peer is created even if dns not working
T4930: Allow WireGuard peers via DNS hostname + new script resetting peer
  • Loading branch information
sskaje committed Dec 31, 2024
1 parent ec80c75 commit 68f3470
Show file tree
Hide file tree
Showing 8 changed files with 328 additions and 46 deletions.
18 changes: 18 additions & 0 deletions interface-definitions/interfaces_wireguard.xml.in
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,19 @@
</properties>
<defaultValue>0</defaultValue>
</leafNode>
<leafNode name="max-dns-retry">
<properties>
<help>Max retry when DNS resolves failed.</help>
<valueHelp>
<format>u32:1-15</format>
<description>Max retry times</description>
</valueHelp>
<constraint>
<validator name="numeric" argument="--range 1-15"/>
</constraint>
</properties>
<defaultValue>3</defaultValue>
</leafNode>
<leafNode name="private-key">
<properties>
<help>Base64 encoded private key</help>
Expand Down Expand Up @@ -98,9 +111,14 @@
<format>ipv6</format>
<description>IPv6 address of remote tunnel endpoint</description>
</valueHelp>
<valueHelp>
<format>hostname</format>
<description>Endpoint FQDN</description>
</valueHelp>
<constraint>
<validator name="ip-address"/>
<validator name="ipv6-link-local"/>
<validator name="fqdn"/>
</constraint>
</properties>
</leafNode>
Expand Down
34 changes: 34 additions & 0 deletions op-mode-definitions/reset-wireguard.xml.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?xml version="1.0"?>
<interfaceDefinition>
<node name="reset">
<children>
<node name="wireguard">
<properties>
<help>Reset WireGuard Peers</help>
</properties>
<children>
<tagNode name="interface">
<properties>
<help>WireGuard interface name</help>
<completionHelp>
<path>interfaces wireguard</path>
</completionHelp>
</properties>
<command>sudo ${vyos_op_scripts_dir}/reset_wireguard.py reset_peer --interface="$4"</command>
<children>
<tagNode name="peer">
<properties>
<help>WireGuard peer name</help>
<completionHelp>
<path>interfaces wireguard ${COMP_WORDS[3]} peer</path>
</completionHelp>
</properties>
<command>sudo ${vyos_op_scripts_dir}/reset_wireguard.py reset_peer --interface="$4" --peer="$6"</command>
</tagNode>
</children>
</tagNode>
</children>
</node>
</children>
</node>
</interfaceDefinition>
4 changes: 2 additions & 2 deletions python/vyos/ifconfig/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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):
"""
Expand Down
183 changes: 144 additions & 39 deletions python/vyos/ifconfig/wireguard.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
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):
Expand Down Expand Up @@ -156,6 +157,86 @@ def show_interface(self):
answer += '\n'
return answer

def get_latest_handshakes(self):
"""Get latest handshake time for each peer"""
output = {}

# Dump wireguard last handshake
_f = self._cmd(f'wg show {self.ifname} latest-handshakes')
# Output:
# xxxw= 1732812147
# xxx= 0
for line in _f.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']
)
port = c.value(
['interfaces', 'wireguard', self.ifname, 'peer', peer, 'port']
)

if not address or not port:
if peer_name is not None:
print(f'Peer {peer_name} endpoint not set')
continue

if c.exists(
['interfaces', 'wireguard', self.ifname, 'peer', peer, 'disable']
):
continue

cmd = f'wg set {self.ifname} peer {peer_public_key} endpoint {address}:{port}'
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 {address}:{port} ... '
else:
message = f'Resetting {self.ifname} peer {peer_public_key} endpoint to {address}:{port} ... '
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):
Expand Down Expand Up @@ -187,60 +268,84 @@ def update(self, config):
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():
# T4702: No need to configure this peer when it was explicitly
# 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

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)
24 changes: 23 additions & 1 deletion src/conf_mode/interfaces_wireguard.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@
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.template import is_ip
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
Expand All @@ -54,6 +56,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 'address' in peer_config and not is_ip(peer_config['address']):
wireguard['peers_need_resolve'].append(peer)

return wireguard

def verify(wireguard):
Expand Down Expand Up @@ -122,6 +130,20 @@ 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:
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)
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__':
Expand Down
8 changes: 4 additions & 4 deletions src/conf_mode/nat.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import os

from sys import exit
from pathlib import Path

from vyos.base import Warning
from vyos.config import Config
Expand All @@ -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',
Expand Down Expand Up @@ -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')

Expand Down
Loading

0 comments on commit 68f3470

Please sign in to comment.