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;
[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

View File

@@ -15,14 +15,14 @@ public class StorageService
Directory.CreateDirectory(PathBuilder.Dir("storage", "plugins"));
await UpdateResources();
return;
if(IsEmpty(PathBuilder.Dir("storage", "resources")))
if (IsEmpty(PathBuilder.Dir("storage", "resources")))
{
Logger.Info("Default resources not found. Copying default resources");
CopyFilesRecursively(
PathBuilder.Dir("defaultstorage", "resources"),
PathBuilder.Dir("defaultstorage", "resources"),
PathBuilder.Dir("storage", "resources")
);
}
@@ -30,9 +30,9 @@ public class StorageService
if (IsEmpty(PathBuilder.Dir("storage", "configs")))
{
Logger.Info("Default configs not found. Copying default configs");
CopyFilesRecursively(
PathBuilder.Dir("defaultstorage", "configs"),
PathBuilder.Dir("defaultstorage", "configs"),
PathBuilder.Dir("storage", "configs")
);
}
@@ -40,64 +40,73 @@ public class StorageService
private async Task UpdateResources()
{
Logger.Info("Checking resources");
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)
try
{
IReadOnlyList<RepositoryContent> contents;
if(string.IsNullOrEmpty(dirPath))
contents = await client.Repository.Content.GetAllContents(user, repo);
else
contents = await client.Repository.Content.GetAllContents(user, repo, dirPath);
Logger.Info("Checking resources");
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"))
continue;
if(File.Exists(localPath) && !content.Name.EndsWith(".lang"))
continue;
string localPath = Path.Combine(localDir, content.Name);
if (content.Name.EndsWith(".lang") && File.Exists(localPath) &&
new FileInfo(localPath).Length == content.Size)
if (content.Type == ContentType.File)
{
Logger.Info($"Skipped language file '{content.Name}'");
continue;
}
var fileContent = await client.Repository.Content.GetRawContent(user, repo, content.Path);
Directory.CreateDirectory(localDir); // Ensure the directory exists
await File.WriteAllBytesAsync(localPath, fileContent);
if (content.Name.EndsWith(".gitattributes"))
continue;
Logger.Debug($"Synced file '{content.Path}'");
}
else if (content.Type == ContentType.Dir)
{
await CopyDirectory(content.Path, localPath);
if (File.Exists(localPath) && !content.Name.EndsWith(".lang"))
continue;
if (content.Name.EndsWith(".lang") && File.Exists(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)
{
return !Directory.EnumerateFileSystemEntries(path).Any();
}
private static void CopyFilesRecursively(string sourcePath, string targetPath)
{
//Now Create all of the directories
@@ -107,7 +116,7 @@ public class StorageService
}
//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);
}

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="Stripe.net" Version="41.23.0-beta.1" />
<PackageReference Include="UAParser" Version="3.1.47" />
<PackageReference Include="WhoisClient.NET" Version="5.0.0" />
<PackageReference Include="XtermBlazor" Version="1.8.1" />
</ItemGroup>

View File

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

View File

@@ -22,6 +22,7 @@
@inject DynamicBackgroundService DynamicBackgroundService
@inject KeyListenerService KeyListenerService
@inject ConfigService ConfigService
@inject IpVerificationService IpVerificationService
@{
var uri = new Uri(NavigationManager.Uri);
@@ -46,9 +47,35 @@
<DefaultLayout>
<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" &&
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
{
@@ -122,8 +136,8 @@
<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>
<h2>@(SmartTranslateService.Translate("Authenticating"))...</h2>
<p class="mt-3 fw-normal fs-6">@(SmartTranslateService.Translate("Verifying token, loading user data"))</p>
</div>
</div>
</div>
@@ -137,6 +151,7 @@
{
private bool UserProcessed = false;
private bool IsIpBanned = false;
private bool IsIpSuspicious = false;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
@@ -146,11 +161,6 @@
{
DynamicBackgroundService.OnBackgroundImageChanged += async (_, _) => { await InvokeAsync(StateHasChanged); };
IsIpBanned = await IpBanService.IsBanned();
if (IsIpBanned)
await InvokeAsync(StateHasChanged);
await Event.On<Object>("ipBan.update", this, async _ =>
{
IsIpBanned = await IpBanService.IsBanned();
@@ -158,6 +168,10 @@
});
await IdentityService.Load();
IsIpBanned = await IpBanService.IsBanned();
IsIpSuspicious = await IpVerificationService.IsDatacenterOrVpn(IdentityService.Ip);
UserProcessed = true;
await InvokeAsync(StateHasChanged);