Skip to content

Instantly share code, notes, and snippets.

@a1678991
Created May 4, 2025 07:38
Show Gist options
  • Save a1678991/18d94fe0543369078bed64219892c001 to your computer and use it in GitHub Desktop.
Save a1678991/18d94fe0543369078bed64219892c001 to your computer and use it in GitHub Desktop.
#!/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