Skip to content

Commit 7fbe1e9

Browse files
Add WireGuard (wg) Command Output Parser (#606)
* feat: add parser to parse the output of wg * fixup! feat: add parser to parse the output of wg * feat: Add tests for windows 10 --------- Co-authored-by: Kelly Brazil <[email protected]>
1 parent 7d33850 commit 7fbe1e9

9 files changed

+771
-0
lines changed

jc/lib.py

+1
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@
225225
'vmstat-s',
226226
'w',
227227
'wc',
228+
'wg-show',
228229
'who',
229230
'x509-cert',
230231
'x509-csr',

jc/parsers/wg_show.py

+270
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
r"""jc - JSON Convert `wg show` command output parser
2+
3+
Parses the output of the `wg show all dump` command, providing structured JSON output for easy integration and analysis.
4+
5+
Usage (cli):
6+
7+
$ wg show all dump | jc --wg-show
8+
9+
or
10+
11+
$ jc wg-show
12+
13+
Usage (module):
14+
15+
import jc
16+
result = jc.parse('wg-show', wg_command_output)
17+
18+
Schema:
19+
20+
[
21+
{
22+
"device": string,
23+
"privateKey": string,
24+
"publicKey": string,
25+
"listenPort": integer,
26+
"fwmark": integer,
27+
"peers": [
28+
{
29+
"publicKey": string,
30+
"presharedKey": string,
31+
"endpoint": string,
32+
"latestHandshake": integer,
33+
"transferRx": integer,
34+
"transferSx": integer,
35+
"persistentKeepalive": integer,
36+
"allowedIps": [string]
37+
}
38+
]
39+
}
40+
]
41+
42+
Examples:
43+
44+
$ wg show all dump | jc --wg-show -p
45+
[
46+
{
47+
"device": "wg0",
48+
"privateKey": "aEbVdvHSEp3oofHDNVCsUoaRSxk1Og8/pTLof5yF+1M=",
49+
"publicKey": "OIxbQszw1chdO5uigAxpsl4fc/h04yMYafl72gUbakM=",
50+
"listenPort": 51820,
51+
"fwmark": null,
52+
"peers": {
53+
"sQFGAhSdx0aC7DmTFojzBOW8Ccjv1XV5+N9FnkZu5zc=": {
54+
"presharedKey": null,
55+
"endpoint": "79.134.136.199:40036",
56+
"latestHandshake": 1728809756,
57+
"transferRx": 1378724,
58+
"transferSx": 406524,
59+
"persistentKeepalive": null,
60+
"allowedIps": ["10.10.0.2/32"]
61+
},
62+
"B9csmpvrv4Q7gpjc6zAbNNO8hIOYfpBqxmik2aNpwwE=": {
63+
"presharedKey": null,
64+
"endpoint": "79.134.136.199:35946",
65+
"latestHandshake": 1728809756,
66+
"transferRx": 4884248,
67+
"transferSx": 3544596,
68+
"persistentKeepalive": null,
69+
"allowedIps": ["10.10.0.3/32"]
70+
},
71+
"miiSYR5UdevREhlWpmnci+vv/dEGLHbNtKu7u1CuOD4=": {
72+
"presharedKey": null,
73+
"allowedIps": ["10.10.0.4/32"]
74+
},
75+
"gx9+JHLHJvOfBNjTmZ8KQAnThFFiZMQrX1kRaYcIYzw=": {
76+
"presharedKey": null,
77+
"endpoint": "173.244.225.194:45014",
78+
"latestHandshake": 1728809827,
79+
"transferRx": 1363652,
80+
"transferSx": 458252,
81+
"persistentKeepalive": null,
82+
"allowedIps": ["10.10.0.5/32"]
83+
}
84+
}
85+
}
86+
]
87+
88+
89+
$ wg show all dump | jc --wg-show -p -r
90+
[
91+
{
92+
"device": "wg0",
93+
"privateKey": "aEbVdvHSEp3oofHDNVCsUoaRSxk1Og8/pTLof5yF+1M=",
94+
"publicKey": "OIxbQszw1chdO5uigAxpsl4fc/h04yMYafl72gUbakM=",
95+
"listenPort": 51820,
96+
"fwmark": null,
97+
"peers": {
98+
"sQFGAhSdx0aC7DmTFojzBOW8Ccjv1XV5+N9FnkZu5zc=": {
99+
"presharedKey": null,
100+
"endpoint": "79.134.136.199:40036",
101+
"latestHandshake": 1728809756,
102+
"transferRx": 1378724,
103+
"transferSx": 406524,
104+
"persistentKeepalive": -1,
105+
"allowedIps": ["10.10.0.2/32"]
106+
},
107+
"B9csmpvrv4Q7gpjc6zAbNNO8hIOYfpBqxmik2aNpwwE=": {
108+
"presharedKey": null,
109+
"endpoint": "79.134.136.199:35946",
110+
"latestHandshake": 1728809756,
111+
"transferRx": 4884248,
112+
"transferSx": 3544596,
113+
"persistentKeepalive": -1,
114+
"allowedIps": ["10.10.0.3/32"]
115+
},
116+
"miiSYR5UdevREhlWpmnci+vv/dEGLHbNtKu7u1CuOD4=": {
117+
"presharedKey": null,
118+
"allowedIps": ["10.10.0.4/32"]
119+
},
120+
"gx9+JHLHJvOfBNjTmZ8KQAnThFFiZMQrX1kRaYcIYzw=": {
121+
"presharedKey": null,
122+
"endpoint": "173.244.225.194:45014",
123+
"latestHandshake": 1728809827,
124+
"transferRx": 1363652,
125+
"transferSx": 458252,
126+
"persistentKeepalive": -1,
127+
"allowedIps": ["10.10.0.5/32"]
128+
}
129+
}
130+
}
131+
]
132+
"""
133+
134+
from typing import List, Dict, Optional, Union
135+
from jc.jc_types import JSONDictType
136+
import jc.utils
137+
import re
138+
139+
PeerData = Dict[str, Union[Optional[str], Optional[int], List[str]]]
140+
DeviceData = Dict[str, Union[Optional[str], Optional[int], Dict[str, PeerData]]]
141+
142+
143+
class info:
144+
"""Provides parser metadata (version, author, etc.)"""
145+
146+
version = "1.0"
147+
description = (
148+
"Parses the output of the `wg show` command to provide structured JSON data"
149+
)
150+
author = "Hamza Saht"
151+
author_email = "[email protected]"
152+
compatible = ["linux", "darwin", "cygwin", "win32", "aix", "freebsd"]
153+
tags = ["command"]
154+
magic_commands = ["wg-show"]
155+
156+
157+
__version__ = info.version
158+
159+
160+
def _process(proc_data: List[DeviceData]) -> List[JSONDictType]:
161+
"""
162+
Final processing to conform to the schema.
163+
164+
Parameters:
165+
166+
proc_data: (List[Dict]) Raw structured data to process
167+
168+
Returns:
169+
170+
List[Dict]: Structured data that conforms to the schema
171+
"""
172+
processed_data: List[JSONDictType] = []
173+
for device in proc_data:
174+
processed_device = {
175+
"device": device["device"],
176+
"privateKey": device.get("privateKey"),
177+
"publicKey": device.get("publicKey"),
178+
"listenPort": device.get("listenPort"),
179+
"fwmark": device.get("fwmark"),
180+
"peers": [
181+
{
182+
"publicKey": peer_key,
183+
"presharedKey": peer_data.get("presharedKey"),
184+
"endpoint": peer_data.get("endpoint"),
185+
"latestHandshake": peer_data.get("latestHandshake", 0),
186+
"transferRx": peer_data.get("transferRx", 0),
187+
"transferSx": peer_data.get("transferSx", 0),
188+
"persistentKeepalive": peer_data.get("persistentKeepalive", -1),
189+
"allowedIps": peer_data.get("allowedIps", []),
190+
}
191+
for peer_key, peer_data in device.get("peers", {}).items()
192+
],
193+
}
194+
processed_data.append(processed_device)
195+
return processed_data
196+
197+
198+
def parse(data: str, raw: bool = False, quiet: bool = False) -> List[DeviceData]:
199+
"""
200+
Main text parsing function.
201+
202+
Parses the output of the `wg` command, specifically `wg show all dump`, into structured JSON format.
203+
204+
Parameters:
205+
206+
data: (str) Text data to parse, typically the output from `wg show all dump`
207+
raw: (bool) If True, returns unprocessed output
208+
quiet: (bool) Suppress warning messages if True
209+
210+
Returns:
211+
212+
List[Dict]: Parsed data in JSON-friendly format, either raw or processed.
213+
"""
214+
jc.utils.compatibility(__name__, info.compatible, quiet)
215+
jc.utils.input_type_check(data)
216+
217+
raw_output: List[DeviceData] = []
218+
current_device: Optional[str] = None
219+
device_data: DeviceData = {}
220+
221+
if jc.utils.has_data(data):
222+
for line in filter(None, data.splitlines()):
223+
fields = re.split(r"\s+", line.strip())
224+
if len(fields) == 5:
225+
device, private_key, public_key, listen_port, fwmark = fields
226+
if current_device:
227+
raw_output.append({"device": current_device, **device_data})
228+
current_device = device
229+
device_data = {
230+
"privateKey": private_key if private_key != "(none)" else None,
231+
"publicKey": public_key if public_key != "(none)" else None,
232+
"listenPort": int(listen_port) if listen_port != "0" else None,
233+
"fwmark": int(fwmark) if fwmark != "off" else None,
234+
"peers": {},
235+
}
236+
elif len(fields) == 9:
237+
(
238+
interface,
239+
public_key,
240+
preshared_key,
241+
endpoint,
242+
allowed_ips,
243+
latest_handshake,
244+
transfer_rx,
245+
transfer_tx,
246+
persistent_keepalive,
247+
) = fields
248+
peer_data: PeerData = {
249+
"presharedKey": preshared_key
250+
if preshared_key != "(none)"
251+
else None,
252+
"endpoint": endpoint if endpoint != "(none)" else None,
253+
"latestHandshake": int(latest_handshake),
254+
"transferRx": int(transfer_rx),
255+
"transferSx": int(transfer_tx),
256+
"persistentKeepalive": int(persistent_keepalive)
257+
if persistent_keepalive != "off"
258+
else -1,
259+
"allowedIps": allowed_ips.split(",")
260+
if allowed_ips != "(none)"
261+
else [],
262+
}
263+
device_data["peers"][public_key] = {
264+
k: v for k, v in peer_data.items() if v is not None
265+
}
266+
267+
if current_device:
268+
raw_output.append({"device": current_device, **device_data})
269+
270+
return raw_output if raw else _process(raw_output)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
[
2+
{
3+
"device": "wg0",
4+
"privateKey": "ZkG2dvJSUq5ohkDFNWBqToaVSyk4Pp8/bULaf6yX+3N=",
5+
"publicKey": "PQsbRs4w2fidQ7uhjgCypl5fc/804bHZbfl83gGvakN=",
6+
"listenPort": 51820,
7+
"fwmark": null,
8+
"peers": [
9+
{
10+
"publicKey": "rQFRAjRdx1aD8DnTFpkcBOY9Edjt3ZU7+P9HokZv7xe=",
11+
"presharedKey": null,
12+
"endpoint": "82.132.137.204:50024",
13+
"latestHandshake": 1829439251,
14+
"transferRx": 1536642,
15+
"transferSx": 492320,
16+
"persistentKeepalive": -1,
17+
"allowedIps": [
18+
"10.10.0.2/32"
19+
]
20+
},
21+
{
22+
"publicKey": "A0fsnrwsv7S9hrk8yzCgOON9gKPYfpCrzqil4bPpxAE=",
23+
"presharedKey": null,
24+
"endpoint": "82.132.137.204:49732",
25+
"latestHandshake": 1829439125,
26+
"transferRx": 5641359,
27+
"transferSx": 4206783,
28+
"persistentKeepalive": -1,
29+
"allowedIps": [
30+
"10.10.0.3/32"
31+
]
32+
},
33+
{
34+
"publicKey": "pkjVZR5VdfnSFhlWsqnkf+wv/eHGLHzOtKu8v2CvOD5=",
35+
"presharedKey": null,
36+
"endpoint": null,
37+
"latestHandshake": 0,
38+
"transferRx": 0,
39+
"transferSx": 0,
40+
"persistentKeepalive": -1,
41+
"allowedIps": [
42+
"10.10.0.4/32"
43+
]
44+
},
45+
{
46+
"publicKey": "hz0+KJRHJyXfDNjUnZ9LRAnShFFjZNTrW2jRbXjIazx=",
47+
"presharedKey": null,
48+
"endpoint": "180.255.226.203:21678",
49+
"latestHandshake": 1829439243,
50+
"transferRx": 1555523,
51+
"transferSx": 544780,
52+
"persistentKeepalive": -1,
53+
"allowedIps": [
54+
"10.10.0.5/32"
55+
]
56+
},
57+
{
58+
"publicKey": "kujr4Z8eutQ8ebzFf33DLc3PNKrO3J+ZVy3jkrR1cUZ=",
59+
"presharedKey": null,
60+
"endpoint": "82.132.137.204:67130",
61+
"latestHandshake": 0,
62+
"transferRx": 0,
63+
"transferSx": 0,
64+
"persistentKeepalive": -1,
65+
"allowedIps": [
66+
"10.10.0.6/32"
67+
]
68+
},
69+
{
70+
"publicKey": "XcgkMRmxhQiIgXowYqsxUt1SlwUydXJd4Zmi8dgGeYe=",
71+
"presharedKey": null,
72+
"endpoint": "180.255.226.203:56092",
73+
"latestHandshake": 1829439121,
74+
"transferRx": 1900247,
75+
"transferSx": 1352639,
76+
"persistentKeepalive": -1,
77+
"allowedIps": [
78+
"10.10.0.8/32"
79+
]
80+
},
81+
{
82+
"publicKey": "tB9yFqLmyvcDD1rJ1rTgeKtKK98hdUL1jefMRt4XbDh=",
83+
"presharedKey": null,
84+
"endpoint": "82.132.137.204:60184",
85+
"latestHandshake": 1829037182,
86+
"transferRx": 75047,
87+
"transferSx": 184725,
88+
"persistentKeepalive": -1,
89+
"allowedIps": [
90+
"10.10.0.7/32"
91+
]
92+
}
93+
]
94+
}
95+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
wg0 ZkG2dvJSUq5ohkDFNWBqToaVSyk4Pp8/bULaf6yX+3N= PQsbRs4w2fidQ7uhjgCypl5fc/804bHZbfl83gGvakN= 51820 off
2+
wg0 rQFRAjRdx1aD8DnTFpkcBOY9Edjt3ZU7+P9HokZv7xe= (none) 82.132.137.204:50024 10.10.0.2/32 1829439251 1536642 492320 off
3+
wg0 A0fsnrwsv7S9hrk8yzCgOON9gKPYfpCrzqil4bPpxAE= (none) 82.132.137.204:49732 10.10.0.3/32 1829439125 5641359 4206783 off
4+
wg0 pkjVZR5VdfnSFhlWsqnkf+wv/eHGLHzOtKu8v2CvOD5= (none) (none) 10.10.0.4/32 0 0 0 off
5+
wg0 hz0+KJRHJyXfDNjUnZ9LRAnShFFjZNTrW2jRbXjIazx= (none) 180.255.226.203:21678 10.10.0.5/32 1829439243 1555523 544780 off
6+
wg0 kujr4Z8eutQ8ebzFf33DLc3PNKrO3J+ZVy3jkrR1cUZ= (none) 82.132.137.204:67130 10.10.0.6/32 0 0 0 off
7+
wg0 XcgkMRmxhQiIgXowYqsxUt1SlwUydXJd4Zmi8dgGeYe= (none) 180.255.226.203:56092 10.10.0.8/32 1829439121 1900247 1352639 off
8+
wg0 tB9yFqLmyvcDD1rJ1rTgeKtKK98hdUL1jefMRt4XbDh= (none) 82.132.137.204:60184 10.10.0.7/32 1829037182 75047 184725 off
9+

0 commit comments

Comments
 (0)