Implemented ip filtering to detect datacenter and vpn ips

This commit is contained in:
Marcel Baumgartner
2023-08-29 15:03:07 +02:00
parent 5d91077d42
commit 18b7c82613
6 changed files with 194 additions and 70 deletions

View File

@@ -304,6 +304,14 @@ public class ConfigV1
public int BlockIpDuration { get; set; } = 15; public int BlockIpDuration { get; set; } = 15;
[JsonProperty("ReCaptcha")] public ReCaptchaData ReCaptcha { get; set; } = new(); [JsonProperty("ReCaptcha")] public ReCaptchaData ReCaptcha { get; set; } = new();
[JsonProperty("BlockDatacenterIps")]
[Description("If this option is enabled, users with an ip from datacenters will not be able to access the panel")]
public bool BlockDatacenterIps { get; set; } = true;
[JsonProperty("AllowCloudflareIps")]
[Description("Allow cloudflare ips to bypass the datacenter ip check")]
public bool AllowCloudflareIps { get; set; } = false;
} }
public class ReCaptchaData public class ReCaptchaData

View File

@@ -15,14 +15,14 @@ public class StorageService
Directory.CreateDirectory(PathBuilder.Dir("storage", "plugins")); Directory.CreateDirectory(PathBuilder.Dir("storage", "plugins"));
await UpdateResources(); await UpdateResources();
return; return;
if(IsEmpty(PathBuilder.Dir("storage", "resources"))) if (IsEmpty(PathBuilder.Dir("storage", "resources")))
{ {
Logger.Info("Default resources not found. Copying default resources"); Logger.Info("Default resources not found. Copying default resources");
CopyFilesRecursively( CopyFilesRecursively(
PathBuilder.Dir("defaultstorage", "resources"), PathBuilder.Dir("defaultstorage", "resources"),
PathBuilder.Dir("storage", "resources") PathBuilder.Dir("storage", "resources")
); );
} }
@@ -30,9 +30,9 @@ public class StorageService
if (IsEmpty(PathBuilder.Dir("storage", "configs"))) if (IsEmpty(PathBuilder.Dir("storage", "configs")))
{ {
Logger.Info("Default configs not found. Copying default configs"); Logger.Info("Default configs not found. Copying default configs");
CopyFilesRecursively( CopyFilesRecursively(
PathBuilder.Dir("defaultstorage", "configs"), PathBuilder.Dir("defaultstorage", "configs"),
PathBuilder.Dir("storage", "configs") PathBuilder.Dir("storage", "configs")
); );
} }
@@ -40,64 +40,73 @@ public class StorageService
private async Task UpdateResources() private async Task UpdateResources()
{ {
Logger.Info("Checking resources"); try
var client = new GitHubClient(
new ProductHeaderValue("Moonlight-Panel"));
string user = "Moonlight-Panel";
string repo = "Resources";
string branch = "main";
string resourcesDir = PathBuilder.Dir("storage", "resources");
async Task CopyDirectory(string dirPath, string localDir)
{ {
IReadOnlyList<RepositoryContent> contents; Logger.Info("Checking resources");
if(string.IsNullOrEmpty(dirPath))
contents = await client.Repository.Content.GetAllContents(user, repo);
else
contents = await client.Repository.Content.GetAllContents(user, repo, dirPath);
foreach (var content in contents) var client = new GitHubClient(
new ProductHeaderValue("Moonlight-Panel"));
var user = "Moonlight-Panel";
var repo = "Resources";
var resourcesDir = PathBuilder.Dir("storage", "resources");
async Task CopyDirectory(string dirPath, string localDir)
{ {
string localPath = Path.Combine(localDir, content.Name); IReadOnlyList<RepositoryContent> contents;
if (content.Type == ContentType.File) if (string.IsNullOrEmpty(dirPath))
contents = await client.Repository.Content.GetAllContents(user, repo);
else
contents = await client.Repository.Content.GetAllContents(user, repo, dirPath);
foreach (var content in contents)
{ {
if(content.Name.EndsWith(".gitattributes")) string localPath = Path.Combine(localDir, content.Name);
continue;
if(File.Exists(localPath) && !content.Name.EndsWith(".lang"))
continue;
if (content.Name.EndsWith(".lang") && File.Exists(localPath) && if (content.Type == ContentType.File)
new FileInfo(localPath).Length == content.Size)
{ {
Logger.Info($"Skipped language file '{content.Name}'"); if (content.Name.EndsWith(".gitattributes"))
continue; continue;
}
var fileContent = await client.Repository.Content.GetRawContent(user, repo, content.Path);
Directory.CreateDirectory(localDir); // Ensure the directory exists
await File.WriteAllBytesAsync(localPath, fileContent);
Logger.Debug($"Synced file '{content.Path}'"); if (File.Exists(localPath) && !content.Name.EndsWith(".lang"))
} continue;
else if (content.Type == ContentType.Dir)
{ if (content.Name.EndsWith(".lang") && File.Exists(localPath) &&
await CopyDirectory(content.Path, localPath); new FileInfo(localPath).Length == content.Size)
continue;
var fileContent = await client.Repository.Content.GetRawContent(user, repo, content.Path);
Directory.CreateDirectory(localDir); // Ensure the directory exists
await File.WriteAllBytesAsync(localPath, fileContent);
Logger.Debug($"Synced file '{content.Path}'");
}
else if (content.Type == ContentType.Dir)
{
await CopyDirectory(content.Path, localPath);
}
} }
} }
await CopyDirectory("", resourcesDir);
}
catch (RateLimitExceededException)
{
Logger.Warn("Unable to sync resources due to your ip being rate-limited by github");
}
catch (Exception e)
{
Logger.Warn("Unable to sync resources");
Logger.Warn(e);
} }
await CopyDirectory("", resourcesDir);
} }
private bool IsEmpty(string path) private bool IsEmpty(string path)
{ {
return !Directory.EnumerateFileSystemEntries(path).Any(); return !Directory.EnumerateFileSystemEntries(path).Any();
} }
private static void CopyFilesRecursively(string sourcePath, string targetPath) private static void CopyFilesRecursively(string sourcePath, string targetPath)
{ {
//Now Create all of the directories //Now Create all of the directories
@@ -107,7 +116,7 @@ public class StorageService
} }
//Copy all the files & Replaces any files with the same name //Copy all the files & Replaces any files with the same name
foreach (string newPath in Directory.GetFiles(sourcePath, "*.*",SearchOption.AllDirectories)) foreach (string newPath in Directory.GetFiles(sourcePath, "*.*", SearchOption.AllDirectories))
{ {
File.Copy(newPath, newPath.Replace(sourcePath, targetPath), true); File.Copy(newPath, newPath.Replace(sourcePath, targetPath), true);
} }

View File

@@ -0,0 +1,91 @@
using Moonlight.App.Helpers;
using Whois.NET;
namespace Moonlight.App.Services;
public class IpVerificationService
{
private readonly ConfigService ConfigService;
public IpVerificationService(ConfigService configService)
{
ConfigService = configService;
}
public async Task<bool> IsDatacenterOrVpn(string ip)
{
if (!ConfigService.Get().Moonlight.Security.BlockDatacenterIps)
return false;
if (string.IsNullOrEmpty(ip))
return false;
var datacenterNames = new List<string>()
{
"amazon",
"aws",
"microsoft",
"azure",
"google",
"google cloud",
"gcp",
"digitalocean",
"linode",
"vultr",
"ovh",
"ovhcloud",
"alibaba",
"oracle",
"ibm cloud",
"bluehost",
"godaddy",
"rackpace",
"hetzner",
"tencent",
"scaleway",
"softlayer",
"dreamhost",
"a2 hosting",
"inmotion hosting",
"red hat openstack",
"kamatera",
"hostgator",
"siteground",
"greengeeks",
"liquidweb",
"joyent",
"aruba",
"interoute",
"fastcomet",
"rosehosting",
"lunarpages",
"fatcow",
"jelastic",
"datacamp"
};
if(!ConfigService.Get().Moonlight.Security.AllowCloudflareIps)
datacenterNames.Add("cloudflare");
try
{
var response = await WhoisClient.QueryAsync(ip);
var responseText = response.Raw.ToLower();
foreach (var name in datacenterNames)
{
if (responseText.Contains(name))
{
Logger.Debug(name);
return true;
}
}
}
catch (Exception)
{
return false;
}
return false;
}
}

View File

@@ -55,6 +55,7 @@
<PackageReference Include="SSH.NET" Version="2020.0.2" /> <PackageReference Include="SSH.NET" Version="2020.0.2" />
<PackageReference Include="Stripe.net" Version="41.23.0-beta.1" /> <PackageReference Include="Stripe.net" Version="41.23.0-beta.1" />
<PackageReference Include="UAParser" Version="3.1.47" /> <PackageReference Include="UAParser" Version="3.1.47" />
<PackageReference Include="WhoisClient.NET" Version="5.0.0" />
<PackageReference Include="XtermBlazor" Version="1.8.1" /> <PackageReference Include="XtermBlazor" Version="1.8.1" />
</ItemGroup> </ItemGroup>

View File

@@ -220,6 +220,7 @@ namespace Moonlight
builder.Services.AddScoped<TicketClientService>(); builder.Services.AddScoped<TicketClientService>();
builder.Services.AddScoped<TicketAdminService>(); builder.Services.AddScoped<TicketAdminService>();
builder.Services.AddScoped<MalwareScanService>(); builder.Services.AddScoped<MalwareScanService>();
builder.Services.AddSingleton<IpVerificationService>();
builder.Services.AddScoped<SessionClientService>(); builder.Services.AddScoped<SessionClientService>();
builder.Services.AddSingleton<SessionServerService>(); builder.Services.AddSingleton<SessionServerService>();

View File

@@ -22,6 +22,7 @@
@inject DynamicBackgroundService DynamicBackgroundService @inject DynamicBackgroundService DynamicBackgroundService
@inject KeyListenerService KeyListenerService @inject KeyListenerService KeyListenerService
@inject ConfigService ConfigService @inject ConfigService ConfigService
@inject IpVerificationService IpVerificationService
@{ @{
var uri = new Uri(NavigationManager.Uri); var uri = new Uri(NavigationManager.Uri);
@@ -46,9 +47,35 @@
<DefaultLayout> <DefaultLayout>
<SoftErrorBoundary> <SoftErrorBoundary>
@if (!IsIpBanned) @if (UserProcessed)
{ {
if (UserProcessed) if (IsIpBanned)
{
<div class="modal d-block">
<div class="modal-dialog modal-dialog-centered mw-900px">
<div class="modal-content">
<div class="pt-2 modal-body py-lg-10 px-lg-10">
<h2>@(SmartTranslateService.Translate("Your ip has been banned"))</h2>
<p class="mt-3 fw-normal fs-6">@(SmartTranslateService.Translate("Your ip address has been banned by an admin"))</p>
</div>
</div>
</div>
</div>
}
else if (IsIpSuspicious)
{
<div class="modal d-block">
<div class="modal-dialog modal-dialog-centered mw-900px">
<div class="modal-content">
<div class="pt-2 modal-body py-lg-10 px-lg-10">
<h2>@(SmartTranslateService.Translate("Your ip his blocked. VPNs and Datacenter IPs are prohibited from accessing this site"))</h2>
<p class="mt-3 fw-normal fs-6">@(SmartTranslateService.Translate("Please disable your vpn or proxy and try it again"))</p>
</div>
</div>
</div>
</div>
}
else
{ {
if (uri.LocalPath != "/login" && if (uri.LocalPath != "/login" &&
uri.LocalPath != "/passwordreset" && uri.LocalPath != "/passwordreset" &&
@@ -102,19 +129,6 @@
} }
} }
} }
else
{
<div class="modal d-block">
<div class="modal-dialog modal-dialog-centered mw-900px">
<div class="modal-content">
<div class="pt-2 modal-body py-lg-10 px-lg-10">
<h2>@(SmartTranslateService.Translate("Authenticating"))...</h2>
<p class="mt-3 fw-normal fs-6">@(SmartTranslateService.Translate("Verifying token, loading user data"))</p>
</div>
</div>
</div>
</div>
}
} }
else else
{ {
@@ -122,8 +136,8 @@
<div class="modal-dialog modal-dialog-centered mw-900px"> <div class="modal-dialog modal-dialog-centered mw-900px">
<div class="modal-content"> <div class="modal-content">
<div class="pt-2 modal-body py-lg-10 px-lg-10"> <div class="pt-2 modal-body py-lg-10 px-lg-10">
<h2>@(SmartTranslateService.Translate("Your ip has been banned"))</h2> <h2>@(SmartTranslateService.Translate("Authenticating"))...</h2>
<p class="mt-3 fw-normal fs-6">@(SmartTranslateService.Translate("Your ip address has been banned by an admin"))</p> <p class="mt-3 fw-normal fs-6">@(SmartTranslateService.Translate("Verifying token, loading user data"))</p>
</div> </div>
</div> </div>
</div> </div>
@@ -137,6 +151,7 @@
{ {
private bool UserProcessed = false; private bool UserProcessed = false;
private bool IsIpBanned = false; private bool IsIpBanned = false;
private bool IsIpSuspicious = false;
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
@@ -146,11 +161,6 @@
{ {
DynamicBackgroundService.OnBackgroundImageChanged += async (_, _) => { await InvokeAsync(StateHasChanged); }; DynamicBackgroundService.OnBackgroundImageChanged += async (_, _) => { await InvokeAsync(StateHasChanged); };
IsIpBanned = await IpBanService.IsBanned();
if (IsIpBanned)
await InvokeAsync(StateHasChanged);
await Event.On<Object>("ipBan.update", this, async _ => await Event.On<Object>("ipBan.update", this, async _ =>
{ {
IsIpBanned = await IpBanService.IsBanned(); IsIpBanned = await IpBanService.IsBanned();
@@ -158,6 +168,10 @@
}); });
await IdentityService.Load(); await IdentityService.Load();
IsIpBanned = await IpBanService.IsBanned();
IsIpSuspicious = await IpVerificationService.IsDatacenterOrVpn(IdentityService.Ip);
UserProcessed = true; UserProcessed = true;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);