2025, Dec 05 23:00
Parsing WireGuard wg.conf in Python: Fixing Peer List Overwrites and Building a Clean JSON-ready Structure
Learn how to parse WireGuard wg.conf in Python, handle multiple [Peer] sections with lists, avoid overwriting, and output predictable JSON structure for peers.
Parsing a WireGuard-style wg.conf into a clean Python structure looks trivial until you hit nested blocks like multiple [Peer] sections. The classic pitfall is to end up overwriting a single object instead of accumulating a list of peers. Below is a practical walk-through of what goes wrong and how to fix it cleanly, without changing the core logic.
Example input
Assume a config laid out like this:
[Interface]
Address = 10.200.200.1/24
ListenPort = 10086
PrivateKey = [Server's private key]
[Peer]
# Client 1
PublicKey = [Client's public key]
AllowedIPs = 10.200.200.2/32
[Peer]
# Client 2
PublicKey = [Client's public key]
AllowedIPs = 10.200.200.2/32
Problematic code sample
The following snippet tries to build a nested structure but mistakenly turns the peers container into a single dict, increments an index in the wrong place, and never uses that index to address elements:
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)
What’s actually going wrong
There are three distinct issues here that compound into the wrong shape of the result.
First, the container for peers is set to a dict, not a list, so you cannot accumulate multiple peer objects. Each new peer overwrites the previous one under the same keys.
Second, the code increments an index but never uses it to select where a peer should land. Even if the container were a list, you would still be writing to the same place because the index is not used to address a specific element.
Third, incrementing the index at the PublicKey line is too late for the current block. If you go with an index-based approach, the index should advance as soon as a new [Peer] section begins to ensure all subsequent lines within that block are assigned to the same peer entry.
In Python, associative arrays are called dictionaries. If you expect an ordered, indexable collection (like multiple peers), use a list.
These issues often surface as either a single peer object in the output or a KeyError (for example, KeyError: 0) when treating a dict as if it were a list.
Fix: model peers as a list and append per [Peer]
A straightforward way to stay aligned with the structure of the file is to initialize peers as a list and append a new dict each time you hit a [Peer] block. Then, while you are inside that block, write into the last element via [-1]. The parsing logic remains the same; only the data structure and the timing of when a peer is created are adjusted.
# json import is not required unless you serialize later
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)
This keeps the core approach intact: read line by line, switch context when a section header is encountered, and capture key/value pairs based on the current section. The only structural change is using a list for peers and appending a new dict at each [Peer].
Why this matters
When extracting structured data from line-oriented configs, the choice between dict and list is crucial for preserving multiplicity. A dict maps unique keys to values, so it works for singletons like srv_addr or srv_port. A list models repeated sections such as multiple [Peer] blocks. Getting this wrong leads to overwriting data or index errors. It also determines how downstream code consumes the result, for instance iterating peers reliably or serializing to JSON with the expected shape.
Takeaways
Use a list for repeated blocks like peers and append a new dict immediately when a new [Peer] section starts. If you prefer index-based addressing, increment the index exactly at the [Peer] header and then write all fields to that indexed element. Avoid incrementing the index at a field line like PublicKey, and avoid treating a dict as a list. With these small adjustments, the parser produces a stable structure that matches the intended JSON shape and behaves predictably.