Compare commits

...
Sign in to create a new pull request.

5 commits
main ... vpn

Author SHA1 Message Date
Pat Hartl
d1ed6f17c9 Added VPN controller routes, add ZT settings 2024-03-23 11:45:45 -05:00
Pat Hartl
656a3c2dae Add functionality to talk to local ZT1 service 2024-03-20 19:32:46 -05:00
Pat Hartl
716fc98f8a Change initial integer value of VPN type 2024-03-20 19:20:40 -05:00
Pat Hartl
5e01835826 Added configuration page in settings 2024-03-20 19:19:59 -05:00
Pat Hartl
9ad2ce50e3 Start ZeroTier One integration
Starting the scaffolding of controlling the ZeroTier One Windows service to connect to a network.
2024-03-16 14:03:06 -05:00
26 changed files with 787 additions and 1 deletions

View file

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace LANCommander.SDK.Enums
{
public enum VPNStatus
{
Disconnected,
Connected,
}
}

View file

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace LANCommander.SDK.Enums
{
public enum VPNType
{
ZeroTier = 1
}
}

View file

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace LANCommander.SDK.VPN.Clients
{
public interface IVPNClient
{
bool Connect();
bool Disconnect();
VPNConnectionStatus GetStatus();
}
}

View file

@ -0,0 +1,174 @@
using LANCommander.SDK.VPN.Configurations;
using LANCommander.SDK.VPN.Exceptions;
using LANCommander.SDK.VPN.Models.ZeroTier;
using RestSharp;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
namespace LANCommander.SDK.VPN.Clients
{
public class ZeroTierClient : IVPNClient
{
private ZeroTierConfiguration Configuration;
private RestClient ZeroTierServiceClient;
private Client Client;
private string Token;
private const string BaseUrl = "http://localhost:9993";
public ZeroTierClient(ZeroTierConfiguration configuration, Client client) {
Configuration = configuration;
Client = client;
ZeroTierServiceClient = new RestClient(BaseUrl);
var tokenPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "ZeroTier", "One", "authtoken.secret");
if (File.Exists(tokenPath))
Token = File.ReadAllText(tokenPath);
else
throw new ClientInitializationException("Could not read the token");
}
public bool Connect()
{
try
{
var task = PostRequest<UpdateNetworkSettingsResponse>($"/network/{Configuration.NetworkId}", new UpdateNetworkSettingsRequest
{
AllowDefault = false,
AllowDNS = true,
AllowManaged = true,
AllowGlobal = false
});
task.Wait();
var response = task.Result;
Client.PostRequest<ApproveNodeRequest>("/api/ZeroTier/ApproveNode", new ApproveNodeRequest
{
NodeId = response.Id
});
}
catch (Exception ex)
{
return false;
}
return true;
}
public bool Disconnect()
{
try
{
var status = GetNodeStatus();
var task = DeleteRequest<LeaveNetworkResponse>($"/network/{Configuration.NetworkId}");
task.Wait();
var response = task.Result;
if (response.Result)
{
Client.PostRequest<ApproveNodeRequest>("/api/ZeroTier/RemoveNode", new ApproveNodeRequest
{
NodeId = status.Address
});
}
return response.Result;
}
catch (Exception ex)
{
return false;
}
}
public VPNConnectionStatus GetStatus()
{
try
{
var response = GetNodeStatus();
return new VPNConnectionStatus
{
Connected = response.Online,
Status = response.Online ? "Connected" : "Not Connected"
};
}
catch (Exception ex)
{
return null;
}
}
private NodeStatusResponse GetNodeStatus()
{
var task = GetRequest<NodeStatusResponse>("/status");
task.Wait();
var response = task.Result;
return response;
}
private async Task<T> PostRequest<T>(string route, object body)
{
if (Token == null)
return default;
var request = new RestRequest(route)
.AddJsonBody(body)
.AddHeader("X-ZT1-AUTH", Token);
var response = await ZeroTierServiceClient.PostAsync<T>(request);
return response;
}
private async Task<T> PostRequest<T>(string route)
{
if (Token == null)
return default;
var request = new RestRequest(route)
.AddHeader("X-ZT1-AUTH", Token);
var response = await ZeroTierServiceClient.PostAsync<T>(request);
return response;
}
private async Task<T> GetRequest<T>(string route)
{
if (Token == null)
return default;
var request = new RestRequest(route)
.AddHeader("X-ZT1-AUTH", Token);
var response = await ZeroTierServiceClient.GetAsync<T>(request);
return response;
}
private async Task<T> DeleteRequest<T>(string route)
{
if (Token == null)
return default;
var request = new RestRequest(route)
.AddHeader("X-ZT1-AUTH", Token);
var response = await ZeroTierServiceClient.DeleteAsync<T>(request);
return response;
}
}
}

View file

@ -0,0 +1,13 @@
using LANCommander.SDK.Enums;
using System;
using System.Collections.Generic;
using System.Text;
namespace LANCommander.SDK.VPN.Configurations
{
public class VPNConfiguration
{
public VPNType Type { get; set; }
public object Data { get; set; }
}
}

View file

@ -0,0 +1,12 @@
using LANCommander.SDK.Enums;
using System;
using System.Collections.Generic;
using System.Text;
namespace LANCommander.SDK.VPN.Configurations
{
public class ZeroTierConfiguration
{
public string NetworkId { get; set; }
}
}

View file

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace LANCommander.SDK.VPN
{
public class VPNConnectionStatus
{
public bool Connected { get; set; }
public string Status { get; set; }
}
}

View file

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace LANCommander.SDK.VPN.Exceptions
{
public class ClientConnectionException : Exception
{
public ClientConnectionException(string message) : base(message) { }
}
}

View file

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace LANCommander.SDK.VPN.Exceptions
{
public class ClientInitializationException : Exception
{
public ClientInitializationException(string message) : base(message) { }
}
}

View file

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace LANCommander.SDK.VPN.Exceptions.ZeroTier
{
public class ServiceNotInstalledException : Exception
{
public ServiceNotInstalledException(string message) : base(message) { }
}
}

View file

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace LANCommander.SDK.VPN.Models.ZeroTier
{
public class ApproveNodeRequest
{
public string NodeId { get; set; }
}
}

View file

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace LANCommander.SDK.VPN.Models.ZeroTier
{
public class LeaveNetworkRequest
{
public string NetworkId { get; set; }
}
}

View file

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace LANCommander.SDK.VPN.Models.ZeroTier
{
public class LeaveNetworkResponse
{
public bool Result { get; set; }
}
}

View file

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace LANCommander.SDK.VPN.Models.ZeroTier
{
public class NodeStatusResponse
{
public string Address { get; set; }
public bool Online { get; set; }
public bool TcpFallbackActive { get; set; }
}
}

View file

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace LANCommander.SDK.VPN.Models.ZeroTier
{
public class RemoveNodeRequest
{
public string NodeId { get; set; }
}
}

View file

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace LANCommander.SDK.VPN.Models.ZeroTier
{
public class UpdateNetworkSettingsRequest
{
public bool AllowDNS { get; set; }
public bool AllowDefault { get; set; }
public bool AllowManaged { get; set; }
public bool AllowGlobal { get; set; }
}
}

View file

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace LANCommander.SDK.VPN.Models.ZeroTier
{
public class UpdateNetworkSettingsResponse
{
public string Id { get; set; }
}
}

View file

