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