#!/usr/bin/env python # # Copyright 2025 David Vazgenovich Shakaryan import argparse import collections import copy import functools import io import ipaddress import os import re import shutil import string import sys import tomllib class Network(dict): def __init__(self, data, name): super().__init__(data) self.name = name self.peers = {} def add_peer(self, peer): self.peers[peer.name] = peer class Peer(dict): def __init__(self, data, name, network): super().__init__(data) self.name = name self.network = network self.interfaces = {} self.network.add_peer(self) def add_interface(self, interface): self.interfaces[interface.name] = interface def ip_interfaces(self, version=None): return [ ipaddress.ip_interface( f'{x[ int(self['id']) if x.version == 4 else int(str(self['id']), 16)]}' f'/{x.prefixlen}') for x in self.network['subnets'] if not version or x.version == int(version)] class Interface(collections.UserDict): _INHERITABLE = [ 'port', 'fwmark', 'keepalive', 'file-prefix', 'name-format', 'privkey-path', ] def __init__(self, data, name, peer): super().__init__(data) self.name = name self.peer = peer self.peer.add_interface(self) def __contains__(self, key): return ( key in self.data or (key in self._INHERITABLE and key in self.peer)) def __getitem__(self, key): try: return self.data[key] except KeyError: if key in self._INHERITABLE: return self.peer[key] raise @functools.cached_property def network(self): return self.peer.network @functools.cached_property def formatted_name(self): return self.format_string(self.get('name-format', '$network$_if')) def format_string(self, s): return string.Template(s).substitute({ 'network': self.network.name, 'peer': self.peer.name, 'if': self.name, '_if': f'-{self.name}' if self.name else ''}) def deep_merge(d, src): for k, v in src.items(): if isinstance(v, dict) and (dv := d.get(k)): deep_merge(dv, v) else: d[k] = v return d # given peer with ips 10.0.0.22, fd00:ff:dead:beef::22 # {peer4/24} = 10.0.0.0/24 # {peer4/28} = 10.0.0.16/28 # {peer4} = 10.0.0.22/32 # {peer6/56} = fd00:ff:dead:be00::/56 # {peer6/64} = fd00:ff:dead:beef::/64 # {peer6} = fd00:ff:dead:beef::22/128 # {peer} = 10.0.0.22/32, fd00:ff:dead:beef::22/128 # # a subnet size of '-', e.g. {peer/-}, will take the subnet sizes from the # network configuration. # # by default, this returns network addresses with host bits removed. # passing interface=True will maintain the host bits. def ipspec_to_ips(peer, ipspec, *, interface=False): if not (m := re.fullmatch( r'\{peer([46])?(?!(?