mirror of
https://github.com/Empostor/Empostor.git
synced 2026-10-20 01:46:07 +00:00
New Update (No Test):
## Fix: players keep getting kicked mid-game (auth cache TTL)
**Root cause**: the auth cache (`AuthCacheService`) has a 10-min TTL but `CreatedAt` was only set when the token was created — never refreshed while the player was actually connected. After ~12 min, the cleanup timer would expire the port entry, fire `OnPortExpired` → return the port to the pool → stop the delta UDP listener → the active player got force-disconnected ("Server shutting down" in the logs).
**What I changed**:
- `AuthCacheService`: added a `_confirmedPorts` set + `ConfirmPort()`. Cleanup now **skips confirmed (actively connected) ports** — as long as the player is connected, the port lease is never cleared.
- `ClientManager`: calls `_authCache.ConfirmPort(deltaPort)` right after `_portPool.ConfirmPort()` when the port matches, so an active session is marked confirmed.
- Port is still properly released on disconnect (existing `RemoveByPort` path untouched).
---
## Change: hide the default UDP port, use last delta port as default
**Why**: the well-known default port (my config: 22023) was getting flooded with garbage UDP. So now the server reserves the **last port** of the delta range as the main listener, so the default game socket is never the obvious 22023, and even if the pool is fully allocated, the server still runs.
**What I changed**:
- `ServerConfig`: new option `ReserveLastDeltaPortAsDefault` (default true).
- `PortPoolService`: `DefaultPort` = last delta port (`DeltaPortEnd`) when enabled; pool excludes it.
- `MatchmakerService`: main UDP listener binds to `DefaultPort` instead of `ListenPort`.
- `GamesController`: fallback port in `GetDeltaPort()` uses `DefaultPort`.
---
## Change: Filtaur-style line-speed UDP packet filter
**What I changed**: new `UdpPacketFilter` in Hazel + hooked into `UdpConnectionListener.ProcessData`. It drops junk at line speed before it reaches the game server:
- size check (min/max length),
- send-option byte check (layer-7),
- per-IP connection burst limit + temporary blacklist (default 30 burst / 60s blacklist).
Only applies to **unknown endpoints** (new connection attempts); established connections are never filtered, so in-game traffic is unaffected. This is the "filtaur" idea — force spammers through overhead and drop bad packets fast so they can't overload the server and drop everyone.
---
## Config (config.json)
Added `"ReserveLastDeltaPortAsDefault": true` under `Server`.
This commit is contained in:
1 parent
c6e6214288
commit
2cd2f92d4f
12 files changed
+273
-16
No files matched your search
+3
-1
@@ -79,7 +79,9 @@ ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
|
||||
|
||||
EXPOSE 80/tcp
|
||||
EXPOSE 22023/tcp
|
||||
EXPOSE 22023/udp
|
||||
# Main UDP listener moved to the last delta port (DeltaPortEnd).
|
||||
# Expose the whole delta range so players can connect.
|
||||
EXPOSE 22024-22099/udp
|
||||
|
||||
VOLUME ["/app/config", "/app/plugins", "/app/libraries", "/app/Languages", "/app/Log", "/app/Data", "/app/marketplace"]
|
||||
|
||||
|
||||
+6
-1
@@ -7,8 +7,13 @@ services:
|
||||
restart: unless-stopped
|
||||
|
||||
ports:
|
||||
# HTTP API / admin panel (TCP)
|
||||
- "22023:22023/tcp"
|
||||
- "22023:22023/udp"
|
||||
# Main UDP listener now uses the last port of the delta range
|
||||
# (DeltaPortEnd), so 22023/udp is no longer the game socket.
|
||||
# Expose the whole delta range so players can connect.
|
||||
# Adjust the range to match DeltaPortStart-DeltaPortEnd in config.json.
|
||||
- "22024-22099:22024-22099/udp"
|
||||
# Uncomment to expose admin HTTP panel:
|
||||
# - "80:80/tcp"
|
||||
|
||||
|
||||
@@ -25,6 +25,14 @@ namespace Empostor.Api.Config
|
||||
|
||||
public ushort DeltaPortEnd { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// When the delta port pool is enabled, reserve the last port of the
|
||||
/// range as the main UDP listener instead of the well-known
|
||||
/// <see cref="ListenPort" />. This hides the default port from garbage
|
||||
/// UDP floods targeting the commonly known port (e.g. 22023).
|
||||
/// </summary>
|
||||
public bool ReserveLastDeltaPortAsDefault { get; set; } = true;
|
||||
|
||||
public string ResolvePublicIp()
|
||||
{
|
||||
return _resolvedPublicIp ??= IpUtils.ResolveIp(PublicIp);
|
||||
|
||||
@@ -9,6 +9,7 @@ using Empostor.Api.Config;
|
||||
using Empostor.Api.Games;
|
||||
using Empostor.Api.Games.Managers;
|
||||
using Empostor.Server.Extensions;
|
||||
using Empostor.Server.Net;
|
||||
using Empostor.Server.Service.Auth;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -23,17 +24,20 @@ public sealed class GamesController : ControllerBase
|
||||
private readonly ListingManager _listingManager;
|
||||
private readonly ServerConfig _serverConfig;
|
||||
private readonly AuthCacheService _authCache;
|
||||
private readonly PortPoolService _portPool;
|
||||
|
||||
public GamesController(
|
||||
IGameManager gameManager,
|
||||
ListingManager listingManager,
|
||||
IOptions<ServerConfig> serverConfig,
|
||||
AuthCacheService authCache)
|
||||
AuthCacheService authCache,
|
||||
PortPoolService portPool)
|
||||
{
|
||||
_gameManager = gameManager;
|
||||
_listingManager = listingManager;
|
||||
_serverConfig = serverConfig.Value;
|
||||
_authCache = authCache;
|
||||
_portPool = portPool;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -182,7 +186,7 @@ public sealed class GamesController : ControllerBase
|
||||
var clientIp = GetClientIp();
|
||||
if (clientIp == null)
|
||||
{
|
||||
return _serverConfig.PublicPort;
|
||||
return (ushort)(_portPool.DefaultPort > 0 ? _portPool.DefaultPort : _serverConfig.PublicPort);
|
||||
}
|
||||
|
||||
var deltaPort = _authCache.FindPortByIp(clientIp);
|
||||
@@ -191,7 +195,9 @@ public sealed class GamesController : ControllerBase
|
||||
return (ushort)deltaPort;
|
||||
}
|
||||
|
||||
return _serverConfig.PublicPort;
|
||||
// Fall back to the main listener port (reserved default port when the
|
||||
// delta pool is enabled, otherwise the configured public port).
|
||||
return (ushort)(_portPool.DefaultPort > 0 ? _portPool.DefaultPort : _serverConfig.PublicPort);
|
||||
}
|
||||
|
||||
private IPAddress? GetClientIp()
|
||||
|
||||
@@ -160,6 +160,9 @@ namespace Empostor.Server.Net.Manager
|
||||
{
|
||||
// Port matched — cancel the allocation timeout
|
||||
_portPool.ConfirmPort(deltaPort);
|
||||
// Mark the auth-cache entry as active so the inactivity
|
||||
// timer never clears the port while the player is connected.
|
||||
_authCache.ConfirmPort(deltaPort);
|
||||
|
||||
friendCode = authInfo.FriendCode;
|
||||
|
||||
|
||||
@@ -37,7 +37,12 @@ namespace Empostor.Server.Net
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var endpoint = new IPEndPoint(IPAddress.Parse(_serverConfig.ResolveListenIp()), _serverConfig.ListenPort);
|
||||
// Use the reserved default port (last delta port when enabled) so the
|
||||
// well-known ListenPort is not exposed to garbage UDP floods.
|
||||
var listenPort = _portPool.IsEnabled && _portPool.DefaultPort > 0
|
||||
? _portPool.DefaultPort
|
||||
: _serverConfig.ListenPort;
|
||||
var endpoint = new IPEndPoint(IPAddress.Parse(_serverConfig.ResolveListenIp()), listenPort);
|
||||
|
||||
await _matchmaker.StartAsync(endpoint);
|
||||
|
||||
|
||||
@@ -26,6 +26,14 @@ public sealed class PortPoolService : IDisposable
|
||||
public bool IsEnabled => _deltaPortStart > 0
|
||||
&& _deltaPortEnd >= _deltaPortStart;
|
||||
|
||||
/// <summary>
|
||||
/// The main UDP listener port. When delta ports are enabled and
|
||||
/// <see cref="ServerConfig.ReserveLastDeltaPortAsDefault" /> is set, the
|
||||
/// last port of the range is reserved for the main listener so the
|
||||
/// well-known default port is never used for the game socket.
|
||||
/// </summary>
|
||||
public int DefaultPort { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a port is returned to the pool (via disconnect or timeout expiry).
|
||||
/// Subscribers should stop the delta listener and close firewall rules.
|
||||
@@ -45,10 +53,25 @@ public sealed class PortPoolService : IDisposable
|
||||
|
||||
if (IsEnabled)
|
||||
{
|
||||
var skipped = 0;
|
||||
for (var port = _deltaPortStart; port <= _deltaPortEnd; port++)
|
||||
// Reserve the last port of the delta range as the main listener
|
||||
// (anti garbage-UDP-flood on the well-known default port).
|
||||
if (cfg.ReserveLastDeltaPortAsDefault)
|
||||
{
|
||||
if (port == _listenPort)
|
||||
DefaultPort = _deltaPortEnd;
|
||||
_logger.LogInformation(
|
||||
"PortPool reserved last port {Port} as the main UDP listener (anti-flood).",
|
||||
DefaultPort);
|
||||
}
|
||||
else
|
||||
{
|
||||
DefaultPort = _listenPort;
|
||||
}
|
||||
|
||||
var skipped = 0;
|
||||
var poolEnd = cfg.ReserveLastDeltaPortAsDefault ? _deltaPortEnd - 1 : _deltaPortEnd;
|
||||
for (var port = _deltaPortStart; port <= poolEnd; port++)
|
||||
{
|
||||
if (port == _listenPort || port == DefaultPort)
|
||||
{
|
||||
skipped++;
|
||||
continue;
|
||||
@@ -60,16 +83,17 @@ public sealed class PortPoolService : IDisposable
|
||||
if (skipped > 0)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"PortPool skipped {Count} port(s) overlapping with main listen port {ListenPort}",
|
||||
"PortPool skipped {Count} port(s) overlapping with the main listen port {ListenPort}",
|
||||
skipped, _listenPort);
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"PortPool initialized with {Count} ports ({Start}-{End})",
|
||||
_availablePorts.Count, _deltaPortStart, _deltaPortEnd);
|
||||
_availablePorts.Count, _deltaPortStart, poolEnd);
|
||||
}
|
||||
else
|
||||
{
|
||||
DefaultPort = _listenPort;
|
||||
_logger.LogInformation("PortPool disabled (DeltaPortStart={Start}, DeltaPortEnd={End})",
|
||||
_deltaPortStart, _deltaPortEnd);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,12 @@ public sealed class AuthCacheService : IDisposable
|
||||
private readonly ConcurrentDictionary<string, UserAuthInfo> _byIpDirect = new();
|
||||
private readonly ConcurrentDictionary<string, int> _ipToPort = new();
|
||||
|
||||
/// <summary>
|
||||
/// Ports that have an active connection. While a player is connected,
|
||||
/// the port lease must not be cleared by the inactivity timer.
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<int, byte> _confirmedPorts = new();
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a port lease should be returned to the pool (e.g., on expiry during cleanup).
|
||||
/// </summary>
|
||||
@@ -118,8 +124,31 @@ public sealed class AuthCacheService : IDisposable
|
||||
return _byPort.TryGetValue(port, out var info) && !Expired(info) ? info : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks a delta port as having an active connection.
|
||||
/// The inactivity timer will skip confirmed ports so that connected
|
||||
/// players are never kicked because of the auth-cache TTL.
|
||||
/// </summary>
|
||||
public void ConfirmPort(int port)
|
||||
{
|
||||
if (port <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_confirmedPorts[port] = 0;
|
||||
|
||||
// Refresh the expiry so the entry is also valid for other lookups.
|
||||
if (_byPort.TryGetValue(port, out var info))
|
||||
{
|
||||
info.CreatedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveByPort(int port)
|
||||
{
|
||||
_confirmedPorts.TryRemove(port, out _);
|
||||
|
||||
if (_byPort.TryRemove(port, out var info) && info.ClientIp != null)
|
||||
{
|
||||
_byIpDirect.TryRemove(info.ClientIp, out _);
|
||||
@@ -174,7 +203,12 @@ public sealed class AuthCacheService : IDisposable
|
||||
}
|
||||
|
||||
// Clean port-based entries
|
||||
var expiredPorts = _byPort.Where(kv => Expired(kv.Value)).Select(kv => kv.Key).ToList();
|
||||
// Confirmed (actively connected) ports are skipped: the lease is only
|
||||
// released when the player disconnects.
|
||||
var expiredPorts = _byPort
|
||||
.Where(kv => !_confirmedPorts.ContainsKey(kv.Key) && Expired(kv.Value))
|
||||
.Select(kv => kv.Key)
|
||||
.ToList();
|
||||
foreach (var port in expiredPorts)
|
||||
{
|
||||
if (_byPort.TryRemove(port, out var info))
|
||||
|
||||
@@ -72,13 +72,23 @@ internal static class StartupBanner
|
||||
sw.WriteLine(C(BoxML + new string(BoxH[0], width - 2) + BoxMR, Cyan));
|
||||
|
||||
Row(sw, width, maxLabel, "Public IP", server.PublicIp);
|
||||
Row(sw, width, maxLabel, "Listen Port", $"{server.ListenPort} (UDP)");
|
||||
var deltaEnabled = server.DeltaPortStart > 0 && server.DeltaPortEnd >= server.DeltaPortStart;
|
||||
var actualListenPort = deltaEnabled && server.ReserveLastDeltaPortAsDefault
|
||||
? server.DeltaPortEnd
|
||||
: server.ListenPort;
|
||||
Row(sw, width, maxLabel, "Listen Port", $"{actualListenPort} (UDP)");
|
||||
Row(sw, width, maxLabel, "HTTP API", $"http://{http.ListenIp}:{http.ListenPort}");
|
||||
Row(sw, width, maxLabel, "Admin Panel", $"http://{server.PublicIp}:{http.ListenPort}/admin");
|
||||
var poolInfo = server.DeltaPortStart > 0 && server.DeltaPortEnd >= server.DeltaPortStart
|
||||
var poolInfo = deltaEnabled
|
||||
? $"{server.DeltaPortStart}-{server.DeltaPortEnd} ({server.DeltaPortEnd - server.DeltaPortStart + 1} ports)"
|
||||
: C("disabled", Yellow);
|
||||
Row(sw, width, maxLabel, "Port Pool", poolInfo);
|
||||
if (deltaEnabled && server.ReserveLastDeltaPortAsDefault)
|
||||
{
|
||||
Row(sw, width, maxLabel, "Reserved", $"{server.DeltaPortEnd} (main listener)");
|
||||
}
|
||||
|
||||
Row(sw, width, maxLabel, "Packet Filter", C("✔ Enabled", Green));
|
||||
Row(sw, width, maxLabel, "AntiCheat", anticheat.Enabled ? C("✔ Enabled", Green) : C("✗ Disabled", Red));
|
||||
Row(sw, width, maxLabel, "Log Level", logLevel);
|
||||
Row(sw, width, maxLabel, "Log File", TrimPath(logPath, width - maxLabel - 5));
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
"ListenPort": 22023,
|
||||
"UseUfw": false,
|
||||
"UseFirewalld": false,
|
||||
"DeltaPortStart": 0,
|
||||
"DeltaPortEnd": 0
|
||||
"DeltaPortStart": 22024,
|
||||
"DeltaPortEnd": 22099,
|
||||
"ReserveLastDeltaPortAsDefault": true
|
||||
},
|
||||
"HttpServer": {
|
||||
"Enabled": true,
|
||||
|
||||
@@ -20,6 +20,7 @@ public class UdpConnectionListener : NetworkConnectionListener
|
||||
private static readonly ILogger Logger = Log.ForContext<UdpConnectionListener>();
|
||||
private readonly ConcurrentDictionary<EndPoint, UdpServerConnection> _allConnections;
|
||||
private readonly UdpConnectionRateLimit _connectionRateLimit;
|
||||
private readonly UdpPacketFilter _packetFilter;
|
||||
protected readonly ObjectPool<MessageReader> _readerPool;
|
||||
private readonly Timer _reliablePacketTimer;
|
||||
|
||||
@@ -34,6 +35,12 @@ public class UdpConnectionListener : NetworkConnectionListener
|
||||
/// </summary>
|
||||
public AcceptConnectionCheck AcceptConnection;
|
||||
|
||||
/// <summary>
|
||||
/// The layer-7 packet filter used to drop junk UDP at line speed.
|
||||
/// Tune its thresholds before <see cref="StartAsync" /> if needed.
|
||||
/// </summary>
|
||||
public UdpPacketFilter PacketFilter => _packetFilter;
|
||||
|
||||
#nullable enable
|
||||
/// <summary>
|
||||
/// Creates a new UdpConnectionListener for the given <see cref="IPAddress" />, port and <see cref="IPMode" />.
|
||||
@@ -66,6 +73,7 @@ public class UdpConnectionListener : NetworkConnectionListener
|
||||
_stoppingCts.Token.Register(() => { _socket.Dispose(); });
|
||||
|
||||
_connectionRateLimit = new UdpConnectionRateLimit();
|
||||
_packetFilter = new UdpPacketFilter();
|
||||
Setup?.Invoke(_socket, _connectionRateLimit);
|
||||
}
|
||||
|
||||
@@ -162,6 +170,14 @@ public class UdpConnectionListener : NetworkConnectionListener
|
||||
// Get client from active clients
|
||||
if (!_allConnections.TryGetValue(data.RemoteEndPoint, out var client))
|
||||
{
|
||||
// Filtaur-style layer-7 firewall: drop junk packets at line speed
|
||||
// before they can reach the game server. Only applies to unknown
|
||||
// endpoints; established connections are never filtered here.
|
||||
if (_packetFilter.ShouldDrop(data.RemoteEndPoint.Address, data.Buffer, data.Buffer.Length))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for malformed connection attempts
|
||||
if (data.Buffer[0] != (byte)UdpSendOption.Hello) return;
|
||||
|
||||
@@ -230,6 +246,7 @@ public class UdpConnectionListener : NetworkConnectionListener
|
||||
await _reliablePacketTimer.DisposeAsync();
|
||||
|
||||
_connectionRateLimit.Dispose();
|
||||
_packetFilter.Dispose();
|
||||
|
||||
await base.DisposeAsync();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using Next.Hazel.Abstractions;
|
||||
using Serilog;
|
||||
|
||||
namespace Next.Hazel.Udp;
|
||||
|
||||
/// <summary>
|
||||
/// A Filtaur-style layer-7 firewall for the UDP listener.
|
||||
/// It drops malformed / junk packets at line speed before they reach the
|
||||
/// game server, so a simple packet-flooding attacker cannot overload the
|
||||
/// server and drop everyone out of their games.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The filter only applies to <b>new connection attempts</b> (packets from
|
||||
/// unknown endpoints). Packets belonging to an established connection are
|
||||
/// never rate-limited here, so in-game traffic is unaffected.
|
||||
/// </remarks>
|
||||
public sealed class UdpPacketFilter : IDisposable
|
||||
{
|
||||
private static readonly ILogger Logger = Log.ForContext<UdpPacketFilter>();
|
||||
|
||||
/// <summary>
|
||||
/// Minimal valid packet length (send-option byte + 4-byte handshake nonce).
|
||||
/// </summary>
|
||||
public int MinPacketLength { get; set; } = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum accepted packet length. Anything larger is dropped instantly.
|
||||
/// </summary>
|
||||
public int MaxPacketLength { get; set; } = 1203;
|
||||
|
||||
/// <summary>
|
||||
/// Allowed burst of connection attempts from a single IP before
|
||||
/// <see cref="BlacklistSeconds" /> kicks in.
|
||||
/// </summary>
|
||||
public int BurstLimit { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// How long an abusive IP stays blacklisted.
|
||||
/// </summary>
|
||||
public int BlacklistSeconds { get; set; } = 60;
|
||||
|
||||
private readonly bool[] _validOption = BuildValidOptionTable();
|
||||
private readonly ConcurrentDictionary<IPAddress, IpState> _states = new();
|
||||
private bool _isDisposed;
|
||||
|
||||
public UdpPacketFilter()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fast synchronous check. Returns <c>true</c> when the packet must be
|
||||
/// dropped, <c>false</c> when it is allowed through.
|
||||
/// This path performs no allocations.
|
||||
/// </summary>
|
||||
public bool ShouldDrop(IPAddress ip, byte[] buffer, int length)
|
||||
{
|
||||
// 1. Size check (cheapest, no state).
|
||||
if (length < MinPacketLength || length > MaxPacketLength)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. Send-option byte check (layer-7, no state).
|
||||
if (!_validOption[buffer[0]])
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3. Per-IP connection burst limiting with a temporary blacklist.
|
||||
var now = Environment.TickCount64;
|
||||
var state = _states.GetOrAdd(ip, static _ => new IpState());
|
||||
|
||||
// Reset the window when the previous window expired, and lazily shed
|
||||
// blacklisted entries whose cooldown is over.
|
||||
if (now - state.WindowStart > 1000)
|
||||
{
|
||||
if (state.BlacklistUntil > 0 && state.BlacklistUntil <= now)
|
||||
{
|
||||
_states.TryRemove(ip, out _);
|
||||
return false;
|
||||
}
|
||||
|
||||
state.WindowStart = now;
|
||||
state.Count = 0;
|
||||
}
|
||||
|
||||
if (state.BlacklistUntil > now)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
state.Count++;
|
||||
if (state.Count <= BurstLimit)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Abuse detected: blacklist the IP for a while.
|
||||
state.BlacklistUntil = now + BlacklistSeconds * 1000L;
|
||||
state.Count = 0;
|
||||
Logger.Warning("UdpPacketFilter blacklisted {Ip} for {Seconds}s after too many connection attempts",
|
||||
ip, BlacklistSeconds);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool[] BuildValidOptionTable()
|
||||
{
|
||||
var table = new bool[256];
|
||||
// MessageType
|
||||
table[(byte)MessageType.Unreliable] = true;
|
||||
table[(byte)MessageType.Reliable] = true;
|
||||
// UdpSendOption
|
||||
table[(byte)UdpSendOption.Hello] = true;
|
||||
table[(byte)UdpSendOption.Ping] = true;
|
||||
table[(byte)UdpSendOption.Disconnect] = true;
|
||||
table[(byte)UdpSendOption.Acknowledgement] = true;
|
||||
table[(byte)UdpSendOption.Fragment] = true;
|
||||
return table;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isDisposed = true;
|
||||
_states.Clear();
|
||||
}
|
||||
|
||||
private sealed class IpState
|
||||
{
|
||||
public int Count;
|
||||
public long WindowStart;
|
||||
public long BlacklistUntil;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user