-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathProgram.cs
More file actions
372 lines (316 loc) · 15.1 KB
/
Copy pathProgram.cs
File metadata and controls
372 lines (316 loc) · 15.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
using Microsoft.Extensions.ObjectPool;
using Renci.SshNet;
namespace wg_show_dump_API
{
public class Program
{
//I know, I know... This probably shouldn't be "static."
static List<PeerInfo> peerInfos = new List<PeerInfo>();
static DateTime lastRefresh = DateTime.MinValue;
static object refreshLockObject = new Object();
static string? ip = Environment.GetEnvironmentVariable("SSH_IP");
static string? username = Environment.GetEnvironmentVariable("SSH_USERNAME");
static string? password = Environment.GetEnvironmentVariable("SSH_PASSWORD");
static string? wgCommand = Environment.GetEnvironmentVariable("SSH_WG_COMMAND");
static int? minRefreshTime;
static SshClient? client = null;
static object sshClientLockObject = new Object();
public static void Main(string[] args)
{
//Validate environment variables
if (ip == null)
{
Console.WriteLine("[WARN] No SSH IP provided. Defaulting to 127.0.0.1\nPlease use the SSH_IP environment variable to provide one.");
ip = "127.0.0.1";
}
if (username == null)
{
Console.WriteLine("[WARN] No SSH username provided. Defaulting to root\nPlease use the SSH_USERNAME environment variable to provide one.");
username = "root";
}
if (password == null)
//If no password provided, look for key file
if (!File.Exists("key"))
{
//If neither, fatal error
Console.WriteLine("[FATAL] Neither a key file nor a password were provided!");
Environment.Exit(1);
}
else
Console.WriteLine("[INFO] Connecting SSH client using key file...");
if (wgCommand == null)
{
Console.WriteLine("[WARN] No SSH command provided. Defaulting to \"wg show all dump\"\nYou may use the SSH_WG_COMMAND environment variable to configure one if you run WireGuard in a Docker container.");
wgCommand = "wg show all dump";
}
string? minRefreshString = Environment.GetEnvironmentVariable("SSH_MIN_REFRESH");
if (minRefreshString != null)
try
{
minRefreshTime = Convert.ToInt32(minRefreshString);
}
catch
{
Console.WriteLine("[WARN] No minimum refresh time provided. Defaulting to 10 seconds.\nYou may use use the SSH_MIN_REFRESH environment variable to configure this.");
minRefreshTime = 10;
}
Console.WriteLine("[INFO] wg-show-dump-API will use these setting:");
Console.WriteLine("[INFO] IP: " + ip);
Console.WriteLine("[INFO] Username: " + username);
Console.WriteLine("[INFO] Authentication Method: " + ((password == null) ? "Keyfile" : "Password"));
Console.WriteLine("[INFO] Command: " + wgCommand);
Console.WriteLine("[INFO] Minimum Refresh: " + minRefreshTime.ToString() + " seconds");
//Web App init
Console.WriteLine("[INFO] Creating WebApplication...");
var builder = WebApplication.CreateBuilder();
var app = builder.Build();
//Answer on /peer
Console.WriteLine("[INFO] Mapping /peer...");
app.MapGet("/peer", (string id) =>
{
Console.WriteLine("[INFO] Connection received!");
//Hacky, but feeding an ID treats pluses as spaces. So we'll intentionally treat spaces as pluses.
id = id.Replace(" ", "+");
//Grab peer info
Console.WriteLine("[INFO] Gathering peer information...");
PeerInfo peerInfo = getPeerInfoById(id);
//Send the info
return new
{
interfaceName = peerInfo.interfaceName,
publicKey = peerInfo.publicKey,
presharedKey = peerInfo.presharedKey,
endpoint = peerInfo.endpoint,
allowedIPs = peerInfo.allowedIPs,
latestHandshake = peerInfo.latestHandshake,
transferRx = peerInfo.transferRx,
transferTx = peerInfo.transferTx,
persistentKeepAlive = peerInfo.persistentKeepAlive
};
});
Console.WriteLine("[INFO] Mapping /peers");
app.MapGet("/peers", () =>
{
//Grab peer info
Console.WriteLine("[INFO] Gathering peer information...");
updatePeerInfos();
//Pack peerInfos into an object array to return to client
object[] peerInfoObjects = new object[peerInfos.Count];
for(int i = 0; i < peerInfos.Count; i++)
{
PeerInfo peerInfo = peerInfos[i];
peerInfoObjects[i] = new
{
interfaceName = peerInfo.interfaceName,
publicKey = peerInfo.publicKey,
presharedKey = peerInfo.presharedKey,
endpoint = peerInfo.endpoint,
allowedIPs = peerInfo.allowedIPs,
latestHandshake = peerInfo.latestHandshake,
transferRx = peerInfo.transferRx,
transferTx = peerInfo.transferTx,
persistentKeepAlive = peerInfo.persistentKeepAlive
};
}
Console.WriteLine("[INFO] Returning info for {0} peer(s).", peerInfos.Count);
return peerInfoObjects;
});
//Build the SSH client
validateSshClient();
PeriodicTimer saveTimer = new PeriodicTimer(TimeSpan.FromSeconds(15));
Task.Run(async () =>
{
while (await saveTimer.WaitForNextTickAsync())
{
//We can keep the SSH session alive
validateSshClient();
}
});
//Begin app on port 6543
Console.WriteLine("[INFO] Starting server...");
app.Run("http://0.0.0.0:6543");
}
static void validateSshClient(int recursiveDelay = 1)
{
lock (sshClientLockObject)
{
try
{
if (client == null)
{
Console.WriteLine("[INFO] Creating SSH client...");
if (password == null)
{
//If no password provided, look for key file
if (File.Exists("key"))
{
Console.WriteLine("[INFO] Connecting SSH client using key file...");
var keyFile = new PrivateKeyFile("key");
client = new SshClient(ip, username, keyFile);
}
else
{
//If neither, fatal error
Console.WriteLine("[FATAL] Neither a key file nor a password were provided!");
Environment.Exit(1);
}
}
else
{
//Otherwise, use provided password
Console.WriteLine("[INFO] Connecting SSH client using password...");
client = new SshClient(ip, username, password);
}
//Connect
client.Connect();
if (client.IsConnected)
Console.WriteLine("[INFO] SSH client connected successfully!");
else
Console.WriteLine("[ERROR] SSH client failed to connect!");
}
else
if (!client.IsConnected)
{
client.Dispose();
Console.WriteLine("[ERROR] SSH client not connected!");
}
}
catch
{
Console.WriteLine("[ERROR] Something went wrong with the SSH client!");
Console.WriteLine("[ERROR] Trying again in {0} seconds...!", recursiveDelay);
Thread.Sleep(recursiveDelay * 1000);
recursiveDelay *= 2;
if (recursiveDelay > 64)
recursiveDelay = 64;
//Dispose of client
if (client != null)
client.Dispose();
validateSshClient();
}
}
}
//Does nothing if refresh interval has not passed
//Otherwise, uses SSH to query WireGuard and parses results
static void updatePeerInfos()
{
//Use lock object to keep multiple requests from blasting SSH commands
lock (refreshLockObject)
{
//Make sure the minimum refresh time has passed since the last refresh
//Otherwise, return and force cached entries to be used
if (DateTime.Now.Subtract(lastRefresh).TotalSeconds <= minRefreshTime)
{
Console.WriteLine("[INFO] Using cached information...");
return;
}
//Record last refresh time
//This happens BEFORE parsing
//That way, users can resonably expect a 10 second min refresh to be
//10 seconds. Not 10 seconds PLUS the time it takes to refresh and parse.
lastRefresh = DateTime.Now;
Console.WriteLine("[INFO] Refreshing information...");
//Clear cached peer info
peerInfos.Clear();
try
{
//Ensure the SSH client is alive and well
validateSshClient();
//Send wg dump command (IE: "wg show all dump" or "docker exec wireguard wg show all dump")
Console.WriteLine("[INFO] Running WireGuard command...");
using SshCommand cmd = client.RunCommand(wgCommand);
//"wg show all dump" returns a tab-delimited sheet
foreach (string line in cmd.Result.Split("\n"))
{
//Parse results
string[] split = line.Split("\t");
//Valid peer entries have 9 columns
if (split.Length < 9)
continue;
//The sheet does not contain a header
string interfaceName = split[0];
string publicKey = split[1];
string presharedKey = split[2];
string endpoint = split[3];
string allowedIPs = split[4];
string latestHandshake = split[5];
string transferRx = split[6];
string transferTx = split[7];
string persistentKeepAlive = split[8];
PeerInfo peerInfo = new PeerInfo();
peerInfo.interfaceName = interfaceName;
peerInfo.publicKey = publicKey;
peerInfo.presharedKey = presharedKey;
peerInfo.endpoint = endpoint;
peerInfo.allowedIPs = allowedIPs;
//If "0", it has not connected since wg started
//Keep the default value "Never" if never connected
if (latestHandshake != "0")
{
//Convert to long
long latestHandshakeLong = Convert.ToInt64(latestHandshake);
//Convert to DateTime
DateTime latestHandshakeDateTime = DateTimeOffset.FromUnixTimeSeconds(latestHandshakeLong).DateTime;
//Convert to local time
latestHandshakeDateTime = latestHandshakeDateTime.ToLocalTime();
//Convert to string and apply property
peerInfo.latestHandshake = latestHandshakeDateTime.ToString("o");
}
//Convert transferRx text to long (yes, it must be a long)
peerInfo.transferRx = Convert.ToInt64(transferRx);
//Convert transferTx text to long (yes, it must be a long)
peerInfo.transferTx = Convert.ToInt64(transferTx);
//Convert persistentKeepAlive to bool (always either "on" or "off" even for disconnected clients)
peerInfo.persistentKeepAlive = persistentKeepAlive != "off";
peerInfos.Add(peerInfo);
}
Console.WriteLine("[INFO] Parsed {0} peer(s)!", peerInfos.Count);
}
catch (Exception ex)
{
Console.WriteLine("[ERROR] Something went wrong while trying to refresh info from WireGuard!");
Console.WriteLine("[ERROR] Please report this bug!");
Console.WriteLine("[ERROR] " + ex.ToString());
Console.WriteLine("[ERROR] " + ex.Message);
}
}
}
static PeerInfo getPeerInfoById(string id)
{
updatePeerInfos();
//Locate and return the peer with the matching ID
foreach (PeerInfo peerInfo in peerInfos)
if (peerInfo.publicKey == id)
return peerInfo;
Console.WriteLine("[ERROR] Could not find matching peer!");
//No peer is found
return new PeerInfo();
}
}
class PeerInfo
{
public string interfaceName;
public string publicKey;
public string presharedKey;
public string endpoint;
public string allowedIPs;
//Using a string instead of DateTime allows us to use "Never"
//instead of 1970 for clients that have not connected since wg started
public string latestHandshake;
public long transferRx;
public long transferTx;
public bool persistentKeepAlive;
public PeerInfo()
{
interfaceName = "n/a";
publicKey = "n/a";
presharedKey = "n/a";
endpoint = "n/a";
allowedIPs = "n/a";
latestHandshake = "Never";
transferRx = 0;
transferTx = 0;
persistentKeepAlive = false;
}
}
}