2025, Dec 23 06:02

Как парсить wg.conf WireGuard в Python без потери пиров

Пошагowo разбираем wg.conf WireGuard в Python: как вместо dict использовать список для [Peer], не терять пиров и получать корректный JSON. Пример кода и правки.

Разбор wg.conf в стиле WireGuard в аккуратную структуру Python кажется простым, пока не сталкиваешься с вложенными блоками вроде нескольких секций [Peer]. Типичная ловушка — перезаписать один объект вместо того, чтобы накапливать список пиров. Ниже — практический разбор, что идет не так и как это исправить аккуратно, не меняя основную логику.

Пример входных данных

Предположим, конфигурация выглядит так:

[Interface]
Address = 10.200.200.1/24
ListenPort = 10086
PrivateKey = [Server's private key]

[Peer]
# Клиент 1
PublicKey = [Client's public key]
AllowedIPs = 10.200.200.2/32


[Peer]
# Клиент 2
PublicKey = [Client's public key]
AllowedIPs = 10.200.200.2/32

Проблемный пример кода

В следующем фрагменте пытаются собрать вложенную структуру, но по ошибке превращают контейнер пиров в один dict, увеличивают индекс не там, где нужно, и при этом нигде его не используют для доступа к элементам:

import json

conf_path = "/etc/wireguard/wg0.conf"

section_tag = ""
acc = {}
acc["peers"] = {}
idx = 0

with open(conf_path) as fh:
    while row := fh.readline():
        if row.startswith("[Interface]"):
            section_tag = "srv"
        elif row.startswith("[Peer]"):
            section_tag = "peer"

        if section_tag == "srv":
            if row.startswith("Address"):
                acc["srv_addr"] = row.split()[2]
            if row.startswith("ListenPort"):
                acc["srv_port"] = row.split()[2]
            if row.startswith("PrivateKey"):
                acc["srv_priv_key"] = row.split()[2]

        elif section_tag == "peer":
            if row.startswith("#"):
                acc["peers"]["comment"] = row.replace("#", "").replace("\n", "")
            if row.startswith("PublicKey"):
                acc["peers"]["pub_key"] = row.split()[2]
                idx += 1
            if row.startswith("AllowedIPs"):
                acc["peers"]["allow_ip"] = row.split()[2]

print(acc)

Что на самом деле не так

Здесь три отдельные проблемы, которые вместе дают неправильную форму результата.

Во‑первых, контейнер для пиров задан как dict, а не список, поэтому накапливать несколько объектов нельзя. Каждый новый пир перезаписывает предыдущий по тем же ключам.

Во‑вторых, индекс увеличивается, но нигде не применяется, чтобы выбрать, куда помещать данные пира. Даже если бы контейнер был списком, запись шла бы всё равно в одно и то же место, потому что индекс не используется для доступа к конкретному элементу.

В‑третьих, инкремент индекса на строке PublicKey происходит слишком поздно для текущего блока. Если вы выбираете индексный подход, индекс нужно увеличивать сразу при начале новой секции [Peer], чтобы все последующие строки блока попали в одну запись пира.

В Python ассоциативные массивы называются словарями (dictionaries). Если вам нужна упорядоченная индексируемая коллекция (например, несколько пиров), используйте список.

Эти ошибки часто приводят к тому, что на выходе оказывается один единственный пир, либо возникает KeyError (например, KeyError: 0), когда словарь пытаются использовать как список.

Исправление: представить пиров списком и добавлять по каждому [Peer]

Простой способ сохранить соответствие структуре файла — инициализировать peers как список и при встрече каждого блока [Peer] добавлять новый dict. Далее, находясь внутри этого блока, записывать данные в последний элемент через [-1]. Логика разбора не меняется; корректируются лишь структура данных и момент создания записи пира.

# импорт json не требуется, если вы не будете сериализовать позже

conf_path = "/etc/wireguard/wg0.conf"

section_tag = ""
acc = {}
acc["peers"] = []

with open(conf_path) as fh:
    while row := fh.readline():
        if row.startswith("[Interface]"):
            section_tag = "srv"
        elif row.startswith("[Peer]"):
            section_tag = "peer"
            acc["peers"].append({})

        if section_tag == "srv":
            if row.startswith("Address"):
                acc["srv_addr"] = row.split()[2]
            if row.startswith("ListenPort"):
                acc["srv_port"] = row.split()[2]
            if row.startswith("PrivateKey"):
                acc["srv_priv_key"] = row.split()[2]

        elif section_tag == "peer":
            if row.startswith("#"):
                acc["peers"][-1]["comment"] = row.replace("#", "").replace("\n", "")
            if row.startswith("PublicKey"):
                acc["peers"][-1]["pub_key"] = row.split()[2]
            if row.startswith("AllowedIPs"):
                acc["peers"][-1]["allow_ip"] = row.split()[2]

print(acc)

Это сохраняет исходный подход: читаем построчно, меняем контекст при встрече заголовка секции и извлекаем пары ключ/значение в зависимости от текущего раздела. Единственное структурное изменение — использовать список для пиров и добавлять новый dict на каждом [Peer].

Почему это важно

При извлечении структурированных данных из построчных конфигов выбор между dict и list критичен для сохранения кратности. Словарь отображает уникальные ключи в значения, поэтому подходит для одиночных полей вроде srv_addr или srv_port. Список моделирует повторяющиеся секции, такие как несколько блоков [Peer]. Ошибка в выборе приводит к перезаписи данных или ошибкам индексов. Это также влияет на работу последующего кода — например, на надёжную итерацию по пирам или сериализацию в JSON с ожидаемой структурой.

Выводы

Для повторяющихся блоков, таких как peers, используйте список и сразу добавляйте новый dict при начале секции [Peer]. Если вам ближе индексный подход, увеличивайте индекс именно на заголовке [Peer], а затем записывайте все поля в элемент по этому индексу. Не инкрементируйте индекс на строках полей вроде PublicKey и не пытайтесь обращаться со словарём как со списком. С этими небольшими правками парсер выдаёт стабильную структуру, совпадающую с ожидаемой формой JSON и предсказуемо себя ведущую.