Added new ddos protection
This commit is contained in:
@@ -276,6 +276,10 @@ public class ConfigV1
|
||||
[Blur]
|
||||
public string Token { get; set; } = Guid.NewGuid().ToString();
|
||||
|
||||
[JsonProperty("BlockIpDuration")]
|
||||
[Description("The duration in minutes a ip will be blocked by the anti ddos system")]
|
||||
public int BlockIpDuration { get; set; } = 15;
|
||||
|
||||
[JsonProperty("ReCaptcha")] public ReCaptchaData ReCaptcha { get; set; } = new();
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,8 @@ public class DataContext : DbContext
|
||||
public DbSet<IpBan> IpBans { get; set; }
|
||||
public DbSet<PermissionGroup> PermissionGroups { get; set; }
|
||||
public DbSet<SecurityLog> SecurityLogs { get; set; }
|
||||
public DbSet<BlocklistIp> BlocklistIps { get; set; }
|
||||
public DbSet<WhitelistIp> WhitelistIps { get; set; }
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
|
||||
10
Moonlight/App/Database/Entities/BlocklistIp.cs
Normal file
10
Moonlight/App/Database/Entities/BlocklistIp.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Moonlight.App.Database.Entities;
|
||||
|
||||
public class BlocklistIp
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Ip { get; set; } = "";
|
||||
public DateTime ExpiresAt { get; set; }
|
||||
public long Packets { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
7
Moonlight/App/Database/Entities/WhitelistIp.cs
Normal file
7
Moonlight/App/Database/Entities/WhitelistIp.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Moonlight.App.Database.Entities;
|
||||
|
||||
public class WhitelistIp
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Ip { get; set; } = "";
|
||||
}
|
||||
1107
Moonlight/App/Database/Migrations/20230721201950_AddIpRules.Designer.cs
generated
Normal file
1107
Moonlight/App/Database/Migrations/20230721201950_AddIpRules.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Moonlight.App.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddIpRules : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "BlocklistIps",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
Ip = table.Column<string>(type: "longtext", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
ExpiresAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
Packets = table.Column<long>(type: "bigint", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_BlocklistIps", x => x.Id);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "WhitelistIps",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
Ip = table.Column<string>(type: "longtext", nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_WhitelistIps", x => x.Id);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "BlocklistIps");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "WhitelistIps");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,30 @@ namespace Moonlight.App.Database.Migrations
|
||||
.HasAnnotation("ProductVersion", "7.0.3")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.BlocklistIp", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<DateTime>("ExpiresAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Ip")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<long>("Packets")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("BlocklistIps");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.CloudPanel", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -842,6 +866,21 @@ namespace Moonlight.App.Database.Migrations
|
||||
b.ToTable("WebSpaces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.WhitelistIp", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Ip")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("WhitelistIps");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.App.Database.Entities.DdosAttack", b =>
|
||||
{
|
||||
b.HasOne("Moonlight.App.Database.Entities.Node", "Node")
|
||||
|
||||
@@ -3,6 +3,7 @@ using Moonlight.App.Database.Entities;
|
||||
using Moonlight.App.Events;
|
||||
using Moonlight.App.Http.Requests.Daemon;
|
||||
using Moonlight.App.Repositories;
|
||||
using Moonlight.App.Services.Background;
|
||||
|
||||
namespace Moonlight.App.Http.Controllers.Api.Remote;
|
||||
|
||||
@@ -10,19 +11,17 @@ namespace Moonlight.App.Http.Controllers.Api.Remote;
|
||||
[Route("api/remote/ddos")]
|
||||
public class DdosController : Controller
|
||||
{
|
||||
private readonly NodeRepository NodeRepository;
|
||||
private readonly EventSystem Event;
|
||||
private readonly DdosAttackRepository DdosAttackRepository;
|
||||
private readonly Repository<Node> NodeRepository;
|
||||
private readonly DdosProtectionService DdosProtectionService;
|
||||
|
||||
public DdosController(NodeRepository nodeRepository, EventSystem eventSystem, DdosAttackRepository ddosAttackRepository)
|
||||
public DdosController(Repository<Node> nodeRepository, DdosProtectionService ddosProtectionService)
|
||||
{
|
||||
NodeRepository = nodeRepository;
|
||||
Event = eventSystem;
|
||||
DdosAttackRepository = ddosAttackRepository;
|
||||
DdosProtectionService = ddosProtectionService;
|
||||
}
|
||||
|
||||
[HttpPost("update")]
|
||||
public async Task<ActionResult> Update([FromBody] DdosStatus ddosStatus)
|
||||
[HttpPost("start")]
|
||||
public async Task<ActionResult> Start([FromBody] DdosStart ddosStart)
|
||||
{
|
||||
var tokenData = Request.Headers.Authorization.ToString().Replace("Bearer ", "");
|
||||
var id = tokenData.Split(".")[0];
|
||||
@@ -35,18 +34,26 @@ public class DdosController : Controller
|
||||
|
||||
if (token != node.Token)
|
||||
return Unauthorized();
|
||||
|
||||
await DdosProtectionService.ProcessDdosSignal(ddosStart.Ip, ddosStart.Packets);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
var ddosAttack = new DdosAttack()
|
||||
{
|
||||
Ongoing = ddosStatus.Ongoing,
|
||||
Data = ddosStatus.Data,
|
||||
Ip = ddosStatus.Ip,
|
||||
Node = node
|
||||
};
|
||||
[HttpPost("stop")]
|
||||
public async Task<ActionResult> Stop([FromBody] DdosStop ddosStop)
|
||||
{
|
||||
var tokenData = Request.Headers.Authorization.ToString().Replace("Bearer ", "");
|
||||
var id = tokenData.Split(".")[0];
|
||||
var token = tokenData.Split(".")[1];
|
||||
|
||||
ddosAttack = DdosAttackRepository.Add(ddosAttack);
|
||||
var node = NodeRepository.Get().FirstOrDefault(x => x.TokenId == id);
|
||||
|
||||
await Event.Emit("node.ddos", ddosAttack);
|
||||
if (node == null)
|
||||
return NotFound();
|
||||
|
||||
if (token != node.Token)
|
||||
return Unauthorized();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
7
Moonlight/App/Http/Requests/Daemon/DdosStart.cs
Normal file
7
Moonlight/App/Http/Requests/Daemon/DdosStart.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Moonlight.App.Http.Requests.Daemon;
|
||||
|
||||
public class DdosStart
|
||||
{
|
||||
public string Ip { get; set; } = "";
|
||||
public long Packets { get; set; }
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace Moonlight.App.Http.Requests.Daemon;
|
||||
|
||||
public class DdosStatus
|
||||
{
|
||||
public bool Ongoing { get; set; }
|
||||
public long Data { get; set; }
|
||||
public string Ip { get; set; } = "";
|
||||
}
|
||||
7
Moonlight/App/Http/Requests/Daemon/DdosStop.cs
Normal file
7
Moonlight/App/Http/Requests/Daemon/DdosStop.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Moonlight.App.Http.Requests.Daemon;
|
||||
|
||||
public class DdosStop
|
||||
{
|
||||
public string Ip { get; set; } = "";
|
||||
public long Traffic { get; set; }
|
||||
}
|
||||
@@ -44,13 +44,6 @@ public static class Permissions
|
||||
Description = "Create a new shared domain in the admin area"
|
||||
};
|
||||
|
||||
public static Permission AdminNodeDdos = new()
|
||||
{
|
||||
Index = 8,
|
||||
Name = "Admin Node DDoS",
|
||||
Description = "Manage DDoS protection for nodes in the admin area"
|
||||
};
|
||||
|
||||
public static Permission AdminNodeEdit = new()
|
||||
{
|
||||
Index = 9,
|
||||
@@ -400,6 +393,13 @@ public static class Permissions
|
||||
Name = "Admin security logs",
|
||||
Description = "View the security logs"
|
||||
};
|
||||
|
||||
public static Permission AdminSecurityDdos = new()
|
||||
{
|
||||
Index = 59,
|
||||
Name = "Admin security ddos",
|
||||
Description = "Manage the integrated ddos protection"
|
||||
};
|
||||
|
||||
public static Permission? FromString(string name)
|
||||
{
|
||||
|
||||
129
Moonlight/App/Services/Background/DdosProtectionService.cs
Normal file
129
Moonlight/App/Services/Background/DdosProtectionService.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
using Moonlight.App.ApiClients.Daemon;
|
||||
using Moonlight.App.Database.Entities;
|
||||
using Moonlight.App.Events;
|
||||
using Moonlight.App.Helpers;
|
||||
using Moonlight.App.Repositories;
|
||||
|
||||
namespace Moonlight.App.Services.Background;
|
||||
|
||||
public class DdosProtectionService
|
||||
{
|
||||
private readonly IServiceScopeFactory ServiceScopeFactory;
|
||||
|
||||
public DdosProtectionService(IServiceScopeFactory serviceScopeFactory)
|
||||
{
|
||||
ServiceScopeFactory = serviceScopeFactory;
|
||||
|
||||
Task.Run(UnBlocker);
|
||||
}
|
||||
|
||||
private async Task UnBlocker()
|
||||
{
|
||||
var periodicTimer = new PeriodicTimer(TimeSpan.FromMinutes(5));
|
||||
|
||||
while (true)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var blocklistIpRepo = scope.ServiceProvider.GetRequiredService<Repository<BlocklistIp>>();
|
||||
|
||||
var ips = blocklistIpRepo
|
||||
.Get()
|
||||
.ToArray();
|
||||
|
||||
foreach (var ip in ips)
|
||||
{
|
||||
if (DateTime.UtcNow > ip.ExpiresAt)
|
||||
{
|
||||
blocklistIpRepo.Delete(ip);
|
||||
}
|
||||
}
|
||||
|
||||
var newCount = blocklistIpRepo
|
||||
.Get()
|
||||
.Count();
|
||||
|
||||
if (newCount != ips.Length)
|
||||
{
|
||||
await RebuildNodeFirewalls();
|
||||
}
|
||||
|
||||
await periodicTimer.WaitForNextTickAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RebuildNodeFirewalls()
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var blocklistIpRepo = scope.ServiceProvider.GetRequiredService<Repository<BlocklistIp>>();
|
||||
var nodeRepo = scope.ServiceProvider.GetRequiredService<Repository<Node>>();
|
||||
var nodeService = scope.ServiceProvider.GetRequiredService<NodeService>();
|
||||
|
||||
var ips = blocklistIpRepo
|
||||
.Get()
|
||||
.Select(x => x.Ip)
|
||||
.ToArray();
|
||||
|
||||
foreach (var node in nodeRepo.Get().ToArray())
|
||||
{
|
||||
try
|
||||
{
|
||||
await nodeService.RebuildFirewall(node, ips);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Warn($"Error rebuilding firewall on node {node.Name}");
|
||||
Logger.Warn(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ProcessDdosSignal(string ip, long packets)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
|
||||
var blocklistRepo = scope.ServiceProvider.GetRequiredService<Repository<BlocklistIp>>();
|
||||
var whitelistRepo = scope.ServiceProvider.GetRequiredService<Repository<WhitelistIp>>();
|
||||
|
||||
var whitelistIps = whitelistRepo.Get().ToArray();
|
||||
|
||||
if(whitelistIps.Any(x => x.Ip == ip))
|
||||
return;
|
||||
|
||||
var blocklistIps = blocklistRepo.Get().ToArray();
|
||||
|
||||
if(blocklistIps.Any(x => x.Ip == ip))
|
||||
return;
|
||||
|
||||
await BlocklistIp(ip, packets);
|
||||
}
|
||||
|
||||
public async Task BlocklistIp(string ip, long packets)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var blocklistRepo = scope.ServiceProvider.GetRequiredService<Repository<BlocklistIp>>();
|
||||
var configService = scope.ServiceProvider.GetRequiredService<ConfigService>();
|
||||
var eventSystem = scope.ServiceProvider.GetRequiredService<EventSystem>();
|
||||
|
||||
var blocklistIp = blocklistRepo.Add(new()
|
||||
{
|
||||
Ip = ip,
|
||||
Packets = packets,
|
||||
ExpiresAt = DateTime.UtcNow.AddMinutes(configService.Get().Moonlight.Security.BlockIpDuration),
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
await RebuildNodeFirewalls();
|
||||
await eventSystem.Emit("ddos.add", blocklistIp);
|
||||
}
|
||||
|
||||
public async Task UnBlocklistIp(string ip)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var blocklistRepo = scope.ServiceProvider.GetRequiredService<Repository<BlocklistIp>>();
|
||||
|
||||
var blocklist = blocklistRepo.Get().First(x => x.Ip == ip);
|
||||
blocklistRepo.Delete(blocklist);
|
||||
|
||||
await RebuildNodeFirewalls();
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ public class DiscordNotificationService
|
||||
Event.On<User>("supportChat.close", this, OnSupportChatClose);
|
||||
Event.On<User>("user.rating", this, OnUserRated);
|
||||
Event.On<User>("billing.completed", this, OnBillingCompleted);
|
||||
Event.On<BlocklistIp>("ddos.add", this, OnIpBlockListed);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -43,6 +44,18 @@ public class DiscordNotificationService
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnIpBlockListed(BlocklistIp blocklistIp)
|
||||
{
|
||||
await SendNotification("", builder =>
|
||||
{
|
||||
builder.Color = Color.Red;
|
||||
builder.Title = "New ddos attack detected";
|
||||
|
||||
builder.AddField("IP", blocklistIp.Ip);
|
||||
builder.AddField("Packets", blocklistIp.Packets);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task OnBillingCompleted(User user)
|
||||
{
|
||||
await SendNotification("", builder =>
|
||||
|
||||
@@ -50,6 +50,11 @@ public class NodeService
|
||||
return await DaemonApiHelper.Get<DockerMetrics>(node, "metrics/docker");
|
||||
}
|
||||
|
||||
public async Task RebuildFirewall(Node node, string[] ips)
|
||||
{
|
||||
await DaemonApiHelper.Post(node, "firewall/rebuild", ips);
|
||||
}
|
||||
|
||||
public async Task Mount(Node node, string server, string serverPath, string path)
|
||||
{
|
||||
await DaemonApiHelper.Post(node, "mount", new Mount()
|
||||
|
||||
Reference in New Issue
Block a user