# Available states | # Available states | ||||
## `wg.present` | |||||
Creates a wireguard interface and sets interface-wide parameters. | |||||
## `wg.peer_present` | |||||
Adds a peer to an interface and sets peer-specific parameters. | |||||
## `wg.absent` | |||||
Removes a wireguard interface. | |||||
## `wg.peer_absent` | |||||
Removes a peer from an interface. | |||||
No states. Include `wireguard` in the top.sls file. |
import yaml | |||||
import os | |||||
from tempfile import mkstemp | |||||
__virtualname__ = 'wg' | |||||
def __virtual__(): | |||||
# do checks for startup | |||||
return __virtualname__ | |||||
def create(name): | |||||
""" | |||||
create a wireguard interface. This will fail if it already exists. | |||||
""" | |||||
ifaces = __salt__['network.interfaces']() | |||||
if name not in ifaces.keys(): | |||||
__salt__['cmd.run']('ip link add %s type wireguard' % (name,)) | |||||
return {name: dict(new=name, old=None)} | |||||
return 'Interface %s already exists' % (name,) | |||||
def delete(name): | |||||
""" | |||||
delete a interface (not neccessarily a wireguard interface). This will fail | |||||
if it does not exist. | |||||
""" | |||||
ifaces = __salt__['network.interfaces']() | |||||
if name in ifaces.keys(): | |||||
__salt__['cmd.run']('ip link del %s' % (name,)) | |||||
return {name: dict(new=None, old=name)} | |||||
return 'Interface %s does not exist' % (name,) | |||||
def show(name=None, peer=None, hide_keys=True): | |||||
if peer and not name: | |||||
return 'If peer is given, name must also be given' | |||||
if not name: | |||||
return _wg_ifaces(hide_keys=hide_keys) | |||||
elif peer: | |||||
return _wg_ifaces(hide_keys=hide_keys).get(name).get('peers').get(peer) | |||||
else: | |||||
return _wg_ifaces(hide_keys=hide_keys).get(name) | |||||
def showconf(name): | |||||
return __salt__['cmd.run']('wg showconf %s' % (name,)) | |||||
def set(name, listen_port=None, fwmark=None, private_key=None, peer=None, | |||||
preshared_key=None, endpoint=None, persistent_keepalive=None, | |||||
allowed_ips=None, remove=False): | |||||
s = 'wg set %s' % (name,) | |||||
if remove: | |||||
if not peer: | |||||
return 'If remove is given, peer must also be given' | |||||
return __salt__['cmd.run']( | |||||
'%s peer %s remove' % (s, peer) | |||||
) | |||||
if listen_port: | |||||
s = '%s listen-port %s' % (s, listen_port) | |||||
if fwmark: | |||||
s = '%s fwmark %s' % (s, fwmark) | |||||
if private_key: | |||||
fd, filename = mkstemp(text=True) | |||||
with open(filename, 'w') as f: | |||||
f.write(private_key) | |||||
os.close(fd) | |||||
s = '%s private-key %s' % (s, filename) | |||||
if peer: | |||||
s = '%s peer %s' % (s, peer) | |||||
if preshared_key != None: | |||||
if not peer: | |||||
return 'If preshared_key is given, peer must also be given' | |||||
fd2, filename2 = mkstemp(text=True) | |||||
with open(filename2, 'w') as f: | |||||
f.write(preshared_key) | |||||
os.close(fd2) | |||||
s = '%s preshared-key %s' % (s, filename2) | |||||
if endpoint: | |||||
if not peer: | |||||
return 'If endpoint is given, peer must also be given' | |||||
s = '%s endpoint %s' % (s, endpoint) | |||||
if persistent_keepalive is not None: | |||||
if not peer: | |||||
return 'If persistent_keepalive is given, peer must also be given' | |||||
s = '%s persistent-keepalive %s' % (s, persistent_keepalive) | |||||
if allowed_ips: | |||||
if not peer: | |||||
return 'If allowed_ips is given, peer must also be given' | |||||
s = '%s allowed-ips %s' % (s, allowed_ips) | |||||
r = __salt__['cmd.run'](s) | |||||
if private_key: | |||||
os.unlink(filename) | |||||
if preshared_key: | |||||
os.unlink(filename2) | |||||
return r | |||||
def remove_peer(name, peer): | |||||
return __salt__['cmd.run']( | |||||
'wg set %s peer %s remove' % (name, peer) | |||||
) | |||||
def genkey(): | |||||
return __salt__['cmd.run']('wg genkey') | |||||
def genpsk(): | |||||
return __salt__['cmd.run']('wg genpsk') | |||||
def setconf(name, path): | |||||
return __salt__['cmd.run']('wg setconf %s %s' % (name, path)) | |||||
def addconf(name, path): | |||||
return __salt__['cmd.run']('wg addconf %s %s' % (name, path)) | |||||
def _wg_ifaces(hide_keys=True): | |||||
""" | |||||
Parse output from 'wg show' | |||||
""" | |||||
# from https://github.com/saltstack/salt/blob/develop/salt/modules/linux_ip.py | |||||
tmp = dict() | |||||
tmpiface = dict() | |||||
ifaces = dict() | |||||
out = __salt__['cmd.run']('wg show all', | |||||
env={'WG_HIDE_KEYS': 'always' if hide_keys else 'never'}) | |||||
for line in out.splitlines(): | |||||
if line.startswith('interface: '): | |||||
k, v = _wg_splitline(line) | |||||
ifaces[v] = dict(peers=dict()) | |||||
tmpiface = ifaces[v] | |||||
tmp = tmpiface | |||||
elif line.startswith('peer: '): | |||||
k, v = _wg_splitline(line) | |||||
tmpiface['peers'][v] = dict() | |||||
tmp = tmpiface['peers'][v] | |||||
elif line == '': | |||||
continue | |||||
k, v = _wg_splitline(line) | |||||
if k == 'allowed ips': | |||||
tmp[k] = [ s.strip() for s in v.split(',') ] | |||||
else: | |||||
tmp[k] = v | |||||
return ifaces | |||||
def _wg_splitline(line): | |||||
parts = line.split(':', 1) | |||||
return parts[0].strip(), parts[1].strip() |
__virtualname__ = 'wg' | |||||
def __virtual__(): | |||||
if 'wg.show' in __salt__: | |||||
return __virtualname__ | |||||
return False | |||||
def present(name, listen_port=None, fwmark=None, private_key=None): | |||||
""" | |||||
Make sure a wireguard interface exists. | |||||
""" | |||||
ret = dict(name=name, changes=dict(), result=False, comment=None) | |||||
show = __salt__['wg.show'](name, hide_keys=False) | |||||
if not show: | |||||
__salt__['wg.create'](name) | |||||
ret['changes'][name] = 'Interface created.' | |||||
show = __salt__['wg.show'](name, hide_keys=False) | |||||
if listen_port and int(show.get('listening port', 0)) != int(listen_port): | |||||
__salt__['wg.set'](name, listen_port=listen_port) | |||||
ret['changes']['listening port'] = dict( | |||||
old=show.get('listening port', 0), | |||||
new=listen_port, | |||||
) | |||||
if show.get('fwmark', None) != fwmark: | |||||
__salt__['wg.set'](name, fwmark=fwmark) | |||||
ret['changes']['fwmark'] = dict( | |||||
old=show.get('fwmark', None), | |||||
new=fwmark, | |||||
) | |||||
if show.get('private key') != private_key: | |||||
__salt__['wg.set'](name, private_key=private_key) | |||||
ret['changes']['private key'] = 'private key changed.' | |||||
ret['result'] = True | |||||
return ret | |||||
def absent(name): | |||||
""" | |||||
Make sure a wireguard interface is absent. | |||||
""" | |||||
ret = dict(name=name, changes=dict(), result=False, comment=None) | |||||
interface = __salt__['wg.show'](name) | |||||
if not interface: | |||||
ret['comment'] = 'Interface %s already absent.' % (name,) | |||||
ret['result'] = True | |||||
return ret | |||||
__salt__['wg.delete'](name) | |||||
ret['changes'][name] = dict(old=name, new=None) | |||||
ret['result'] = True | |||||
return ret | |||||
def peer_present(name, interface, endpoint=None, persistent_keepalive=None, | |||||
allowed_ips=None, preshared_key=None): | |||||
ret = dict(name=name, changes=dict(), result=False, comment=None) | |||||
show = __salt__['wg.show'](interface, hide_keys=False) | |||||
if not show: | |||||
ret['comment'] = 'Interface %s does not exist.' % (interface) | |||||
return ret | |||||
show = __salt__['wg.show'](name=interface, peer=name, hide_keys=False) | |||||
if not show: | |||||
__salt__['wg.set'](interface, peer=name, endpoint=endpoint, | |||||
persistent_keepalive=persistent_keepalive, | |||||
allowed_ips=','.join(allowed_ips), preshared_key=preshared_key) | |||||
ret['changes'][name] = 'Peer created.' | |||||
ret['result'] = True | |||||
return ret | |||||
if endpoint and show.get('endpoint', '') != endpoint: | |||||
__salt__['wg.set'](interface, peer=name, endpoint=endpoint) | |||||
updated_show = __salt__['wg.show'](name=interface, peer=name) | |||||
if updated_show.get('endpoint') != show.get('endpoint'): | |||||
ret['changes']['endpoint'] = dict( | |||||
old=show.get('endpoint'), new=updated_show.get('endpoint')) | |||||
if persistent_keepalive and not show.get('persistent keepalive', '').startswith('every %s second' % (persistent_keepalive,)): | |||||
__salt__['wg.set'](interface, peer=name, | |||||
persistent_keepalive=persistent_keepalive) | |||||
ret['changes']['persistent keepalive'] = 'persistent keepalive changed.' | |||||
elif not persistent_keepalive and show.get('persistent keepalive'): | |||||
__salt__['wg.set'](interface, peer=name, persistent_keepalive=0) | |||||
ret['changes']['persistent keepalive'] = 'persistent keepalive removed.' | |||||
if sorted(show.get('allowed ips')) != sorted(allowed_ips): | |||||
__salt__['wg.set'](interface, peer=name, allowed_ips=','.join(allowed_ips)) | |||||
ret['changes']['allowed ips'] = dict(new=allowed_ips, old=show.get('allowed ips')) | |||||
if preshared_key and show.get('preshared key') != preshared_key: | |||||
__salt__['wg.set'](interface, peer=name, preshared_key=preshared_key) | |||||
ret['changes']['preshared key'] = 'preshared key changed.' | |||||
elif show.get('preshared key') and not preshared_key: | |||||
__salt__['wg.set'](interface, peer=name, preshared_key='') | |||||
ret['changes']['preshared key'] = 'preshared key deleted.' | |||||
ret['result'] = True | |||||
return ret | |||||
def peer_absent(name, interface): | |||||
ret = dict(name=name, changes=dict(), result=False, comment=None) | |||||
show = __salt__['wg.show'](interface) | |||||
if not show: | |||||
ret['comment'] = 'Interface %s does not exist.' % (interface) | |||||
return ret | |||||
show = __salt__['wg.show'](name=interface, peer=name) | |||||
if not show: | |||||
ret['comment'] = 'Peer %s already absent.' % (name) | |||||
ret['result'] = True | |||||
return ret | |||||
__salt__['wg.set'](interface, peer=name, remove=True) | |||||
ret['changes'][name] = dict(old=name, new=None) | |||||
ret['result'] = True | |||||
return ret |
wireguard: | wireguard: | ||||
interfaces: | |||||
wgtest: | |||||
listen_port: 51820 | |||||
# fwmark: 0x1 | |||||
private_key: secret | |||||
peers: | |||||
- peer: 1ymBfBty05PNhD/QJKUlu4aL2p4jKSWVVqVQWIQG6wM= | |||||
# the note: will not go into wireguard configuration | |||||
# it enables you to label peers | |||||
note: some_note | |||||
endpoint: '10.42.0.0:1338' | |||||
allowed_ips: | |||||
- 10.0.0.2/32 | |||||
- 'fdff::2/128' | |||||
persistent_keepalive: 25 | |||||
# preshared_key: secret | |||||
- peer: 2ymBfBty05PNhD/QJKUlu4aL2p4jKSWVVqVQWIQG6wM= | |||||
endpoint: '[2001:db8::1]:1339' | |||||
allowed_ips: | |||||
- 10.0.0.3/32 | |||||
- 'fdff::3/128' | |||||
# optionally, a list of interfaces can be specified for which forwarding will | |||||
# be set to 1 via sysctl.present | |||||
# ATTENTION: this option is experimental and I haven't made my mind up whether | |||||
# it'll stay. Please don't rely on this for now. | |||||
set_forward_interfaces: | |||||
- all | |||||
- wgtest | |||||
wg0: | |||||
config: | | |||||
[Interface] | |||||
Address = fe80::1/64 | |||||
ListenPort = 51820 | |||||
PrivateKey = private | |||||
Table = off | |||||
[Peer] | |||||
PublicKey = peer | |||||
AllowedIPs = fe80::2 |
{% from "wireguard/map.jinja" import wireguard with context %} | |||||
{%- for interface in salt['pillar.get']('wireguard', {}).keys() %} | |||||
wireguard_interface_{{interface}}: | |||||
file.managed: | |||||
- name: /etc/wireguard/{{interface}}.conf | |||||
- contents_pillar: wireguard:{{interface}}:config | |||||
- mode: 640 | |||||
{% for interface, values in salt['pillar.get']('wireguard:interfaces', {}).items() %} | |||||
wireguard_{{ interface }}: | |||||
wg.present: | |||||
- name: {{ interface }} | |||||
{% for k, v in values.items() %} | |||||
{% if k in ['listen_port', 'fwmark', 'private_key'] %} | |||||
- {{k}}: {{v}} | |||||
{% if salt['pillar.get']('wireguard:' ~ interface ~ ':enable', True) %} | |||||
restart wg-quick@{{interface}}: | |||||
service.running: | |||||
- name: wg-quick@{{interface}} | |||||
- enable: True | |||||
- watch: | |||||
- file: wireguard_interface_{{interface}} | |||||
{% else %} | |||||
stop and disable wg-quick@{{interface}}: | |||||
service.dead: | |||||
- name: wg-quick@{{interface}} | |||||
- enable: False | |||||
{% endif %} | {% endif %} | ||||
{% endfor %} {# values.items() #} | |||||
{% for peer in values.get('peers', {}) %} | |||||
wireguard_{{ interface }}_peer_{{ peer.get('peer') }}: | |||||
wg.peer_present: | |||||
- interface: {{ interface }} | |||||
- name: {{ peer.get('peer') }} | |||||
{% if peer.get('endpoint') != None %} | |||||
- endpoint: '{{ peer.get('endpoint') }}' | |||||
{% endif %} | |||||
{% if peer.get('persistent_keepalive') != None %} | |||||
- persistent_keepalive: {{ peer.get('persistent_keepalive') }} | |||||
{% endif %} | |||||
{% if peer.get('allowed_ips') != None %} | |||||
- allowed_ips: | |||||
{% for subnet in peer.get('allowed_ips', []) %} | |||||
- {{subnet}} | |||||
{% endfor %} | |||||
{% if peer.get('preshared_key') != None %} | |||||
- preshared_key: {{ peer.get('preshared_key') }} | |||||
{% endif %} | |||||
{% endif %} | |||||
{% endfor %} | |||||
{% endfor %} | |||||
{% for interface in salt['pillar.get']('wireguard:set_forward_interfaces', []) %} | |||||
net.ipv4.conf.{{interface}}.forwarding: | |||||
sysctl.present: | |||||
- value: 1 | |||||
net.ipv6.conf.{{interface}}.forwarding: | |||||
sysctl.present: | |||||
- value: 1 | |||||
{% endfor %} | |||||
{%- endfor %} |