Added new ddos protection

This commit is contained in:
Marcel Baumgartner
2023-07-24 00:23:29 +02:00
parent 512a989609
commit 2cf2b77090
19 changed files with 1575 additions and 136 deletions

View File

@@ -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();
}

View File

@@ -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)
{

View 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; }
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.App.Database.Entities;
public class WhitelistIp
{
public int Id { get; set; }
public string Ip { get; set; } = "";
}

File diff suppressed because it is too large Load Diff

View File

@@ -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");
}
}
}

View File

@@ -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")

View File

@@ -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();
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.App.Http.Requests.Daemon;
public class DdosStart
{
public string Ip { get; set; } = "";
public long Packets { get; set; }
}

View File

@@ -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; } = "";
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.App.Http.Requests.Daemon;
public class DdosStop
{
public string Ip { get; set; } = "";
public long Traffic { get; set; }
}

View File

@@ -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)
{

View 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();
}
}

View File

@@ -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 =>

View File

@@ -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()