Merge pull request #827 from Rhefew/feature/ip-connection-limit

feat(plugins): implement customizable maximum concurrent connections

(cherry picked from commit 0e30df9ae22a265c3486dea1eba259567631eb87)
This commit is contained in:
sven-n
2026-07-15 22:34:54 +02:00
committed by Acentech Dev
parent 77df5d1145
commit ed32ebdfd3
7 changed files with 318 additions and 3 deletions

View File

@@ -0,0 +1,16 @@
// <copyright file="IHasIpAddress.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic;
/// <summary>
/// Interface for objects that expose a remote IP address.
/// </summary>
public interface IHasIpAddress
{
/// <summary>
/// Gets the IP address of the remote connection.
/// </summary>
string? IpAddress { get; }
}

View File

@@ -203,6 +203,11 @@ public class Player : AsyncDisposable, IBucketMapObserver, IAttackable, IAttacke
/// <inheritdoc/>
public ushort Id { get; set; }
/// <summary>
/// Gets or sets a custom login result to override the default when login fails.
/// </summary>
public Views.Login.LoginResult? LoginResultOverride { get; set; }
/// <inheritdoc cref="IPartyMember" />
public string Name => this.SelectedCharacter?.Name ?? string.Empty;

View File

@@ -189,7 +189,9 @@ public class LoginAction
private async ValueTask HandleAlreadyConnectedAsync(Player player, string username)
{
await player.InvokeViewPlugInAsync<IShowLoginResultPlugIn>(p => p.ShowLoginResultAsync(LoginResult.AccountAlreadyConnected)).ConfigureAwait(false);
var result = player.LoginResultOverride ?? LoginResult.AccountAlreadyConnected;
player.LoginResultOverride = null;
await player.InvokeViewPlugInAsync<IShowLoginResultPlugIn>(p => p.ShowLoginResultAsync(result)).ConfigureAwait(false);
if (player.GameContext is IGameServerContext gameServerContext)
{
await gameServerContext.EventPublisher.PlayerAlreadyLoggedInAsync(gameServerContext.Id, username).ConfigureAwait(false);

View File

@@ -0,0 +1,68 @@
// <copyright file="MaximumConnectionsPerIpPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlugIns;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// A plugin that limits the number of simultaneous active player sessions connected from the same IP address.
/// </summary>
[PlugIn]
[Display(Name = "Maximum Connections Per IP", Description = "Limits the maximum number of parallel connections from the same IP address.")]
[Guid("2C779F5E-379E-4CE0-BFCC-CA6455D757B3")]
public class MaximumConnectionsPerIpPlugIn : IPlayerStateChangingPlugIn, ISupportCustomConfiguration<MaximumConnectionsPerIpPlugInConfiguration>, ISupportDefaultCustomConfiguration, IDisabledByDefault
{
/// <inheritdoc />
public MaximumConnectionsPerIpPlugInConfiguration? Configuration { get; set; }
/// <inheritdoc />
public async ValueTask PlayerStateChangingAsync(Player player, StateMachine.StateChangeEventArgs eventArgs)
{
if (eventArgs.NextState != PlayerState.Authenticated)
{
return;
}
var ipAddress = (player as IHasIpAddress)?.IpAddress;
if (string.IsNullOrEmpty(ipAddress))
{
return;
}
var config = this.Configuration ?? CreateDefaultConfiguration();
var players = await player.GameContext.GetPlayersAsync().ConfigureAwait(false);
var sameIpCount = players.Count(p =>
(p as IHasIpAddress)?.IpAddress == ipAddress
&& p != player
&& p.PlayerState.CurrentState != PlayerState.Initial
&& p.PlayerState.CurrentState != PlayerState.LoginScreen);
if (sameIpCount >= config.MaximumConnectionsPerIp)
{
player.Logger.LogWarning(
"Login request for IP '{IpAddress}' was cancelled. It exceeded the maximum connection limit of {Limit}.",
ipAddress,
config.MaximumConnectionsPerIp);
player.LoginResultOverride = Views.Login.LoginResult.ServerIsFull;
eventArgs.Cancel = true;
}
}
/// <inheritdoc />
public object CreateDefaultConfig()
{
return CreateDefaultConfiguration();
}
private static MaximumConnectionsPerIpPlugInConfiguration CreateDefaultConfiguration()
{
return new MaximumConnectionsPerIpPlugInConfiguration();
}
}

View File

@@ -0,0 +1,16 @@
// <copyright file="MaximumConnectionsPerIpPlugInConfiguration.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlugIns;
/// <summary>
/// Configuration for the <see cref="MaximumConnectionsPerIpPlugIn"/>.
/// </summary>
public class MaximumConnectionsPerIpPlugInConfiguration
{
/// <summary>
/// Gets or sets the maximum number of concurrent connections per IP address.
/// </summary>
public int MaximumConnectionsPerIp { get; set; } = 3;
}

View File

@@ -1,4 +1,4 @@
// <copyright file="RemotePlayer.cs" company="MUnique">
// <copyright file="RemotePlayer.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
@@ -16,12 +16,14 @@ using MUnique.OpenMU.PlugIns;
/// <summary>
/// A player which is playing through a remote connection.
/// </summary>
public class RemotePlayer : Player, IClientVersionProvider
public class RemotePlayer : Player, IClientVersionProvider, IHasIpAddress
{
private readonly byte[] _packetBuffer = new byte[0xFF];
private ClientVersion _clientVersion;
private readonly string? _ipAddress;
/// <summary>
/// Initializes a new instance of the <see cref="RemotePlayer"/> class.
/// </summary>
@@ -33,6 +35,9 @@ public class RemotePlayer : Player, IClientVersionProvider
{
this.Connection = connection;
this._clientVersion = clientVersion;
this._ipAddress = connection.EndPoint is System.Net.IPEndPoint ipEndPoint
? (ipEndPoint.Address.IsIPv4MappedToIPv6 ? ipEndPoint.Address.MapToIPv4() : ipEndPoint.Address).ToString()
: null;
this.MainPacketHandler = new MainPacketHandlerPlugInContainer(this, gameContext.PlugInManager, gameContext.LoggerFactory);
this.MainPacketHandler.Initialize();
this.Connection!.PacketReceived += this.PacketReceivedAsync;
@@ -42,6 +47,9 @@ public class RemotePlayer : Player, IClientVersionProvider
/// <inheritdoc />
public event EventHandler? ClientVersionChanged;
/// <inheritdoc />
public string? IpAddress => this._ipAddress;
/// <summary>
/// Gets the game server context.
/// </summary>