Created
May 4, 2025 07:38
-
-
Save a1678991/18d94fe0543369078bed64219892c001 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
import argparse | |
import configparser | |
import os | |
import re | |
import sys | |
from pathlib import Path | |
def parse_wg_config(config_path: Path) -> dict: | |
"""Parses a wg-quick configuration file.""" | |
# configparser doesn't handle duplicate keys well or keys without values like Peer section headers easily | |
# We'll parse manually | |
config = {'Interface': {}, 'Peers': []} | |
current_section = None | |
peer_index = -1 | |
with config_path.open('r') as f: | |
for line in f: | |
line = line.strip() | |
if not line or line.startswith('#'): | |
continue | |
section_match = re.match(r'^\[(Interface|Peer)\]$', line, re.IGNORECASE) | |
if section_match: | |
current_section = section_match.group(1).lower() | |
if current_section == 'peer': | |
config['Peers'].append({}) | |
peer_index += 1 | |
continue | |
if current_section: | |
key_value_match = re.match(r'^(\w+)\s*=\s*(.*)$', line) | |
if key_value_match: | |
key, value = key_value_match.groups() | |
value = value.strip() | |
if current_section == 'interface': | |
# Handle multi-value keys like Address, DNS | |
if key in config['Interface']: | |
if isinstance(config['Interface'][key], list): | |
config['Interface'][key].append(value) | |
else: | |
config['Interface'][key] = [config['Interface'][key], value] | |
else: | |
config['Interface'][key] = value | |
elif current_section == 'peer': | |
if key in config['Peers'][peer_index]: | |
if isinstance(config['Peers'][peer_index][key], list): | |
config['Peers'][peer_index][key].append(value) | |
else: | |
config['Peers'][peer_index][key] = [config['Peers'][peer_index][key], value] | |
else: | |
config['Peers'][peer_index][key] = value | |
else: | |
print(f"Warning: Skipping malformed line in [{current_section}]: {line}", file=sys.stderr) | |
else: | |
print(f"Warning: Skipping line outside any section: {line}", file=sys.stderr) | |
# Ensure multi-value keys are lists even if only one entry was found | |
for key in ['Address', 'DNS']: | |
if key in config['Interface'] and not isinstance(config['Interface'][key], list): | |
config['Interface'][key] = [config['Interface'][key]] | |
for i, peer in enumerate(config['Peers']): | |
if 'AllowedIPs' in peer and not isinstance(peer['AllowedIPs'], list): | |
config['Peers'][i]['AllowedIPs'] = [peer['AllowedIPs']] | |
return config | |
def generate_netdev(interface_data: dict, interface_name: str) -> str: | |
"""Generates the content for the .netdev file.""" | |
content = f"[NetDev]\nName={interface_name}\nKind=wireguard\n\n[WireGuard]\n" | |
if 'PrivateKey' not in interface_data: | |
raise ValueError("Missing PrivateKey in [Interface] section") | |
content += f"PrivateKey={interface_data['PrivateKey']}\n" | |
if 'ListenPort' in interface_data: | |
content += f"ListenPort={interface_data['ListenPort']}\n" | |
# Note: FwMark is typically handled by wg-quick scripts, not directly in the config usually. | |
# If needed, it could be added here. | |
return content | |
def generate_network(interface_data: dict, interface_name: str) -> str: | |
"""Generates the content for the .network file.""" | |
content = f"[Match]\nName={interface_name}\n\n[Network]\n" | |
if 'Address' in interface_data: | |
for addr in interface_data['Address']: | |
content += f"Address={addr}\n" # Networkd creates the Address section automatically | |
if 'DNS' in interface_data: | |
for dns in interface_data['DNS']: | |
content += f"DNS={dns}\n" | |
# Add default route if AllowedIPs contains 0.0.0.0/0 or ::/0 in any peer | |
# This is a common setup but needs peer data. We'll handle it later. | |
# Note: MTU, Table, PreUp, PostUp, etc. from wg-quick are not directly mapped here. | |
# They often require separate systemd unit configurations or networkd settings. | |
return content | |
def generate_peer_config(peer_data: dict, peer_filename_prefix: str) -> tuple[str, str]: | |
"""Generates the content for a peer .conf file.""" | |
if 'PublicKey' not in peer_data: | |
raise ValueError("Missing PublicKey in [Peer] section") | |
# Use first 8 chars of public key for a somewhat readable filename | |
# Replace slashes which are invalid in filenames | |
safe_pk_part = peer_data['PublicKey'].replace('/', '_').replace('+', '-')[:8] | |
filename = f"{peer_filename_prefix}_{safe_pk_part}.conf" | |
content = "[WireGuardPeer]\n" | |
content += f"PublicKey={peer_data['PublicKey']}\n" | |
if 'AllowedIPs' in peer_data: | |
# networkd expects space-separated list for AllowedIPs | |
allowed_ips_str = " ".join(peer_data['AllowedIPs']) | |
content += f"AllowedIPs={allowed_ips_str}\n" | |
if 'Endpoint' in peer_data: | |
content += f"Endpoint={peer_data['Endpoint']}\n" | |
if 'PersistentKeepalive' in peer_data: | |
# Name change wg-quick -> systemd-networkd | |
content += f"PersistentKeepaliveInterval={peer_data['PersistentKeepalive']}\n" | |
if 'PresharedKey' in peer_data: | |
content += f"PresharedKey={peer_data['PresharedKey']}\n" | |
return filename, content | |
def main(): | |
parser = argparse.ArgumentParser( | |
description="Convert wg-quick .conf to systemd-networkd configuration.", | |
formatter_class=argparse.ArgumentDefaultsHelpFormatter | |
) | |
parser.add_argument("config_file", help="Path to the wg-quick .conf file.") | |
parser.add_argument( | |
"-o", "--output-dir", default="/etc/systemd/network", | |
help="Directory to write the output .netdev, .network, and .netdev.d files." | |
) | |
parser.add_argument( | |
"-n", "--name", default=None, | |
help="Name for the WireGuard interface (default: name of config file without extension)." | |
) | |
args = parser.parse_args() | |
config_path = Path(args.config_file) | |
output_dir = Path(args.output_dir) | |
interface_name = args.name if args.name else config_path.stem | |
if not config_path.is_file(): | |
print(f"Error: Input config file not found: {config_path}", file=sys.stderr) | |
sys.exit(1) | |
# Create output directory if it doesn't exist | |
output_dir.mkdir(parents=True, exist_ok=True) | |
peer_dir = output_dir / f"{interface_name}.netdev.d" | |
peer_dir.mkdir(exist_ok=True) | |
print(f"Processing '{config_path}' -> Interface '{interface_name}' in '{output_dir}'...") | |
try: | |
config_data = parse_wg_config(config_path) | |
# --- Generate .netdev --- | |
netdev_content = generate_netdev(config_data['Interface'], interface_name) | |
netdev_file = output_dir / f"{interface_name}.netdev" | |
print(f" Generating {netdev_file}...") | |
with netdev_file.open('w') as f: | |
f.write(netdev_content) | |
os.chmod(netdev_file, 0o644) # Standard permissions for networkd files | |
# --- Generate .network --- | |
network_content = generate_network(config_data['Interface'], interface_name) | |
network_file = output_dir / f"{interface_name}.network" | |
print(f" Generating {network_file}...") | |
with network_file.open('w') as f: | |
f.write(network_content) | |
os.chmod(network_file, 0o644) | |
# --- Generate Peer Configs --- | |
if config_data['Peers']: | |
print(f" Generating peer configurations in {peer_dir}...") | |
peer_count = 0 | |
for i, peer in enumerate(config_data['Peers']): | |
try: | |
# Use index as prefix for peer filenames for consistent ordering if needed | |
peer_filename, peer_content = generate_peer_config(peer, f"peer{i+1}") | |
peer_file_path = peer_dir / peer_filename | |
print(f" Generating {peer_file_path}...") | |
with peer_file_path.open('w') as f: | |
f.write(peer_content) | |
os.chmod(peer_file_path, 0o644) | |
peer_count += 1 | |
except ValueError as e: | |
print(f"Warning: Skipping peer {i+1} due to error: {e}", file=sys.stderr) | |
print(f" Generated {peer_count} peer configuration file(s).") | |
else: | |
print(" No [Peer] sections found in the configuration.") | |
print("\nConversion complete.") | |
print(f"Ensure systemd-networkd is enabled and restart it (`systemctl restart systemd-networkd`)") | |
print(f"Then bring up the interface: `networkctl up {interface_name}` or `networkctl reload`") | |
print("Check status with `wg show` and `networkctl status`") | |
except FileNotFoundError: | |
print(f"Error: Output directory cannot be created or accessed: {output_dir}", file=sys.stderr) | |
sys.exit(1) | |
except ValueError as e: | |
print(f"Error processing config file: {e}", file=sys.stderr) | |
sys.exit(1) | |
except Exception as e: | |
print(f"An unexpected error occurred: {e}", file=sys.stderr) | |
sys.exit(1) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment