|
| 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) |
0 commit comments