Simple POC for creating easy way to manage wireguard peer configs on the server.
This commit is contained in:
		
							
								
								
									
										87
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | |||||||
|  | # Wireguard Peer Manager | ||||||
|  |  | ||||||
|  | This is simple CURD for managing wireguard peer notations on a wireguard server config. | ||||||
|  |  | ||||||
|  | ## Requirements | ||||||
|  |  | ||||||
|  | - Python 3.x | ||||||
|  | - `requests` library (for the client) | ||||||
|  | - WireGuard (`wg-quick` and `wg` commands must be available on the server) | ||||||
|  |  | ||||||
|  | ## Server: wpm.py | ||||||
|  |  | ||||||
|  | ### How to Run the Server | ||||||
|  |  | ||||||
|  | `python wpm.py` | ||||||
|  |  | ||||||
|  | ### Endpoints | ||||||
|  |  | ||||||
|  |     GET /peers: List all peers. | ||||||
|  |     POST /peers: Add a new peer. | ||||||
|  |     PUT /peers/<PublicKey>: Update an existing peer. | ||||||
|  |     DELETE /peers/<PublicKey>: Delete an existing peer. | ||||||
|  |     POST /restore: Restore the WireGuard configuration from a backup. | ||||||
|  |      | ||||||
|  |  | ||||||
|  | ## Client: wpm_client.py | ||||||
|  |  | ||||||
|  | The client script allows interaction with the WireGuard Peer Management API. | ||||||
|  |  | ||||||
|  | ### Usage | ||||||
|  |  | ||||||
|  |  | ||||||
|  | python wpm_client.py <action> [options] | ||||||
|  |  | ||||||
|  | ### Available Actions | ||||||
|  |  | ||||||
|  |     create: Create a new peer. | ||||||
|  |         Required options: --public-key, --allowed-ips | ||||||
|  |     update: Update an existing peer. | ||||||
|  |         Required options: --public-key, --allowed-ips | ||||||
|  |     delete: Delete a peer by its public key. | ||||||
|  |         Required options: --public-key | ||||||
|  |     list: List all peers. | ||||||
|  |     restore: Restore the WireGuard configuration from the most recent backup. | ||||||
|  |  | ||||||
|  | ### Example Usage | ||||||
|  |  | ||||||
|  |     List Peers: | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | python wpm_client.py list | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  |     Create a New Peer: | ||||||
|  | ``` | ||||||
|  | python wpm_client.py create --public-key "<peer-public-key>" --allowed-ips "10.0.0.2/32" | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  |     Update an Existing Peer: | ||||||
|  | ``` | ||||||
|  | python wpm_client.py update --public-key "<peer-public-key>" --allowed-ips "10.0.0.3/32" | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  |     Delete a Peer: | ||||||
|  | ``` | ||||||
|  | python wpm_client.py delete --public-key "<peer-public-key>" | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  |     Restore Configuration: | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | python wpm_client.py restore | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Backup and Restore | ||||||
|  |  | ||||||
|  | The server automatically creates a backup before making any changes to the WireGuard configuration. The backups are stored in the same directory as the configuration file, inside a backups/ folder. | ||||||
|  |  | ||||||
|  | You can restore the latest backup by sending a POST /restore request, which can be done using the client or via curl: | ||||||
|  |  | ||||||
|  | curl -X POST http://localhost:8000/restore | ||||||
|  |  | ||||||
							
								
								
									
										11
									
								
								config.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								config.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | [server] | ||||||
|  | host = "127.0.0.1"  # IP address to bind to | ||||||
|  | port = 8080  # Port to bind to | ||||||
|  |  | ||||||
|  | [wireguard] | ||||||
|  | config_file = "../wireguard_example_server_config.conf" | ||||||
|  |  | ||||||
|  | [logging] | ||||||
|  | log_file = "../wpm.log"  # Optional: Log file location | ||||||
|  | debug = true  # Enable debug logging | ||||||
|  |  | ||||||
							
								
								
									
										257
									
								
								wpm.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										257
									
								
								wpm.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,257 @@ | |||||||
|  | import http.server | ||||||
|  | import socketserver | ||||||
|  | import json | ||||||
|  | import re | ||||||
|  | import urllib.parse | ||||||
|  | import tomllib | ||||||
|  | import logging | ||||||
|  | import subprocess | ||||||
|  | import shutil | ||||||
|  | from datetime import datetime | ||||||
|  | from typing import Dict, List, Tuple | ||||||
|  | from pathlib import Path | ||||||
|  |  | ||||||
|  | # Default configuration | ||||||
|  | DEFAULT_CONFIG = { | ||||||
|  |     "server": { | ||||||
|  |         "host": "0.0.0.0", | ||||||
|  |         "port": 8000, | ||||||
|  |     }, | ||||||
|  |     "wireguard": { | ||||||
|  |         "config_file": "/etc/wireguard/wg0.conf", | ||||||
|  |     }, | ||||||
|  |     "logging": { | ||||||
|  |         "log_file": None, | ||||||
|  |         "debug": False, | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | def load_config(config_file: str) -> dict: | ||||||
|  |     try: | ||||||
|  |         with open(config_file, "rb") as f: | ||||||
|  |             config = tomllib.load(f) | ||||||
|  |         # Merge with default config | ||||||
|  |         return {**DEFAULT_CONFIG, **config} | ||||||
|  |     except FileNotFoundError: | ||||||
|  |         logging.warning(f"Config file {config_file} not found. Using default configuration.") | ||||||
|  |         return DEFAULT_CONFIG | ||||||
|  |     except tomllib.TOMLDecodeError as e: | ||||||
|  |         logging.error(f"Error parsing config file: {e}") | ||||||
|  |         exit(1) | ||||||
|  |  | ||||||
|  | CONFIG = load_config("config.toml") | ||||||
|  |  | ||||||
|  | # Set up logging | ||||||
|  | logging.basicConfig( | ||||||
|  |     level=logging.DEBUG if CONFIG["logging"]["debug"] else logging.INFO, | ||||||
|  |     format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", | ||||||
|  |     filename=CONFIG["logging"]["log_file"], | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | def read_config() -> str: | ||||||
|  |     with open(CONFIG["wireguard"]["config_file"], 'r') as file: | ||||||
|  |         return file.read() | ||||||
|  |  | ||||||
|  | def write_config(content: str) -> None: | ||||||
|  |     with open(CONFIG["wireguard"]["config_file"], 'w') as file: | ||||||
|  |         file.write(content) | ||||||
|  |  | ||||||
|  | def create_backup(): | ||||||
|  |     config_file = Path(CONFIG["wireguard"]["config_file"]) | ||||||
|  |     backup_dir = config_file.parent / "backups" | ||||||
|  |     backup_dir.mkdir(exist_ok=True) | ||||||
|  |      | ||||||
|  |     timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | ||||||
|  |     backup_file = backup_dir / f"{config_file.stem}_{timestamp}.conf" | ||||||
|  |      | ||||||
|  |     shutil.copy2(config_file, backup_file) | ||||||
|  |     logging.info(f"Backup created: {backup_file}") | ||||||
|  |     return backup_file | ||||||
|  |  | ||||||
|  | def restore_from_backup(): | ||||||
|  |     config_file = Path(CONFIG["wireguard"]["config_file"]) | ||||||
|  |     backup_dir = config_file.parent / "backups" | ||||||
|  |      | ||||||
|  |     if not backup_dir.exists() or not list(backup_dir.glob("*.conf")): | ||||||
|  |         logging.error("No backups found") | ||||||
|  |         return False | ||||||
|  |      | ||||||
|  |     latest_backup = max(backup_dir.glob("*.conf"), key=lambda f: f.stat().st_mtime) | ||||||
|  |     shutil.copy2(latest_backup, config_file) | ||||||
|  |     logging.info(f"Restored from backup: {latest_backup}") | ||||||
|  |     return True | ||||||
|  |  | ||||||
|  | def parse_peers(config: str) -> List[Dict[str, str]]: | ||||||
|  |     peers = [] | ||||||
|  |     current_peer = None | ||||||
|  |     for line in config.split('\n'): | ||||||
|  |         line = line.strip() | ||||||
|  |         if line == "[Peer]": | ||||||
|  |             if current_peer: | ||||||
|  |                 peers.append(current_peer) | ||||||
|  |             current_peer = {} | ||||||
|  |         elif current_peer is not None and '=' in line: | ||||||
|  |             key, value = line.split('=', 1) | ||||||
|  |             current_peer[key.strip()] = value.strip() | ||||||
|  |     if current_peer: | ||||||
|  |         peers.append(current_peer) | ||||||
|  |     return peers | ||||||
|  |  | ||||||
|  | def peer_to_string(peer: Dict[str, str]) -> str: | ||||||
|  |     return f"[Peer]\n" + "\n".join(f"{k} = {v}" for k, v in peer.items()) | ||||||
|  |  | ||||||
|  | def reload_wireguard_service(): | ||||||
|  |     interface = Path(CONFIG["wireguard"]["config_file"]).stem | ||||||
|  |      | ||||||
|  |     try: | ||||||
|  |         # Check if wg-quick is available | ||||||
|  |         subprocess.run(["which", "wg-quick"], check=True, capture_output=True) | ||||||
|  |     except subprocess.CalledProcessError: | ||||||
|  |         logging.warning("wg-quick not found. WireGuard might not be installed.") | ||||||
|  |         return False, "WireGuard (wg-quick) not found. Please ensure WireGuard is installed." | ||||||
|  |  | ||||||
|  |     try: | ||||||
|  |         # Check if the interface is up | ||||||
|  |         result = subprocess.run(["wg", "show", interface], capture_output=True, text=True) | ||||||
|  |         if result.returncode == 0: | ||||||
|  |             # Interface is up, we'll restart it | ||||||
|  |             subprocess.run(["wg-quick", "down", interface], check=True) | ||||||
|  |             subprocess.run(["wg-quick", "up", interface], check=True) | ||||||
|  |             logging.info(f"WireGuard service for interface {interface} restarted successfully") | ||||||
|  |         else: | ||||||
|  |             # Interface is down, we'll just bring it up | ||||||
|  |             subprocess.run(["wg-quick", "up", interface], check=True) | ||||||
|  |             logging.info(f"WireGuard service for interface {interface} started successfully") | ||||||
|  |          | ||||||
|  |         return True, f"WireGuard service for interface {interface} reloaded successfully" | ||||||
|  |     except subprocess.CalledProcessError as e: | ||||||
|  |         error_message = f"Failed to reload WireGuard service: {e}" | ||||||
|  |         if e.stderr: | ||||||
|  |             error_message += f"\nError details: {e.stderr.decode('utf-8')}" | ||||||
|  |         logging.error(error_message) | ||||||
|  |         return False, error_message | ||||||
|  |  | ||||||
|  | class WireGuardHandler(http.server.SimpleHTTPRequestHandler): | ||||||
|  |     def _send_response(self, status_code: int, data: dict) -> None: | ||||||
|  |         self.send_response(status_code) | ||||||
|  |         self.send_header('Content-type', 'application/json') | ||||||
|  |         self.end_headers() | ||||||
|  |         self.wfile.write(json.dumps(data).encode()) | ||||||
|  |  | ||||||
|  |     def do_GET(self) -> None: | ||||||
|  |         if self.path == '/peers': | ||||||
|  |             config = read_config() | ||||||
|  |             peers = parse_peers(config) | ||||||
|  |             self._send_response(200, peers) | ||||||
|  |         else: | ||||||
|  |             self._send_response(404, {"error": "Not found"}) | ||||||
|  |  | ||||||
|  |     def do_POST(self) -> None: | ||||||
|  |         if self.path == '/peers': | ||||||
|  |             create_backup()  # Create a backup before making changes | ||||||
|  |             content_length = int(self.headers['Content-Length']) | ||||||
|  |             post_data = self.rfile.read(content_length) | ||||||
|  |             new_peer = json.loads(post_data.decode('utf-8')) | ||||||
|  |              | ||||||
|  |             config = read_config() | ||||||
|  |             config += "\n\n" + peer_to_string(new_peer) | ||||||
|  |             write_config(config) | ||||||
|  |              | ||||||
|  |             if reload_wireguard_service(): | ||||||
|  |                 self._send_response(201, {"message": "Peer added successfully and service reloaded"}) | ||||||
|  |             else: | ||||||
|  |                 self._send_response(500, {"error": "Peer added but failed to reload service"}) | ||||||
|  |         elif self.path == '/restore': | ||||||
|  |             if restore_from_backup(): | ||||||
|  |                 if reload_wireguard_service(): | ||||||
|  |                     self._send_response(200, {"message": "Configuration restored from backup and service reloaded"}) | ||||||
|  |                 else: | ||||||
|  |                     self._send_response(500, {"error": "Configuration restored but failed to reload service"}) | ||||||
|  |             else: | ||||||
|  |                 self._send_response(500, {"error": "Failed to restore from backup"}) | ||||||
|  |         else: | ||||||
|  |             self._send_response(404, {"error": "Not found"}) | ||||||
|  |  | ||||||
|  |     def do_PUT(self) -> None: | ||||||
|  |         path_parts = self.path.split('/') | ||||||
|  |         if len(path_parts) == 3 and path_parts[1] == 'peers': | ||||||
|  |             create_backup()  # Create a backup before making changes | ||||||
|  |             public_key = urllib.parse.unquote(path_parts[2]) | ||||||
|  |             content_length = int(self.headers['Content-Length']) | ||||||
|  |             put_data = self.rfile.read(content_length) | ||||||
|  |             updated_peer = json.loads(put_data.decode('utf-8')) | ||||||
|  |              | ||||||
|  |             config = read_config() | ||||||
|  |             peers = parse_peers(config) | ||||||
|  |  | ||||||
|  |             peer_found = False | ||||||
|  |             for i, peer in enumerate(peers): | ||||||
|  |                 if peer.get('PublicKey') == public_key: | ||||||
|  |                     peer_found = True | ||||||
|  |                     # Update the peer | ||||||
|  |                     peers[i] = updated_peer | ||||||
|  |                     new_config = re.sub(r'(\[Interface\].*?\n\n)(.*)',  | ||||||
|  |                                         r'\1' + '\n\n'.join(peer_to_string(p) for p in peers),  | ||||||
|  |                                         config,  | ||||||
|  |                                         flags=re.DOTALL) | ||||||
|  |                     write_config(new_config) | ||||||
|  |  | ||||||
|  |                     # Reload WireGuard service and send the appropriate response | ||||||
|  |                     success, message = reload_wireguard_service() | ||||||
|  |                     if success: | ||||||
|  |                         self._send_response(200, {"message": "Peer updated successfully and service reloaded"}) | ||||||
|  |                     else: | ||||||
|  |                         self._send_response(500, {"error": f"Peer updated but failed to reload service: {message}"}) | ||||||
|  |                     break | ||||||
|  |          | ||||||
|  |             if not peer_found: | ||||||
|  |                 # If no peer with the given public key was found, return a 404 response | ||||||
|  |                 self._send_response(404, {"error": "Peer not found"}) | ||||||
|  |         else: | ||||||
|  |             self._send_response(404, {"error": "Not found"}) | ||||||
|  |  | ||||||
|  |     def do_DELETE(self) -> None: | ||||||
|  |         path_parts = self.path.split('/') | ||||||
|  |         if len(path_parts) == 3 and path_parts[1] == 'peers': | ||||||
|  |             create_backup()  # Create a backup before making changes | ||||||
|  |             public_key = urllib.parse.unquote(path_parts[2]) | ||||||
|  |  | ||||||
|  |             config = read_config() | ||||||
|  |             peers = parse_peers(config) | ||||||
|  |  | ||||||
|  |             peer_found = False | ||||||
|  |             for peer in peers: | ||||||
|  |                 if peer.get('PublicKey') == public_key: | ||||||
|  |                     peer_found = True | ||||||
|  |                     # Remove the peer | ||||||
|  |                     peers.remove(peer) | ||||||
|  |                     new_config = re.sub(r'(\[Interface\].*?\n\n)(.*)',  | ||||||
|  |                                         r'\1' + '\n\n'.join(peer_to_string(p) for p in peers),  | ||||||
|  |                                         config,  | ||||||
|  |                                         flags=re.DOTALL) | ||||||
|  |                     write_config(new_config) | ||||||
|  |  | ||||||
|  |                     # Reload WireGuard service and send the appropriate response | ||||||
|  |                     success, message = reload_wireguard_service() | ||||||
|  |                     if success: | ||||||
|  |                         self._send_response(200, {"message": "Peer deleted successfully and service reloaded"}) | ||||||
|  |                     else: | ||||||
|  |                         self._send_response(500, {"error": f"Peer deleted but failed to reload service: {message}"}) | ||||||
|  |                     break | ||||||
|  |              | ||||||
|  |             if not peer_found: | ||||||
|  |                 # If no peer with the given public key was found, return a 404 response | ||||||
|  |                 self._send_response(404, {"error": "Peer not found"}) | ||||||
|  |         else: | ||||||
|  |             self._send_response(404, {"error": "Not found"}) | ||||||
|  |  | ||||||
|  | def run_server() -> None: | ||||||
|  |     host = CONFIG["server"]["host"] | ||||||
|  |     port = CONFIG["server"]["port"] | ||||||
|  |     with socketserver.TCPServer((host, port), WireGuardHandler) as httpd: | ||||||
|  |         logging.info(f"Serving on {host}:{port}") | ||||||
|  |         httpd.serve_forever() | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     run_server() | ||||||
|  |  | ||||||
							
								
								
									
										84
									
								
								wpm_client.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								wpm_client.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,84 @@ | |||||||
|  | import argparse | ||||||
|  | import requests | ||||||
|  | import json | ||||||
|  | import sys | ||||||
|  |  | ||||||
|  | BASE_URL = "http://localhost:8080"  # Update this if your server is running on a different host or port | ||||||
|  |  | ||||||
|  | def create_or_update_peer(public_key, allowed_ips): | ||||||
|  |     url = f"{BASE_URL}/peers" | ||||||
|  |     data = { | ||||||
|  |         "PublicKey": public_key, | ||||||
|  |         "AllowedIPs": allowed_ips | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     # First, try to update an existing peer | ||||||
|  |     response = requests.put(f"{url}/{public_key}", json=data) | ||||||
|  |      | ||||||
|  |     if response.status_code == 404: | ||||||
|  |         # If the peer doesn't exist, create a new one | ||||||
|  |         response = requests.post(url, json=data) | ||||||
|  |      | ||||||
|  |     if response.status_code in (200, 201): | ||||||
|  |         result = response.json() | ||||||
|  |         if "warning" in result: | ||||||
|  |             print(f"Warning: {result['warning']}") | ||||||
|  |         else: | ||||||
|  |             print(result['message']) | ||||||
|  |     else: | ||||||
|  |         print(f"Error: {response.status_code} - {response.text}")     | ||||||
|  |  | ||||||
|  | def delete_peer(public_key): | ||||||
|  |     url = f"{BASE_URL}/peers/{public_key}" | ||||||
|  |     response = requests.delete(url) | ||||||
|  |      | ||||||
|  |     if response.status_code == 200: | ||||||
|  |         print("Peer deleted successfully.") | ||||||
|  |     else: | ||||||
|  |         print(f"Error: {response.status_code} - {response.text}") | ||||||
|  |  | ||||||
|  | def list_peers(): | ||||||
|  |     url = f"{BASE_URL}/peers" | ||||||
|  |     response = requests.get(url) | ||||||
|  |      | ||||||
|  |     if response.status_code == 200: | ||||||
|  |         peers = response.json() | ||||||
|  |         print(json.dumps(peers, indent=2)) | ||||||
|  |     else: | ||||||
|  |         print(f"Error: {response.status_code} - {response.text}") | ||||||
|  |  | ||||||
|  | def restore_config(): | ||||||
|  |     url = f"{BASE_URL}/restore" | ||||||
|  |     response = requests.post(url) | ||||||
|  |      | ||||||
|  |     if response.status_code == 200: | ||||||
|  |         print("Configuration restored successfully.") | ||||||
|  |     else: | ||||||
|  |         print(f"Error: {response.status_code} - {response.text}") | ||||||
|  |  | ||||||
|  | def main(): | ||||||
|  |     parser = argparse.ArgumentParser(description="WireGuard Config Manager Client") | ||||||
|  |     parser.add_argument("action", choices=["create", "update", "delete", "list", "restore"], help="Action to perform") | ||||||
|  |     parser.add_argument("--public-key", help="Public key of the peer") | ||||||
|  |     parser.add_argument("--allowed-ips", help="Allowed IPs for the peer") | ||||||
|  |      | ||||||
|  |     args = parser.parse_args() | ||||||
|  |      | ||||||
|  |     if args.action in ["create", "update"]: | ||||||
|  |         if not args.public_key or not args.allowed_ips: | ||||||
|  |             print("Error: Both --public-key and --allowed-ips are required for create/update actions.") | ||||||
|  |             sys.exit(1) | ||||||
|  |         create_or_update_peer(args.public_key, args.allowed_ips) | ||||||
|  |     elif args.action == "delete": | ||||||
|  |         if not args.public_key: | ||||||
|  |             print("Error: --public-key is required for delete action.") | ||||||
|  |             sys.exit(1) | ||||||
|  |         delete_peer(args.public_key) | ||||||
|  |     elif args.action == "list": | ||||||
|  |         list_peers() | ||||||
|  |     elif args.action == "restore": | ||||||
|  |         restore_config() | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     main() | ||||||
|  |  | ||||||
		Reference in New Issue
	
	Block a user
	 kalzu rekku
					kalzu rekku