├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── example.py ├── setup.py └── wgnlpy ├── __init__.py ├── key.py ├── nlas ├── __init__.py ├── allowedip.py ├── device.py ├── key.py ├── peer.py ├── sockaddr.py └── timespec.py ├── preshared_key.py ├── private_key.py ├── public_key.py ├── sockaddr.py ├── sockaddr_in.py ├── sockaddr_in6.py ├── wireguard.py ├── wireguardinfo.py └── wireguardpeer.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Argosy Labs 4 | Copyright (c) 2019 Derrick Lyndon Pallas 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wgnlpy 2 | Python netlink connector to WireGuard 3 | ====== 4 | 5 | A simple control interface for [WireGuard](https://www.wireguard.com/) via 6 | Netlink, written in Python. 7 | 8 | ```python 9 | from wgnlpy import WireGuard 10 | 11 | interface = "wg0" 12 | peer = b'...' 13 | 14 | wg = WireGuard() 15 | 16 | wg.set_peer(interface, peer, 17 | endpoint="203.0.113.0:51820", 18 | allowedips=["2001:db8::/32"], 19 | ) 20 | assert peer in wg.get_interface(interface).peers 21 | 22 | wg.remove_peers(interface, peer) 23 | assert peer not in wg.get_interface(interface).peers 24 | ``` 25 | 26 | Requires 27 | * [cryptography](https://cryptography.io/), & 28 | * [pyroute2](https://pyroute2.org/). 29 | 30 | Also useful: the `sockaddr_in` and `sockaddr_in6` utility classes for 31 | sockaddr manipulation. 32 | 33 | License: [MIT](https://opensource.org/licenses/MIT) 34 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # SPDX-License-Identifier: Unlicense 3 | 4 | from wgnlpy import WireGuard, PrivateKey, PresharedKey 5 | from base64 import b64encode 6 | from pprint import pprint 7 | 8 | peer = PrivateKey.generate().public_key() 9 | print("PEER", repr(peer)) 10 | print(peer.lla4()) 11 | 12 | wg = WireGuard() 13 | interface = "wg-test" 14 | 15 | wg.set_interface(interface, private_key=PrivateKey.generate(), replace_peers=True) 16 | 17 | wg.set_peer(interface, peer, 18 | preshared_key=PresharedKey.generate(), 19 | endpoint="[::ffff:203.0.113.0%8]:12345", 20 | allowedips=["2001:db8::/32", "198.51.100.1"], 21 | ) 22 | peers = wg.get_interface(interface).peers 23 | assert peer in peers 24 | 25 | pprint(peers[peer]) 26 | 27 | wg.remove_peers(interface, peer) 28 | assert peer not in wg.get_interface(interface).peers 29 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from setuptools import setup 3 | 4 | from os import path 5 | this_directory = path.abspath(path.dirname(__file__)) 6 | with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f: 7 | long_description = f.read() 8 | 9 | setup( 10 | name = "wgnlpy", 11 | version = "0.1.5", 12 | description = ("Netlink connector to WireGuard"), 13 | url = "https://github.com/ArgosyLabs/wgnlpy", 14 | author = "Derrick Lyndon Pallas", 15 | author_email = "derrick@argosylabs.com", 16 | license = "MIT", 17 | packages = [ "wgnlpy", "wgnlpy/nlas" ], 18 | install_requires = [ "cryptography", "pyroute2" ], 19 | long_description = long_description, 20 | long_description_content_type = "text/markdown", 21 | keywords = "wireguard netlink sockaddr sockaddr_in sockaddr_in6", 22 | classifiers = [ 23 | "Development Status :: 4 - Beta", 24 | "Programming Language :: Python :: 3 :: Only", 25 | "Topic :: Software Development :: Libraries :: Python Modules", 26 | "Topic :: System :: Networking", 27 | "Topic :: Security", 28 | "License :: OSI Approved :: MIT License", 29 | ], 30 | include_package_data=True, 31 | ) 32 | # 33 | -------------------------------------------------------------------------------- /wgnlpy/__init__.py: -------------------------------------------------------------------------------- 1 | from .wireguard import WireGuard 2 | from .preshared_key import PresharedKey 3 | from .private_key import PrivateKey 4 | from .public_key import PublicKey 5 | -------------------------------------------------------------------------------- /wgnlpy/key.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # SPDX-License-Identifier: MIT 3 | 4 | from base64 import b64encode, b64decode 5 | 6 | class Key: 7 | __slots__ = ('__value') 8 | 9 | def __init__(self, key=bytes(32)): 10 | if isinstance(key, Key): 11 | self.__value = key.__value 12 | elif isinstance(key, bytes): 13 | self.__value = key 14 | elif isinstance(key, bytearray): 15 | self.__value = bytes(key) 16 | elif isinstance(key, str): 17 | self.__value = b64decode(key) 18 | else: 19 | raise TypeError() 20 | 21 | assert isinstance(self.__value, bytes) 22 | assert 32 == len(self.__value) 23 | 24 | def __str__(self): 25 | return b64encode(self.__value).decode('utf-8') 26 | 27 | def __bytes__(self): 28 | return self.__value 29 | 30 | def __repr__(self): 31 | return f'{type(self).__name__}({repr(str(self))})' 32 | 33 | def __bool__(self): 34 | return self.__value != bytes(32) 35 | 36 | def __eq__(self, other): 37 | if isinstance(other, Key): 38 | return self.__value == other.__value 39 | elif isinstance(other, (bytes, bytearray)): 40 | return self.__value == other 41 | elif isinstance(other, str): 42 | return self.__value == b64decode(other) 43 | else: 44 | return NotImplemented 45 | 46 | def __hash__(self): 47 | return hash(self.__value) 48 | 49 | # 50 | -------------------------------------------------------------------------------- /wgnlpy/nlas/__init__.py: -------------------------------------------------------------------------------- 1 | from .allowedip import allowedip 2 | from .key import key 3 | from .sockaddr import sockaddr 4 | from .timespec import timespec 5 | 6 | from .peer import peer 7 | 8 | from .device import device 9 | -------------------------------------------------------------------------------- /wgnlpy/nlas/allowedip.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # SPDX-License-Identifier: MIT 3 | 4 | from socket import AF_INET, AF_INET6 5 | from ipaddress import IPv4Network, IPv6Network, ip_network 6 | from pyroute2.netlink import nla, NLA_F_NESTED 7 | 8 | class allowedip(nla): 9 | nla_flags = NLA_F_NESTED 10 | nla_map = ( 11 | ('WGALLOWEDIP_A_UNSPEC', 'none'), 12 | ('WGALLOWEDIP_A_FAMILY', 'uint16'), 13 | ('WGALLOWEDIP_A_IPADDR', 'cdata'), 14 | ('WGALLOWEDIP_A_CIDR_MASK', 'uint8'), 15 | ) 16 | 17 | def network(self): 18 | family, ipaddr, cidr_mask = ( 19 | self.get_attr('WGALLOWEDIP_A_FAMILY'), 20 | self.get_attr('WGALLOWEDIP_A_IPADDR'), 21 | self.get_attr('WGALLOWEDIP_A_CIDR_MASK'), 22 | ) 23 | 24 | try: 25 | return { 26 | AF_INET: IPv4Network, 27 | AF_INET6: IPv6Network, 28 | }[family]((ipaddr, cidr_mask,)) 29 | except: 30 | raise NotImplementedError 31 | 32 | @staticmethod 33 | def frob(nitz): 34 | self = allowedip() 35 | 36 | if not isinstance(nitz, (IPv4Network, IPv6Network)): 37 | nitz = ip_network(nitz) 38 | 39 | if isinstance(nitz, IPv4Network): 40 | self['attrs'].append(('WGALLOWEDIP_A_FAMILY', AF_INET.value)) 41 | elif isinstance(nitz, IPv6Network): 42 | self['attrs'].append(('WGALLOWEDIP_A_FAMILY', AF_INET6.value)) 43 | else: 44 | raise NotImplementedError 45 | 46 | self['attrs'].append(('WGALLOWEDIP_A_IPADDR', nitz.network_address.packed)) 47 | self['attrs'].append(('WGALLOWEDIP_A_CIDR_MASK', nitz.prefixlen)) 48 | 49 | return self 50 | 51 | # 52 | -------------------------------------------------------------------------------- /wgnlpy/nlas/device.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # SPDX-License-Identifier: MIT 3 | 4 | from enum import Enum 5 | from pyroute2.netlink import genlmsg 6 | 7 | class device(genlmsg): 8 | VERSION = 1 9 | 10 | class type(Enum): 11 | GET_DEVICE = 0 12 | SET_DEVICE = 1 13 | 14 | def __interface(self, interface): 15 | if isinstance(interface, str): 16 | self['attrs'].append(('WGDEVICE_A_IFNAME', interface)) 17 | elif isinstance(interface, int): 18 | self['attrs'].append(('WGDEVICE_A_IFINDEX', interface)) 19 | else: 20 | raise TypeError("interface must be int or str") 21 | 22 | @staticmethod 23 | def get_device(interface): 24 | self = device() 25 | self['cmd'] = self.type.GET_DEVICE.value 26 | self['version'] = self.VERSION 27 | self.__interface(interface) 28 | return self 29 | 30 | @staticmethod 31 | def set_device(interface): 32 | self = device() 33 | self['cmd'] = self.type.SET_DEVICE.value 34 | self['version'] = self.VERSION 35 | self.__interface(interface) 36 | return self 37 | 38 | from . import key, peer 39 | 40 | nla_map = ( 41 | ('WGDEVICE_A_UNSPEC', 'none'), 42 | ('WGDEVICE_A_IFINDEX', 'uint32'), 43 | ('WGDEVICE_A_IFNAME', 'asciiz'), 44 | ('WGDEVICE_A_PRIVATE_KEY', 'key'), 45 | ('WGDEVICE_A_PUBLIC_KEY', 'key'), 46 | ('WGDEVICE_A_FLAGS', 'uint32'), 47 | ('WGDEVICE_A_LISTEN_PORT', 'uint16'), 48 | ('WGDEVICE_A_FWMARK', 'uint32'), 49 | ('WGDEVICE_A_PEERS', '*peer'), 50 | ) 51 | 52 | class flag(Enum): 53 | REPLACE_PEERS = 1 << 0 54 | 55 | # 56 | -------------------------------------------------------------------------------- /wgnlpy/nlas/key.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # SPDX-License-Identifier: MIT 3 | 4 | from pyroute2.netlink import nla_base 5 | 6 | class key(nla_base): 7 | fields = ( 8 | ('value', '32s'), 9 | ) 10 | 11 | def encode(self): 12 | assert isinstance(self.value, (bytes, bytearray)) 13 | assert 32 == len(self.value) 14 | self['value'] = self.value 15 | nla_base.encode(self) 16 | 17 | def decode(self): 18 | nla_base.decode(self) 19 | self.value = self['value'] if self['value'] != bytes(32) else None 20 | 21 | # 22 | -------------------------------------------------------------------------------- /wgnlpy/nlas/peer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # SPDX-License-Identifier: MIT 3 | 4 | from enum import Enum 5 | from pyroute2.netlink import nla, NLA_F_NESTED 6 | 7 | class peer(nla): 8 | from . import key, sockaddr, timespec, allowedip 9 | 10 | nla_flags = NLA_F_NESTED 11 | nla_map = ( 12 | ('WGPEER_A_UNSPEC', 'none'), 13 | ('WGPEER_A_PUBLIC_KEY', 'key'), 14 | ('WGPEER_A_PRESHARED_KEY', 'key'), 15 | ('WGPEER_A_FLAGS', 'uint32'), 16 | ('WGPEER_A_ENDPOINT', 'sockaddr'), 17 | ('WGPEER_A_PERSISTENT_KEEPALIVE_INTERVAL', 'uint16'), 18 | ('WGPEER_A_LAST_HANDSHAKE_TIME', 'timespec'), 19 | ('WGPEER_A_RX_BYTES', 'uint64'), 20 | ('WGPEER_A_TX_BYTES', 'uint64'), 21 | ('WGPEER_A_ALLOWEDIPS', '*allowedip'), 22 | ('WGPEER_A_PROTOCOL_VERSION', 'uint32'), 23 | ) 24 | 25 | class flag(Enum): 26 | REMOVE_ME = 1 << 0 27 | REPLACE_ALLOWEDIPS = 1 << 1 28 | UPDATE_ONLY = 1 << 2 29 | 30 | # 31 | -------------------------------------------------------------------------------- /wgnlpy/nlas/sockaddr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # SPDX-License-Identifier: MIT 3 | 4 | from struct import unpack_from 5 | from socket import AF_INET, AF_INET6, getaddrinfo 6 | from urllib.parse import urlparse 7 | from pyroute2.netlink import nla_base 8 | from ..sockaddr import sockaddr as to_sa 9 | from ..sockaddr_in import sockaddr_in 10 | from ..sockaddr_in6 import sockaddr_in6 11 | 12 | class sockaddr(nla_base): 13 | fields = ( 14 | ('value', 's'), 15 | ) 16 | 17 | def encode(self): 18 | assert isinstance(self.value, (sockaddr_in, sockaddr_in6, )) 19 | self['value'] = bytes(self.value) 20 | nla_base.encode(self) 21 | 22 | def decode(self): 23 | nla_base.decode(self) 24 | self["value"] = self.data[self.offset+4:self.offset+self.length] 25 | family, = unpack_from("H", self["value"]) 26 | 27 | try: 28 | type = { 29 | AF_INET: sockaddr_in, 30 | AF_INET6: sockaddr_in6, 31 | }[family] 32 | except: 33 | raise NotImplementedError 34 | 35 | if isinstance(self['value'], bytearray): 36 | self.value = type.from_buffer(self['value']) 37 | elif isinstance(self['value'], bytes): 38 | self.value = type.from_buffer_copy(self['value']) 39 | 40 | @staticmethod 41 | def frob(nitz): 42 | DEFAULT_PORT = 51820 43 | if isinstance(nitz, (sockaddr_in, sockaddr_in6)): 44 | return nitz 45 | elif isinstance(nitz, (list, tuple)): 46 | fields = ('addr', 'port', 'flowinfo', 'scope_id', '') 47 | kwargs = { 'port': DEFAULT_PORT } 48 | kwargs.update(dict(zip(fields, nitz))) 49 | return to_sa(**kwargs) 50 | elif isinstance(nitz, dict): 51 | if 'port' in nitz: 52 | return to_sa(**nitz) 53 | else: 54 | return to_sa(port=DEFAULT_PORT, **nitz) 55 | elif isinstance(nitz, str): 56 | url = urlparse("//" + nitz) 57 | family, *meh, sockaddr = getaddrinfo(url.hostname, url.port or DEFAULT_PORT)[0] 58 | 59 | try: 60 | type, *fields = { 61 | AF_INET: (sockaddr_in, 'addr', 'port', ''), 62 | AF_INET6: (sockaddr_in6, 'addr', 'port', 'flowinfo', 'scope_id', ''), 63 | }[family] 64 | except: 65 | raise NotImplementedError 66 | 67 | return type(**dict(zip(fields, sockaddr))) 68 | else: 69 | return to_sa(addr=nitz, port=DEFAULT_PORT) 70 | 71 | # 72 | -------------------------------------------------------------------------------- /wgnlpy/nlas/timespec.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # SPDX-License-Identifier: MIT 3 | 4 | from math import modf 5 | from pyroute2.netlink import nla_base 6 | 7 | class timespec(nla_base): 8 | fields = ( 9 | ('tv_sec', 'q'), 10 | ('tv_nsec', 'l'), 11 | ) 12 | 13 | def encode(self): 14 | tv_sec, tv_nsec = modf(self.value) 15 | self['tv_sec'] = int(tv_sec) 16 | self['tv_nsec'] = int(tv_nsec * 1e9) 17 | nla_base.encode(self) 18 | 19 | def decode(self): 20 | nla_base.decode(self) 21 | tv_sec = int(self['tv_sec']) 22 | tv_nsec = int(self['tv_nsec']) / 1e9 23 | self.value = tv_sec + tv_nsec 24 | 25 | # 26 | -------------------------------------------------------------------------------- /wgnlpy/preshared_key.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # SPDX-License-Identifier: MIT 3 | 4 | from .key import Key 5 | 6 | from secrets import token_bytes 7 | 8 | class PresharedKey(Key): 9 | def __init__(self, key=None): 10 | if isinstance(key, type(None)): 11 | super().__init__() 12 | elif isinstance(key, (type(None), PresharedKey, bytes, bytearray, str)): 13 | super().__init__(key) 14 | else: 15 | raise TypeError("key must be PresharedKey, bytes, bytearray, or str") 16 | 17 | def __eq__(self, other): 18 | if isinstance(other, Key) and not isinstance(other, PresharedKey): 19 | return NotImplemented 20 | 21 | return super().__eq__(other) 22 | 23 | def __hash__(self): 24 | return super().__hash__() 25 | 26 | @staticmethod 27 | def generate(): 28 | return PresharedKey(token_bytes(32)) 29 | 30 | # 31 | -------------------------------------------------------------------------------- /wgnlpy/private_key.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # SPDX-License-Identifier: MIT 3 | 4 | from .key import Key 5 | from .public_key import PublicKey 6 | 7 | from base64 import b64decode 8 | from cryptography.hazmat.primitives import serialization 9 | from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey 10 | 11 | class PrivateKey(Key): 12 | def __init__(self, key=None): 13 | if key is None: 14 | super().__init__() 15 | elif isinstance(key, PrivateKey): 16 | super().__init__(key) 17 | elif isinstance(key, X25519PrivateKey): 18 | super().__init__(key.private_bytes( 19 | encoding=serialization.Encoding.Raw, 20 | format=serialization.PrivateFormat.Raw, 21 | encryption_algorithm=serialization.NoEncryption(), 22 | )) 23 | elif isinstance(key, (bytes, bytearray)): 24 | super().__init__(X25519PrivateKey.from_private_bytes(key).private_bytes( 25 | encoding=serialization.Encoding.Raw, 26 | format=serialization.PrivateFormat.Raw, 27 | encryption_algorithm=serialization.NoEncryption(), 28 | )) 29 | elif isinstance(key, str): 30 | super().__init__(X25519PrivateKey.from_private_bytes(b64decode(key)).private_bytes( 31 | encoding=serialization.Encoding.Raw, 32 | format=serialization.PrivateFormat.Raw, 33 | encryption_algorithm=serialization.NoEncryption(), 34 | )) 35 | else: 36 | raise TypeError("key must be PrivateKey, bytes, bytearray, or str") 37 | 38 | def __eq__(self, other): 39 | if isinstance(other, Key) and not isinstance(other, PrivateKey): 40 | return NotImplemented 41 | 42 | return super().__eq__(other) 43 | 44 | def __hash__(self): 45 | return super().__hash__() 46 | 47 | def public_key(self): 48 | return PublicKey(X25519PrivateKey.from_private_bytes(bytes(self)).public_key().public_bytes( 49 | encoding=serialization.Encoding.Raw, 50 | format=serialization.PublicFormat.Raw, 51 | )) 52 | 53 | if isinstance(other, Key) and not isinstance(other, PrivateKey): 54 | return NotImplemented 55 | 56 | return super().__eq__(other) 57 | 58 | @staticmethod 59 | def generate(): 60 | return PrivateKey(X25519PrivateKey.generate().private_bytes( 61 | encoding=serialization.Encoding.Raw, 62 | format=serialization.PrivateFormat.Raw, 63 | encryption_algorithm=serialization.NoEncryption(), 64 | )) 65 | 66 | # 67 | -------------------------------------------------------------------------------- /wgnlpy/public_key.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # SPDX-License-Identifier: MIT 3 | 4 | from .key import Key 5 | 6 | from base64 import b64decode 7 | from hashlib import shake_128, blake2s 8 | from ipaddress import ip_network, IPv4Network, IPv6Network 9 | from cryptography.hazmat.primitives import serialization 10 | from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey 11 | 12 | class PublicKey(Key): 13 | def __init__(self, key=None): 14 | if key is None: 15 | super().__init__() 16 | elif isinstance(key, PublicKey): 17 | super().__init__(key) 18 | elif isinstance(key, X25519PublicKey): 19 | super().__init__(key.public_bytes( 20 | encoding=serialization.Encoding.Raw, 21 | format=serialization.PublicFormat.Raw, 22 | )) 23 | elif isinstance(key, (bytes, bytearray)): 24 | super().__init__(X25519PublicKey.from_public_bytes(key).public_bytes( 25 | encoding=serialization.Encoding.Raw, 26 | format=serialization.PublicFormat.Raw, 27 | )) 28 | elif isinstance(key, str): 29 | super().__init__(X25519PublicKey.from_public_bytes(b64decode(key)).public_bytes( 30 | encoding=serialization.Encoding.Raw, 31 | format=serialization.PublicFormat.Raw, 32 | )) 33 | else: 34 | raise TypeError("key must be PublicKey, bytes, bytearray, or str") 35 | 36 | def __eq__(self, other): 37 | if isinstance(other, Key) and not isinstance(other, PublicKey): 38 | return NotImplemented 39 | 40 | return super().__eq__(other) 41 | 42 | def __hash__(self): 43 | return super().__hash__() 44 | 45 | def orchid(self, secret=b'', network=None): 46 | if network is None: 47 | return self.orchid6(secret) 48 | 49 | if isinstance(secret, str): 50 | secret = secret.encode('utf-8') 51 | elif not isinstance(secret, (bytes, bytearray)): 52 | secret = bytes(secret) 53 | 54 | if not isinstance(network, (IPv4Network, IPv6Network, )): 55 | network = ip_network(network) 56 | 57 | hash = shake_128(secret + bytes(self)).digest(network.max_prefixlen//8) 58 | mask = int.from_bytes(network.hostmask.packed, byteorder='big') 59 | host = int.from_bytes(hash, byteorder='big') 60 | addr = network[host & mask] 61 | 62 | if addr == network.network_address: 63 | addr += 1 64 | elif addr == network.broadcast_address: 65 | addr -= 1 66 | 67 | assert addr != network.network_address, "Generated network address" 68 | assert addr != network.broadcast_address, "Generated broadcast address" 69 | assert addr in network, "Generated out-of-network address" 70 | return addr 71 | 72 | def orchid4(self, secret=b''): 73 | return self.orchid(secret, IPv4Network("100.64.0.0/10")) 74 | 75 | def orchid6(self, secret=b''): 76 | return self.orchid(secret, IPv6Network("2001:20::/28")) 77 | 78 | 79 | def lla(self, secret=b'', network=None): 80 | if network is None: 81 | return self.lla6(secret) 82 | 83 | if isinstance(secret, str): 84 | secret = secret.encode('utf-8') 85 | elif not isinstance(secret, (bytes, bytearray)): 86 | secret = bytes(secret) 87 | 88 | if not isinstance(network, (IPv4Network, IPv6Network, )): 89 | network = ip_network(network) 90 | 91 | hash = blake2s(bytes(self), 92 | digest_size=32, 93 | key=secret, 94 | ).digest()[:network.max_prefixlen//8] 95 | mask = int.from_bytes(network.hostmask.packed, byteorder='big') 96 | host = int.from_bytes(hash, byteorder='big') 97 | addr = network[host & mask] 98 | 99 | if addr == network.network_address: 100 | addr += 1 101 | elif addr == network.broadcast_address: 102 | addr -= 1 103 | 104 | assert addr != network.network_address, "Generated network address" 105 | assert addr != network.broadcast_address, "Generated broadcast address" 106 | assert addr in network, "Generated out-of-network address" 107 | return addr 108 | 109 | def lla4(self, secret=b''): 110 | return self.lla(secret, IPv4Network("169.254.0.0/16")) 111 | 112 | def lla6(self, secret=b''): 113 | return self.lla(secret, IPv6Network("fe80::/10")) 114 | 115 | # 116 | -------------------------------------------------------------------------------- /wgnlpy/sockaddr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # SPDX-License-Identifier: MIT 3 | 4 | from .sockaddr_in import sockaddr_in 5 | from .sockaddr_in6 import sockaddr_in6 6 | 7 | from ipaddress import ip_address, IPv4Address, IPv6Address 8 | 9 | def sockaddr(addr, **kwargs): 10 | if not isinstance(addr, (IPv4Address, IPv6Address)): 11 | addr = ip_address(addr) 12 | 13 | if isinstance(addr, IPv4Address): 14 | return sockaddr_in(addr=addr, **kwargs) 15 | elif isinstance(addr, IPv6Address): 16 | return sockaddr_in6(addr=addr, **kwargs) 17 | else: 18 | raise NotImplementedError 19 | 20 | # 21 | -------------------------------------------------------------------------------- /wgnlpy/sockaddr_in.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # SPDX-License-Identifier: MIT 3 | 4 | import ctypes 5 | from socket import AF_INET 6 | from ipaddress import IPv4Address 7 | 8 | class sockaddr_in(ctypes.Structure): 9 | _fields_ = ( 10 | ('_family', ctypes.c_uint16), 11 | ('port', ctypes.c_uint16.__ctype_be__), 12 | ('_addr', ctypes.c_ubyte * 4), 13 | ('_zero', ctypes.c_byte * 8), 14 | ) 15 | 16 | def __init__(self, **kwargs): 17 | super().__init__(_family=self.family) 18 | for key, value in kwargs.items(): 19 | if not hasattr(self, key): 20 | raise AttributeError 21 | setattr(self, key, value) 22 | 23 | def __str__(self): 24 | return f'{self.addr}:{self.port}' 25 | 26 | def __repr__(self): 27 | return repr({ 28 | 'family': self.family, 29 | 'port': self.port, 30 | 'addr': self.addr, 31 | }) 32 | 33 | @property 34 | def family(self): 35 | return AF_INET 36 | 37 | @property 38 | def addr(self): 39 | return IPv4Address(bytes(self._addr)) 40 | 41 | @addr.setter 42 | def addr(self, value): 43 | if not isinstance(value, IPv4Address): 44 | value = IPv4Address(value) 45 | 46 | self._addr = type(self._addr).from_buffer_copy(value.packed) 47 | 48 | # 49 | -------------------------------------------------------------------------------- /wgnlpy/sockaddr_in6.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # SPDX-License-Identifier: MIT 3 | 4 | import ctypes 5 | from socket import AF_INET6 6 | from ipaddress import IPv6Address 7 | 8 | class sockaddr_in6(ctypes.Structure): 9 | _fields_ = ( 10 | ('_family', ctypes.c_uint16), 11 | ('port', ctypes.c_uint16.__ctype_be__), 12 | ('flowinfo', ctypes.c_uint32.__ctype_be__), 13 | ('_addr', ctypes.c_ubyte * 16), 14 | ('scope_id', ctypes.c_uint32), 15 | ) 16 | 17 | def __init__(self, **kwargs): 18 | super().__init__(_family=self.family) 19 | for key, value in kwargs.items(): 20 | if not hasattr(self, key): 21 | raise AttributeError 22 | setattr(self, key, value) 23 | 24 | def __str__(self): 25 | if self.scope_id > 0: 26 | return f'[{self.addr}%{self.scope_id}]:{self.port}' 27 | else: 28 | return f'[{self.addr}]:{self.port}' 29 | 30 | def __repr__(self): 31 | return repr({ 32 | 'family': self.family, 33 | 'port': self.port, 34 | 'flowinfo': self.flowinfo, 35 | 'addr': self.addr, 36 | 'scope_id': self.scope_id, 37 | }) 38 | 39 | @property 40 | def family(self): 41 | return AF_INET6 42 | 43 | @property 44 | def addr(self): 45 | return IPv6Address(bytes(self._addr)) 46 | 47 | @addr.setter 48 | def addr(self, value): 49 | if not isinstance(value, IPv6Address): 50 | value = IPv6Address(value) 51 | 52 | self._addr = type(self._addr).from_buffer_copy(value.packed) 53 | 54 | # 55 | -------------------------------------------------------------------------------- /wgnlpy/wireguard.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # SPDX-License-Identifier: MIT 3 | 4 | from pyroute2 import netlink 5 | 6 | from .preshared_key import PresharedKey 7 | from .private_key import PrivateKey 8 | from .public_key import PublicKey 9 | from .wireguardinfo import WireGuardInfo 10 | 11 | class WireGuard(object): 12 | __slots__ = ( "__socket" ) 13 | 14 | from .nlas import device as __device 15 | 16 | def __init__(self, **kwargs): 17 | self.__socket = netlink.generic.GenericNetlinkSocket() 18 | self.__socket.bind('wireguard', self.__device) 19 | 20 | def __del__(self): 21 | self.__socket.close() 22 | 23 | def __get(self, device): 24 | flags = netlink.NLM_F_REQUEST | netlink.NLM_F_DUMP 25 | return self.__socket.nlm_request(device, msg_type=self.__socket.prid, msg_flags=flags) 26 | 27 | def __set(self, device): 28 | flags = netlink.NLM_F_ACK | netlink.NLM_F_REQUEST 29 | return self.__socket.nlm_request(device, msg_type=self.__socket.prid, msg_flags=flags) 30 | 31 | def get_interface(self, interface, spill_private_key=False, spill_preshared_keys=False): 32 | device = self.__device.get_device(interface) 33 | messages = self.__get(device) 34 | 35 | return WireGuardInfo(messages, spill_private_key, spill_preshared_keys) 36 | 37 | def set_interface(self, interface, 38 | private_key=None, 39 | listen_port=None, 40 | fwmark=None, 41 | replace_peers=False, 42 | ): 43 | 44 | device = self.__device.set_device(interface) 45 | 46 | if replace_peers: 47 | device['attrs'].append(('WGDEVICE_A_FLAGS', device.flag.REPLACE_PEERS.value)) 48 | 49 | if private_key is not None: 50 | if isinstance(private_key, PrivateKey): 51 | private_key = bytes(private_key) 52 | elif not isinstance(private_key, (bytes, bytearray)): 53 | private_key = bytes(PrivateKey(private_key)) 54 | device['attrs'].append(('WGDEVICE_A_PRIVATE_KEY', private_key)) 55 | 56 | if listen_port is not None: 57 | device['attrs'].append(('WGDEVICE_A_LISTEN_PORT', listen_port)) 58 | 59 | if fwmark is not None: 60 | device['attrs'].append(('WGDEVICE_A_FWMARK', fwmark)) 61 | 62 | return self.__set(device) 63 | 64 | def remove_peers(self, interface, *public_keys): 65 | device = self.__device.set_device(interface) 66 | device['attrs'].append(('WGDEVICE_A_PEERS', [])) 67 | 68 | for public_key in public_keys: 69 | peer = self.__device.peer() 70 | if isinstance(public_key, PublicKey): 71 | public_key = bytes(public_key) 72 | elif not isinstance(public_key, (bytes, bytearray)): 73 | public_key = bytes(PublicKey(public_key)) 74 | peer['attrs'].append(('WGPEER_A_PUBLIC_KEY', public_key)) 75 | peer['attrs'].append(('WGPEER_A_FLAGS', peer.flag.REMOVE_ME.value)) 76 | device.get_attr('WGDEVICE_A_PEERS').append({'attrs': peer['attrs']}) 77 | 78 | return self.__set(device) 79 | 80 | def set_peer(self, interface, public_key, 81 | preshared_key=None, 82 | endpoint=None, 83 | persistent_keepalive_interval=None, 84 | allowedips=None, 85 | replace_allowedips=None, 86 | update_only=False, 87 | ): 88 | 89 | device = self.__device.set_device(interface) 90 | 91 | peer = device.peer() 92 | if isinstance(public_key, PublicKey): 93 | public_key = bytes(public_key) 94 | elif not isinstance(public_key, (bytes, bytearray)): 95 | public_key = bytes(PublicKey(public_key)) 96 | peer['attrs'].append(('WGPEER_A_PUBLIC_KEY', public_key)) 97 | 98 | if replace_allowedips is None and allowedips is not None: 99 | replace_allowedips = True 100 | 101 | flags = 0 102 | if replace_allowedips: 103 | flags |= peer.flag.REPLACE_ALLOWEDIPS.value 104 | if update_only: 105 | flags |= peer.flag.UPDATE_ONLY.value 106 | if flags: 107 | peer['attrs'].append(('WGPEER_A_FLAGS', flags)) 108 | 109 | if preshared_key is not None: 110 | if isinstance(preshared_key, PresharedKey): 111 | preshared_key = bytes(preshared_key) 112 | elif not isinstance(preshared_key, (bytes, bytearray)): 113 | preshared_key = bytes(PresharedKey(preshared_key)) 114 | peer['attrs'].append(('WGPEER_A_PRESHARED_KEY', preshared_key)) 115 | 116 | if endpoint is not None: 117 | peer['attrs'].append(('WGPEER_A_ENDPOINT', self.__device.peer.sockaddr.frob(endpoint))) 118 | 119 | if persistent_keepalive_interval is not None: 120 | peer['attrs'].append(('WGPEER_A_PERSISTENT_KEEPALIVE_INTERVAL', persistent_keepalive_interval)) 121 | 122 | if allowedips is not None: 123 | peer['attrs'].append(('WGPEER_A_ALLOWEDIPS', [])) 124 | 125 | for allowedip in allowedips: 126 | peer.get_attr('WGPEER_A_ALLOWEDIPS').append({'attrs': self.__device.peer.allowedip.frob(allowedip)['attrs']}) 127 | 128 | device['attrs'].append(('WGDEVICE_A_PEERS', [{'attrs': peer['attrs']}])) 129 | return self.__set(device) 130 | 131 | def replace_allowedips(self, interface, *public_keys): 132 | device = self.__device.set_device(interface) 133 | device['attrs'].append(('WGDEVICE_A_PEERS', [])) 134 | 135 | for public_key in public_keys: 136 | peer = self.__device.peer() 137 | if isinstance(public_key, PublicKey): 138 | public_key = bytes(public_key) 139 | elif not isinstance(public_key, (bytes, bytearray)): 140 | public_key = bytes(PublicKey(public_key)) 141 | peer['attrs'].append(('WGPEER_A_PUBLIC_KEY', public_key)) 142 | peer['attrs'].append(('WGPEER_A_FLAGS', peer.flag.REPLACE_ALLOWEDIPS.value)) 143 | device.get_attr('WGDEVICE_A_PEERS').append({'attrs': peer['attrs']}) 144 | 145 | return self.__set(device) 146 | 147 | # 148 | -------------------------------------------------------------------------------- /wgnlpy/wireguardinfo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # SPDX-License-Identifier: MIT 3 | 4 | from .private_key import PrivateKey 5 | from .public_key import PublicKey 6 | from .wireguardpeer import WireGuardPeer 7 | 8 | class WireGuardInfo(object): 9 | __slots__ = ( 10 | "ifindex", 11 | "ifname", 12 | "private_key", 13 | "public_key", 14 | "listen_port", 15 | "fwmark", 16 | "peers", 17 | ) 18 | 19 | def __init__(self, messages, spill_private_key, spill_preshared_keys): 20 | self.ifindex = messages[0].get_attr('WGDEVICE_A_IFINDEX') 21 | self.ifname = messages[0].get_attr('WGDEVICE_A_IFNAME') 22 | self.private_key = messages[0].get_attr('WGDEVICE_A_PRIVATE_KEY') 23 | if not spill_private_key: 24 | self.private_key = self.private_key is not None 25 | elif self.private_key is not None: 26 | self.private_key = PrivateKey(self.private_key) 27 | self.public_key = messages[0].get_attr('WGDEVICE_A_PUBLIC_KEY') 28 | if self.public_key is not None: 29 | self.public_key = PublicKey(self.public_key) 30 | self.listen_port = messages[0].get_attr('WGDEVICE_A_LISTEN_PORT') 31 | self.fwmark = messages[0].get_attr('WGDEVICE_A_FWMARK') 32 | 33 | self.peers = { } 34 | 35 | for message in messages: 36 | wgp = lambda p: WireGuardPeer(p, spill_preshared_keys) 37 | for peer in map(wgp, message.get_attr('WGDEVICE_A_PEERS') or []): 38 | assert peer.public_key not in self.peers 39 | self.peers[peer.public_key] = peer 40 | 41 | def __repr__(self): 42 | return repr({ 43 | 'ifindex': self.ifindex, 44 | 'ifname': self.ifname, 45 | 'private_key': self.private_key, 46 | 'public_key': self.public_key, 47 | 'listen_port': self.listen_port, 48 | 'fwmark': self.fwmark, 49 | 'peers': self.peers, 50 | }) 51 | 52 | # 53 | -------------------------------------------------------------------------------- /wgnlpy/wireguardpeer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # SPDX-License-Identifier: MIT 3 | 4 | from .preshared_key import PresharedKey 5 | from .public_key import PublicKey 6 | 7 | class WireGuardPeer(object): 8 | __slots__ = ( 9 | "public_key", 10 | "preshared_key", 11 | "endpoint", 12 | "persistent_keepalive_interval", 13 | "last_handshake_time", 14 | "rx_bytes", 15 | "tx_bytes", 16 | "allowedips", 17 | "protocol_version", 18 | ) 19 | 20 | def __init__(self, peer, spill_preshared_keys=False): 21 | self.public_key = PublicKey(peer.get_attr('WGPEER_A_PUBLIC_KEY')) 22 | self.preshared_key = peer.get_attr('WGPEER_A_PRESHARED_KEY') 23 | if not spill_preshared_keys: 24 | self.preshared_key = self.preshared_key is not None 25 | else: 26 | self.preshared_key = PresharedKey(self.preshared_key) 27 | self.endpoint = peer.get_attr('WGPEER_A_ENDPOINT') 28 | self.persistent_keepalive_interval = peer.get_attr('WGPEER_A_PERSISTENT_KEEPALIVE_INTERVAL') 29 | self.last_handshake_time = peer.get_attr('WGPEER_A_LAST_HANDSHAKE_TIME') 30 | if not self.last_handshake_time > 0: 31 | self.last_handshake_time = None 32 | self.rx_bytes = peer.get_attr('WGPEER_A_RX_BYTES') 33 | self.tx_bytes = peer.get_attr('WGPEER_A_TX_BYTES') 34 | self.protocol_version = peer.get_attr('WGPEER_A_PROTOCOL_VERSION') 35 | 36 | self.allowedips = [] 37 | for allowedip in peer.get_attr('WGPEER_A_ALLOWEDIPS') or []: 38 | self.allowedips.append(allowedip.network()) 39 | 40 | def __repr__(self): 41 | return repr({ 42 | 'public_key': self.public_key, 43 | 'preshared_key': self.preshared_key, 44 | 'endpoint': self.endpoint, 45 | 'persistent_keepalive_interval': self.persistent_keepalive_interval, 46 | 'last_handshake_time': self.last_handshake_time, 47 | 'rx_bytes': self.rx_bytes, 48 | 'tx_bytes': self.tx_bytes, 49 | 'allowedips': self.allowedips, 50 | 'protocol_version': self.protocol_version, 51 | }) 52 | 53 | # 54 | --------------------------------------------------------------------------------