@ -0,0 +1,63 @@
using LANCommander.SDK.VPN;
using LANCommander.SDK.VPN.Clients;
using LANCommander.SDK.VPN.Configurations;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace LANCommander.SDK
{
public class VPNService
{
private readonly ILogger Logger;
private Client Client { get; set; }
public delegate void OnConnectedHandler(object sender);
public event OnConnectedHandler OnConnected;
public delegate void OnDisconnectedHandler(object sender);
public event OnDisconnectedHandler OnDisconnected;
private IVPNClient VPNClient;
public VPNService(Client client)
{
Client = client;
}
public VPNService(Client client, ILogger logger)
{
Client = client;
Logger = logger;
}
public void Init()
{
var configuration = Client.GetRequest<VPNConfiguration>("/api/VPN/Configuration");
switch (configuration.Type)
{
case Enums.VPNType.ZeroTier:
VPNClient = new ZeroTierClient(configuration.Data as ZeroTierConfiguration, Client);
break;
}
}
public bool Connect()
{
return VPNClient.Connect();
}
public bool Disconnect()
{
return VPNClient.Disconnect();
}
public VPNConnectionStatus GetStatus()
{
return VPNClient.GetStatus();
}
}
}

View file

@ -0,0 +1,38 @@
using LANCommander.Models;
using LANCommander.SDK.VPN.Configurations;
using LANCommander.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LANCommander.Controllers.Api
{
[Authorize(AuthenticationSchemes = "Bearer")]
[Route("api/[controller]")]
[ApiController]
public class VPNController : ControllerBase
{
public VPNController() { }
[HttpGet]
public async Task<VPNConfiguration> GetConfiguration()
{
var settings = SettingService.GetSettings();
var configuration = new VPNConfiguration()
{
Type = SDK.Enums.VPNType.ZeroTier
};
switch (settings.VPN.Type)
{
case SDK.Enums.VPNType.ZeroTier:
configuration.Data = new ZeroTierConfiguration
{
NetworkId = (settings.VPN.Configuration as LANCommanderZeroTierSettings).NetworkId
};
break;
}
return configuration;
}
}
}

View file

@ -0,0 +1,74 @@
using LANCommander.Data.Models;
using LANCommander.Models;
using LANCommander.SDK.VPN.Models.ZeroTier;
using LANCommander.Services;
using LANCommander.Services.VPNServices;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
namespace LANCommander.Controllers.Api
{
[Authorize(AuthenticationSchemes = "Bearer")]
[Route("api/[controller]")]
[ApiController]
public class ZeroTierController : ControllerBase
{
private readonly ZeroTierService ZeroTierService;
private readonly UserManager<User> UserManager;
private readonly RoleManager<Role> RoleManager;
public ZeroTierController(ZeroTierService zeroTierService, UserManager<User> userManager, RoleManager<Role> roleManager)
{
ZeroTierService = zeroTierService;
UserManager = userManager;
RoleManager = roleManager;
}
[HttpPost]
public async Task<IActionResult> ApproveNode(ApproveNodeRequest request)
{
var settings = SettingService.GetSettings();
var user = await UserManager.FindByNameAsync(User.Identity.Name);
var roleNames = await UserManager.GetRolesAsync(user);
bool allowed = false;
foreach (var roleName in roleNames)
{
var role = await RoleManager.FindByNameAsync(roleName);
if (role != null && settings.VPN.AllowedRoles.Contains(role.Id))
{
allowed = true;
break;
}
}
if (!allowed)
return Forbid();
var config = settings.VPN.Configuration as LANCommanderZeroTierSettings;
await ZeroTierService.ApproveNode(config.NetworkId, user.UserName);
return Ok();
}
[HttpPost]
public async Task<IActionResult> RemoveNode(RemoveNodeRequest request)
{
var settings = SettingService.GetSettings();
var user = await UserManager.FindByNameAsync(User.Identity.Name);
// Do we add any check here that the user is authorized to do VPN stuff?
// We should at least do a check that they own that
await ZeroTierService.RemoveNode(request.NodeId);
return Ok();
}
}
}

View file

