wgm 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "WebSearch",
5
+ "Bash(npm search:*)",
6
+ "Bash(git init)",
7
+ "Bash(git add:*)",
8
+ "Bash(git commit:*)",
9
+ "Bash(git reset:*)",
10
+ "Bash(git rm:*)",
11
+ "Bash(git remote:*)",
12
+ "Bash(git branch:*)",
13
+ "Bash(git push:*)"
14
+ ]
15
+ }
16
+ }
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # wgm - Simplified WireGuard Peer Management
2
+
3
+ A lightweight CLI tool for managing WireGuard peers with interactive prompts.
4
+
5
+ ## Features
6
+
7
+ - Interactive peer addition without manual config editing
8
+ - Automatic IP address allocation
9
+ - Automatic config backup
10
+ - Client config file generation with QR code
11
+ - Support for manual or auto key generation
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install -g inquirer qrcode ini
17
+ ```
18
+
19
+ Or use directly with npx:
20
+
21
+ ```bash
22
+ sudo npx wgm add
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ```bash
28
+ wgm add # Add a new peer interactively
29
+ wgm list # List all peers
30
+ wgm rm <name> # Remove a peer
31
+ ```
32
+
33
+ ## Example
34
+
35
+ ```bash
36
+ $ sudo wgm add
37
+ ? Peer name: macbook
38
+ ? Key generation method:
39
+ ❯ I provide the client public key (recommended)
40
+ Auto-generate key pair
41
+ ? Paste client publicKey: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
42
+ Assigned IP: 10.0.0.3/24
43
+ ✅ Peer "macbook" added successfully
44
+ 📄 Config saved: macbook.conf
45
+ 📱 QR Code:
46
+ [QR code displayed]
47
+ ```
48
+
49
+ ## Requirements
50
+
51
+ - WireGuard installed (`wg` command available)
52
+ - Node.js >= 16
53
+ - Root privileges
54
+
55
+ ## License
56
+
57
+ MIT
package/bin/wgm ADDED
@@ -0,0 +1,400 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * wgm - Simplified WireGuard Peer Management Tool
5
+ *
6
+ * A lightweight CLI tool for managing WireGuard peers with interactive prompts.
7
+ */
8
+
9
+ import { execSync } from 'child_process';
10
+ import inquirer from 'inquirer';
11
+ import QRCode from 'qrcode';
12
+ import { parse, stringify } from 'ini';
13
+ import { readFileSync, writeFileSync, copyFileSync, existsSync } from 'fs';
14
+ import { fileURLToPath } from 'url';
15
+ import { dirname, join } from 'path';
16
+
17
+ const __filename = fileURLToPath(import.meta.url);
18
+ const __dirname = dirname(__filename);
19
+
20
+ const CONFIG_DIR = '/etc/wireguard';
21
+
22
+ let INTERFACE = null;
23
+ let CONFIG_PATH = null;
24
+
25
+ /**
26
+ * Get WireGuard interface from args, env, or auto-detect
27
+ * @returns {Promise<string>} - Interface name
28
+ */
29
+ async function getInterface() {
30
+ // Check CLI arg: -i <name>
31
+ const argIndex = process.argv.indexOf('-i');
32
+ if (argIndex !== -1 && process.argv[argIndex + 1]) {
33
+ return process.argv[argIndex + 1];
34
+ }
35
+
36
+ // List available configs
37
+ try {
38
+ const output = execSync(`ls ${CONFIG_DIR}/*.conf 2>/dev/null | xargs -n1 basename`, { encoding: 'utf-8' });
39
+ const configs = output.trim().split('\n').filter(c => c && c.endsWith('.conf'));
40
+ if (configs.length === 0) {
41
+ console.log('[ERROR] No WireGuard configs found in ' + CONFIG_DIR);
42
+ process.exit(1);
43
+ }
44
+ const names = configs.map(c => c.replace('.conf', ''));
45
+ if (names.length === 1) {
46
+ return names[0];
47
+ }
48
+ const { interface: selected } = await inquirer.prompt([{
49
+ type: 'list',
50
+ name: 'interface',
51
+ message: 'Select WireGuard interface:',
52
+ choices: names
53
+ }]);
54
+ return selected;
55
+ } catch {
56
+ console.log('[ERROR] Cannot read ' + CONFIG_DIR);
57
+ process.exit(1);
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Initialize interface and config path
63
+ */
64
+ async function init() {
65
+ if (!INTERFACE) {
66
+ INTERFACE = await getInterface();
67
+ CONFIG_PATH = join(CONFIG_DIR, `${INTERFACE}.conf`);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Validate WireGuard public key format
73
+ * @param {string} key - Public key to validate
74
+ * @returns {boolean} - Whether the key is valid
75
+ */
76
+ function validateKey(key) {
77
+ return /^[A-Za-z0-9+/]{43}=?$/.test(key.trim());
78
+ }
79
+
80
+ /**
81
+ * Get server's public key from config
82
+ * @returns {string|null} - Server public key or null if not found
83
+ */
84
+ function getServerPublicKey() {
85
+ try {
86
+ const config = parse(readFileSync(CONFIG_PATH, 'utf-8'));
87
+ const privKey = config.Interface?.PrivateKey;
88
+ if (privKey) {
89
+ return execSync(`echo "${privKey}" | wg pubkey`, { encoding: 'utf-8' }).trim();
90
+ }
91
+ } catch (e) {
92
+ return null;
93
+ }
94
+ return null;
95
+ }
96
+
97
+ /**
98
+ * Get server endpoint (IP:Port)
99
+ * @returns {string|null} - Endpoint string or null
100
+ */
101
+ function getEndpoint() {
102
+ try {
103
+ const config = parse(readFileSync(CONFIG_PATH, 'utf-8'));
104
+ const port = config.Interface?.ListenPort || '51820';
105
+ try {
106
+ const ip = execSync('curl -s ifconfig.me', { encoding: 'utf-8', timeout: 3000 }).trim();
107
+ return `${ip}:${port}`;
108
+ } catch {
109
+ return `YOUR_SERVER_IP:${port}`;
110
+ }
111
+ } catch {
112
+ return null;
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Get next available IP address from the configured network range
118
+ * @returns {string|null} - Available IP with CIDR or null
119
+ */
120
+ function getAvailableIP() {
121
+ const config = parse(readFileSync(CONFIG_PATH, 'utf-8'));
122
+ const network = config.Interface?.Address;
123
+
124
+ if (!network) return null;
125
+
126
+ const [baseIp, cidr] = network.split('/');
127
+ const prefix = baseIp.split('.').slice(0, 3).join('.');
128
+
129
+ // Collect used IPs
130
+ const used = new Set();
131
+ for (const [key, val] of Object.entries(config)) {
132
+ if (key.startsWith('Peer') && val.AllowedIPs) {
133
+ const ip = val.AllowedIPs.split('/')[0];
134
+ const octets = ip.split('.');
135
+ if (octets.length === 4) {
136
+ const lastOctet = parseInt(octets[3]);
137
+ if (!isNaN(lastOctet)) used.add(lastOctet);
138
+ }
139
+ }
140
+ }
141
+
142
+ // Find first available IP (starting from .2)
143
+ for (let i = 2; i < 255; i++) {
144
+ if (!used.has(i)) {
145
+ return `${prefix}.${i}/${cidr}`;
146
+ }
147
+ }
148
+ return null;
149
+ }
150
+
151
+ /**
152
+ * Save configuration and reload WireGuard
153
+ * @param {Object} config - Parsed INI config object
154
+ */
155
+ function saveConfig(config) {
156
+ // Create backup
157
+ const backupPath = `${CONFIG_PATH}.backup.${Date.now()}`;
158
+ copyFileSync(CONFIG_PATH, backupPath);
159
+
160
+ // Save new config
161
+ writeFileSync(CONFIG_PATH, stringify(config, { section: '=' }));
162
+
163
+ // Reload WireGuard
164
+ try {
165
+ execSync(`wg syncconf ${INTERFACE} <(wg-quick strip ${INTERFACE})`, { stdio: 'pipe' });
166
+ } catch {
167
+ execSync(`wg-quick down ${INTERFACE} 2>/dev/null; wg-quick up ${INTERFACE}`, { stdio: 'inherit' });
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Add a new peer interactively
173
+ */
174
+ async function addPeer() {
175
+ await init();
176
+ const config = parse(readFileSync(CONFIG_PATH, 'utf-8'));
177
+
178
+ // Prompt for peer name
179
+ const { name } = await inquirer.prompt([{
180
+ type: 'input',
181
+ name: 'name',
182
+ message: 'Peer name:',
183
+ validate: input => input.length > 0 || 'Name cannot be empty'
184
+ }]);
185
+
186
+ // Check for duplicate
187
+ if (config[`Peer #${name}`]) {
188
+ console.log(`[ERROR] Peer "${name}" already exists`);
189
+ return;
190
+ }
191
+
192
+ // Choose key method
193
+ const { method } = await inquirer.prompt([{
194
+ type: 'list',
195
+ name: 'method',
196
+ message: 'Key generation method:',
197
+ choices: [
198
+ { name: 'I provide the client public key (recommended)', value: 'manual' },
199
+ { name: 'Auto-generate key pair', value: 'auto' }
200
+ ]
201
+ }]);
202
+
203
+ let publicKey, privateKey;
204
+ if (method === 'manual') {
205
+ console.log('\nOn the client device, run:');
206
+ console.log(' wg genkey | tee privatekey | wg pubkey');
207
+ console.log('Send the PUBLIC KEY to the server admin.\n');
208
+
209
+ const { key } = await inquirer.prompt([{
210
+ type: 'input',
211
+ name: 'key',
212
+ message: 'Paste client public key:',
213
+ validate: input => validateKey(input) || 'Invalid key format (44 character base64)'
214
+ }]);
215
+ publicKey = key.trim();
216
+ } else {
217
+ privateKey = execSync('wg genkey', { encoding: 'utf-8' }).trim();
218
+ publicKey = execSync(`echo "${privateKey}" | wg pubkey`, { encoding: 'utf-8' }).trim();
219
+ }
220
+
221
+ // Get available IP
222
+ const ip = getAvailableIP();
223
+ if (!ip) {
224
+ console.log('[ERROR] Unable to get available IP');
225
+ return;
226
+ }
227
+ console.log(`Assigned IP: ${ip}`);
228
+
229
+ // Get server's VPN IP for AllowedIPs
230
+ let serverIp = 'SERVER_VPN_IP';
231
+ try {
232
+ const config = parse(readFileSync(CONFIG_PATH, 'utf-8'));
233
+ if (config.Interface?.Address) {
234
+ serverIp = config.Interface.Address.split('/')[0];
235
+ }
236
+ } catch {}
237
+
238
+ const { allowedIPs } = await inquirer.prompt([{
239
+ type: 'input',
240
+ name: 'allowedIPs',
241
+ message: 'AllowedIPs (traffic to route through VPN):',
242
+ default: `${serverIp}/32`,
243
+ validate: input => input.length > 0 || 'AllowedIPs cannot be empty'
244
+ }]);
245
+
246
+ // Save config
247
+ config[`Peer #${name}`] = {
248
+ PublicKey: publicKey,
249
+ AllowedIPs: clientIp
250
+ };
251
+ saveConfig(config);
252
+ console.log(`[OK] Peer "${name}" added successfully`);
253
+
254
+ // Generate client config
255
+ const serverPubKey = getServerPublicKey();
256
+ const endpoint = getEndpoint();
257
+ const dns = config.Interface?.DNS || '1.1.1.1, 8.8.8.8';
258
+
259
+ const clientConfig = `[Interface]
260
+ ${privateKey ? `PrivateKey = ${privateKey}
261
+ ` : ''}Address = ${ip}
262
+ DNS = ${dns}
263
+
264
+ [Peer]
265
+ PublicKey = ${serverPubKey}
266
+ AllowedIPs = ${allowedIPs}
267
+ Endpoint = ${endpoint}
268
+ PersistentKeepalive = 25`;
269
+
270
+ // Generate client config
271
+ const serverPubKey = getServerPublicKey();
272
+ const endpoint = getEndpoint();
273
+ const dns = config.Interface?.DNS || '1.1.1.1, 8.8.8.8';
274
+
275
+ const clientConfig = `[Interface]
276
+ ${privateKey ? `PrivateKey = ${privateKey}
277
+ ` : ''}Address = ${ip}
278
+ DNS = ${dns}
279
+
280
+ [Peer]
281
+ PublicKey = ${serverPubKey}
282
+ AllowedIPs = 0.0.0.0/0, ::/0
283
+ Endpoint = ${endpoint}
284
+ PersistentKeepalive = 25`;
285
+
286
+ // Save to file
287
+ const filename = `${name}.conf`;
288
+ writeFileSync(filename, clientConfig);
289
+ console.log(`[FILE] Config saved: ${filename}`);
290
+
291
+ // Generate QR code
292
+ try {
293
+ const qr = await QRCode.toString(clientConfig, { type: 'terminal', small: true });
294
+ console.log('\n[QR] QR Code:');
295
+ console.log(qr);
296
+ } catch {}
297
+
298
+ if (privateKey) {
299
+ console.log('\n[WARN] Private key generated. Keep it safe!');
300
+ }
301
+ }
302
+
303
+ /**
304
+ * List all peers with their status
305
+ */
306
+ async function listPeers() {
307
+ await init();
308
+ const config = parse(readFileSync(CONFIG_PATH, 'utf-8'));
309
+
310
+ console.log(`\n Peers on ${INTERFACE}:\n`);
311
+
312
+ // Get online status
313
+ let onlinePeers = new Set();
314
+ try {
315
+ const status = execSync(`wg show ${INTERFACE}`, { encoding: 'utf-8' });
316
+ onlinePeers = new Set(status.match(/peer: ([^\s]+)/g)?.map(p => p.replace('peer: ', '')) || []);
317
+ } catch {}
318
+
319
+ const peers = Object.entries(config).filter(([k]) => k.startsWith('Peer'));
320
+
321
+ if (peers.length === 0) {
322
+ console.log(' No peers configured');
323
+ return;
324
+ }
325
+
326
+ for (const [key, val] of peers) {
327
+ const name = key.replace('Peer #', '');
328
+ const isOnline = onlinePeers.has(val.PublicKey);
329
+ console.log(` ${isOnline ? '[ONLINE]' : '[OFFLINE]'} ${name} ${val.AllowedIPs}`);
330
+ }
331
+ console.log('');
332
+ }
333
+
334
+ /**
335
+ * Remove a peer by name
336
+ * @param {string} name - Peer name to remove
337
+ */
338
+ async function removePeer(name) {
339
+ await init();
340
+ const config = parse(readFileSync(CONFIG_PATH, 'utf-8'));
341
+ const peerKey = Object.keys(config).find(k => k === `Peer #${name}` || config[k]?.PublicKey === name);
342
+
343
+ if (!peerKey) {
344
+ console.log(`[ERROR] Peer not found: ${name}`);
345
+ return;
346
+ }
347
+
348
+ delete config[peerKey];
349
+ saveConfig(config);
350
+ console.log(`[OK] Removed: ${name}`);
351
+ }
352
+
353
+ /**
354
+ * Show help message
355
+ */
356
+ function showHelp() {
357
+ console.log(`
358
+ wgm - Simplified WireGuard Peer Management
359
+
360
+ Usage:
361
+ wgm add Add a new peer interactively
362
+ wgm list List all peers
363
+ wgm rm <name> Remove a peer
364
+ wgm -i <name> Specify interface (default: auto-detect)
365
+
366
+ Examples:
367
+ wgm add
368
+ wgm -i wg1 add
369
+ wgm list
370
+ wgm rm laptop
371
+
372
+ Requirements:
373
+ - WireGuard installed
374
+ - Node.js >= 16
375
+ - Root privileges
376
+ `);
377
+ }
378
+
379
+ /**
380
+ * Main entry point
381
+ */
382
+ async function main() {
383
+ const cmd = process.argv[2];
384
+
385
+ if (cmd === 'add') {
386
+ await addPeer();
387
+ } else if (cmd === 'list' || cmd === 'ls') {
388
+ await listPeers();
389
+ } else if (cmd === 'rm' || cmd === 'remove') {
390
+ const name = process.argv[3];
391
+ if (name) await removePeer(name);
392
+ else console.log('Usage: wgm rm <name>');
393
+ } else if (cmd === '-h' || cmd === '--help' || cmd === '-v' || cmd === '--version') {
394
+ showHelp();
395
+ } else {
396
+ showHelp();
397
+ }
398
+ }
399
+
400
+ main();
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "wgm",
3
+ "version": "1.0.0",
4
+ "description": "Simplified WireGuard Peer Management Tool",
5
+ "main": "bin/wgm",
6
+ "bin": {
7
+ "wgm": "./bin/wgm"
8
+ },
9
+ "type": "module",
10
+ "scripts": {
11
+ "start": "node bin/wgm"
12
+ },
13
+ "keywords": ["wireguard", "vpn", "peer", "cli", "wireguard-manager"],
14
+ "license": "MIT",
15
+ "dependencies": {
16
+ "inquirer": "^9.2.0",
17
+ "qrcode": "^1.5.3",
18
+ "ini": "^4.1.0"
19
+ },
20
+ "engines": {
21
+ "node": ">=16.0.0"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/wgm/wgm"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/wgm/wgm/issues"
29
+ },
30
+ "homepage": "https://github.com/wgm/wgm#readme"
31
+ }