@ -1,4 +1,5 @@
using NLog.Targets;
using LANCommander.SDK.Enums;
using NLog.Targets;
namespace LANCommander.Models
{
@ -23,6 +24,7 @@ namespace LANCommander.Models
public LANCommanderArchiveSettings Archives { get; set; } = new LANCommanderArchiveSettings();
public LANCommanderMediaSettings Media { get; set; } = new LANCommanderMediaSettings();
public LANCommanderIPXRelaySettings IPXRelay { get; set; } = new LANCommanderIPXRelaySettings();
public LANCommanderVPNSettings VPN { get; set; } = new LANCommanderVPNSettings();
public LANCommanderWikiSettings Wiki { get; set; } = new LANCommanderWikiSettings();
public LANCommanderUpdateSettings Update { get; set; } = new LANCommanderUpdateSettings();
public LANCommanderLogSettings Logs { get; set; } = new LANCommanderLogSettings();
@ -90,6 +92,20 @@ namespace LANCommander.Models
public bool Logging { get; set; } = false;
}
public class LANCommanderVPNSettings
{
public bool Enabled { get; set; } = false;
public IEnumerable<Guid> AllowedRoles { get; set; }
public VPNType Type { get; set; } = VPNType.ZeroTier;
public object Configuration { get; set; }
}
public class LANCommanderZeroTierSettings
{
public string NetworkId { get; set; }
public string ApiKey { get; set; }
}
public class LANCommanderWikiSettings
{
public bool Enabled { get; set; } = false;

View file

@ -0,0 +1,8 @@
namespace LANCommander.Models.ZeroTier
{
public class MemberConfig
{
public bool ActiveBridge { get; set; }
public bool Authorized { get; set; }
}
}

View file

@ -0,0 +1,10 @@
namespace LANCommander.Models.ZeroTier
{
public class ModifyMemberRequest
{
public bool Hidden { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public MemberConfig Config { get; set; }
}
}

View file

@ -0,0 +1,92 @@
using LANCommander.Models;
using LANCommander.Models.ZeroTier;
using RestSharp;
namespace LANCommander.Services.VPNServices
{
public class ZeroTierService
{
private const string BaseUrl = "https://api.zerotier.com/api/v1";
private RestClient Client { get; set; }
private LANCommanderZeroTierSettings Configuration { get; set; }
public ZeroTierService() {
Client = new RestClient(BaseUrl);
var settings = SettingService.GetSettings();
Configuration = settings.VPN.Configuration as LANCommanderZeroTierSettings;
}
public async Task ApproveNode(string nodeId, string username)
{
await PostRequestAsync<object>($"/network/{Configuration.NetworkId}/member/{nodeId}", new ModifyMemberRequest
{
Hidden = false,
Name = username,
Config = new MemberConfig
{
Authorized = true
}
});
}
public async Task RemoveNode(string nodeId)
{
await DeleteRequest<object>($"/network/{Configuration.NetworkId}/member/{nodeId}");
}
private async Task<T> PostRequestAsync<T>(string route, object body)
{
if (String.IsNullOrWhiteSpace(Configuration.ApiKey))
return default;
var request = new RestRequest(route)
.AddJsonBody(body)
.AddHeader("Authorization", $"token {Configuration.ApiKey}");
var response = await Client.PostAsync<T>(request);
return response;
}
private async Task<T> PostRequestAsync<T>(string route)
{
if (String.IsNullOrWhiteSpace(Configuration.ApiKey))
return default;
var request = new RestRequest(route)
.AddHeader("Authorization", $"token {Configuration.ApiKey}");
var response = await Client.PostAsync<T>(request);
return response;
}
private async Task<T> GetRequest<T>(string route)
{
if (String.IsNullOrWhiteSpace(Configuration.ApiKey))
return default;
var request = new RestRequest(route)
.AddHeader("Authorization", $"token {Configuration.ApiKey}");
var response = await Client.GetAsync<T>(request);
return response;
}
private async Task<T> DeleteRequest<T>(string route)
{
if (String.IsNullOrWhiteSpace(Configuration.ApiKey))
return default;
var request = new RestRequest(route)
.AddHeader("Authorization", $"token {Configuration.ApiKey}");
var response = await Client.DeleteAsync<T>(request);
return response;
}
}
}

View file

@ -0,0 +1,28 @@
@using LANCommander.Models
@using LANCommander.SDK.VPN.Configurations
<FormItem Label="Network ID">
<Input @bind-Value="Configuration.NetworkId" />
</FormItem>
@code {
[Parameter] public string Value { get; set; } = "";
[Parameter] public EventCallback<string> ValueChanged { get; set; }
[Parameter] public LANCommanderZeroTierSettings Configuration { get; set; } = new LANCommanderZeroTierSettings();
[Parameter] public EventCallback<LANCommanderZeroTierSettings> ConfigurationChanged { get; set; }
protected override async Task OnInitializedAsync()
{
if (Configuration == null)
{
Configuration = new LANCommanderZeroTierSettings();
await ConfigurationChanged.InvokeAsync(Configuration);
}
}
async Task Changed()
{
await ConfigurationChanged.InvokeAsync(Configuration);
}
}

View file

@ -0,0 +1,95 @@
@page "/Settings/VPN"
@using LANCommander.SDK.VPN.Configurations
@using Microsoft.EntityFrameworkCore
@using LANCommander.Models;
@using LANCommander.Extensions
@using LANCommander.SDK.Enums
@layout SettingsLayout
@inject SettingService SettingService
@inject IMessageService MessageService
@inject RoleManager<Role> RoleManager
@attribute [Authorize(Roles = "Administrator")]
<PageHeader Title="VPN" />
<div style="padding: 0 24px;">
<Form Model="Settings" Layout="@FormLayout.Vertical">
<FormItem Label="Enable">
<Switch @bind-Checked="context.VPN.Enabled" />
</FormItem>
<FormItem Label="Type">
<Select @bind-Value="@context.VPN.Type" TItem="VPNType" TItemValue="VPNType" DataSource="Enum.GetValues<VPNType>()">
<LabelTemplate Context="Value">@Value.GetDisplayName()</LabelTemplate>
<ItemTemplate Context="Value">@Value.GetDisplayName()</ItemTemplate>
</Select>
</FormItem>
<FormItem Label="Allowed Roles">
<Transfer DataSource="AllowedRolesTransferItems" TargetKeys="AllowedRolesTargetKeys" OnChange="OnChangeAllowedRoles" Titles="@(new string[] { "Available", "Allowed" })" />
</FormItem>
@switch (context.VPN.Type)
{
case VPNType.ZeroTier:
<LANCommander.UI.Pages.Settings.VPN.Forms.ZeroTier Configuration="context.VPN.Configuration as LANCommanderZeroTierSettings" ConfigurationChanged="config => context.VPN.Configuration = config" />
break;
}
<FormItem>
<Button OnClick="Save" Type="@ButtonType.Primary">Save</Button>
</FormItem>
</Form>
</div>
@code {
LANCommanderSettings Settings;
ICollection<Role> Roles = new List<Role>();
IEnumerable<TransferItem> AllowedRolesTransferItems { get; set; } = new List<TransferItem>();
List<string> AllowedRolesTargetKeys { get; set; } = new List<string>();
ICollection<Role> SelectedAllowedRoles = new List<Role>();
protected override async Task OnInitializedAsync()
{
Settings = SettingService.GetSettings();
Roles = await RoleManager.Roles.ToListAsync();
SelectedAllowedRoles = Roles.Where(r => Settings.VPN.AllowedRoles.Contains(r.Id)).ToList();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
AllowedRolesTransferItems = Roles.Select(r => new TransferItem()
{
Key = r.Id.ToString(),
Title = r.Name
});
if (SelectedAllowedRoles != null)
AllowedRolesTargetKeys = SelectedAllowedRoles.Select(i => i.Id.ToString()).ToList();
}
}
async Task OnChangeAllowedRoles(TransferChangeArgs e)
{
SelectedAllowedRoles = Roles.Where(r => e.TargetKeys.Contains(r.Id.ToString())).ToList();
Settings.VPN.AllowedRoles = SelectedAllowedRoles.Select(r => r.Id);
}
private void Save()
{
try
{
SettingService.SaveSettings(Settings);
MessageService.Success("Settings saved!");
}
catch
{
MessageService.Error("An unknown error occurred.");
}
}
}