## Breaking Changes * The Firewall and IP Limiter have been rewritten. Please read the notes carefully! * `TcpServer.Instances` moved back to `NetState.Instances` - sorry - it was stupid to move it to begin with. > [!Note] > Sockets that fail the IP Limiter or Firewall will be immediately and forcibly disconnected. > This means they will be stuck at "Verifying account..." if it was a real client. ### Summary - Removes firewall wildcard support. - Removes `AccessRestrictions`. - Moves Firewall/IPLimiter to the core. - Moves `TcpServer` to its own thread. - Removes the `SocketConnect` and `SocketDisconnect` event sinks. - Moves `Instances` back to `NetState.Instances`. - Fixes a long standing bug with bad handling of duplicate listener addresses. #### Firewall The firewall has been completely rewritten. There is now an "Admin Firewall" which saves to the config file. Secondarily, there is an internal firewall used exclusively by the TcpServer while processing sockets. The Admin firewall mirrors it's additions/deletions to the internal firewall by adding requests to a queue. > [!IMPORTANT] > **Wildcard firewall entries, such as `X`, `*`, `?` are not allowed.** > **Ranges in between IP classes or sextets are not allowed.** > **Please make sure to use one of the following:** > * IP Address - `192.168.1.1` > * CIDR - `192.168.1.0/24` > * Range - `192.168.1.1-192.168.1.100` #### IP Limiter The IP Limiter has been completely rewritten. The available configurations are: ```json "ipLimiter.enable": "True", "ipLimiter.maxConnectionsPerIP": 10, "ipLimiter.clearConnectionAttemptsDuration": "00:00:00:10", "ipLimiter.clearThrottledDuration": "00:00:02:00", ``` The IP Limiter is set up to prevent spamming connections from the same IP. Every time an IP connects, it is added to a connection list. After 10 attempts, the IP is added to the throttle list. To keep the system fast, the connection list is entirely wiped every 10 seconds, and the throttle list is entirely wiped every 2 minutes.
216 lines
7.7 KiB
C#
216 lines
7.7 KiB
C#
using System;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Net.NetworkInformation;
|
|
using System.Net.Sockets;
|
|
using Server.Logging;
|
|
using Server.Network;
|
|
|
|
namespace Server.Misc
|
|
{
|
|
/*
|
|
* The default settings are configured to automatically detect your external IP address.
|
|
*
|
|
* If your public IP address cannot be determined then set your IP address in modernuo.json, for example:
|
|
* "serverListing.address": "1.2.3.4"
|
|
*
|
|
* If you do not plan on allowing clients outside of your LAN to connect, then set the following in modernuo.json
|
|
* "serverListing.address": null,
|
|
* "serverListing.autoDetect": false
|
|
*
|
|
* If you want players outside your LAN to be able to connect to your server and you are behind a router, you must also
|
|
* forward TCP port 2593 to your private IP address. The procedure for doing this varies by manufacturer but generally
|
|
* involves configuration of the router through your web browser.
|
|
*
|
|
* ServerList will direct connecting clients depending on both the address they are connecting from and the address and
|
|
* port they are connecting to. If it is determined that both ends of a connection are private IP addresses, ServerList
|
|
* will direct the client to the local private IP address. If a client is connecting to a local public IP address, they
|
|
* will be directed to whichever address and port they initially connected to. This allows multi-homed servers to function
|
|
* properly and fully supports listening on multiple ports. If a client with a public IP address is connecting to a
|
|
* locally private address, the server will direct the client to either the automatically detected IP address or the
|
|
* manually entered IP address or hostname, whichever is applicable. Loopback clients will be directed to loopback.
|
|
*/
|
|
public static class ServerList
|
|
{
|
|
private static readonly ILogger logger = LogFactory.GetLogger(typeof(ServerList));
|
|
|
|
private static IPAddress _publicAddress;
|
|
public static string Address { get; private set; }
|
|
public static string ServerName { get; private set; }
|
|
|
|
public static bool AutoDetect { get; private set; }
|
|
|
|
public static void Configure()
|
|
{
|
|
Address = ServerConfiguration.GetOrUpdateSetting("serverListing.address", null);
|
|
AutoDetect = ServerConfiguration.GetOrUpdateSetting("serverListing.autoDetect", true);
|
|
ServerName = ServerConfiguration.GetOrUpdateSetting("serverListing.serverName", "ModernUO");
|
|
}
|
|
|
|
public static void Initialize()
|
|
{
|
|
if (Address == null)
|
|
{
|
|
if (AutoDetect)
|
|
{
|
|
AutoDetection();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Resolve(Address, out _publicAddress);
|
|
}
|
|
|
|
EventSink.ServerList += EventSink_ServerList;
|
|
}
|
|
|
|
private static void EventSink_ServerList(ServerListEventArgs e)
|
|
{
|
|
try
|
|
{
|
|
var ns = e.State;
|
|
|
|
var ipep = (IPEndPoint)ns.Connection?.LocalEndPoint;
|
|
if (ipep == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var localAddress = ipep.Address;
|
|
var localPort = ipep.Port;
|
|
|
|
if (IsPrivateNetwork(localAddress))
|
|
{
|
|
ipep = (IPEndPoint)ns.Connection.RemoteEndPoint;
|
|
if (ipep == null || !IsPrivateNetwork(ipep.Address) && _publicAddress != null)
|
|
{
|
|
localAddress = _publicAddress;
|
|
}
|
|
}
|
|
|
|
e.AddServer(ServerName, new IPEndPoint(localAddress, localPort));
|
|
}
|
|
catch (Exception er)
|
|
{
|
|
logger.Warning(er, "Unhandled exception at server list");
|
|
e.Rejected = true;
|
|
}
|
|
}
|
|
|
|
private static void AutoDetection()
|
|
{
|
|
if (!HasPublicIPAddress())
|
|
{
|
|
_publicAddress = FindPublicAddress();
|
|
|
|
if (_publicAddress != null)
|
|
{
|
|
logger.Information("Auto-detected public IP address ({IPAddress})", _publicAddress);
|
|
}
|
|
else
|
|
{
|
|
logger.Error("Could not auto-detect public IP address. Users will not be able to connect!");
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void Resolve(string addr, out IPAddress outValue)
|
|
{
|
|
if (IPAddress.TryParse(addr, out outValue))
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
var iphe = Dns.GetHostEntry(addr);
|
|
|
|
if (iphe.AddressList.Length > 0)
|
|
{
|
|
outValue = iphe.AddressList[^1];
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignored
|
|
}
|
|
}
|
|
|
|
private static bool HasPublicIPAddress()
|
|
{
|
|
foreach (var adapter in NetworkInterface.GetAllNetworkInterfaces())
|
|
{
|
|
foreach (var unicast in adapter.GetIPProperties().UnicastAddresses)
|
|
{
|
|
var ip = unicast.Address;
|
|
if (!IPAddress.IsLoopback(ip) && ip.AddressFamily != AddressFamily.InterNetworkV6 &&
|
|
!IsPrivateNetwork(ip))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool IsPrivateNetwork(IPAddress ip) =>
|
|
ip.AddressFamily switch
|
|
{
|
|
AddressFamily.InterNetwork => IsPrivateNetworkV4(ip),
|
|
AddressFamily.InterNetworkV6 => IsPrivateNetworkV6(ip),
|
|
_ => false
|
|
};
|
|
|
|
private static readonly IFirewallEntry[] _privateNetworkV4 =
|
|
[
|
|
new CidrFirewallEntry("192.168.0.0/16"),
|
|
new CidrFirewallEntry("10.0.0.0/8"),
|
|
new CidrFirewallEntry("172.16.0.0/12"),
|
|
new CidrFirewallEntry("169.254.0.0/16"),
|
|
new CidrFirewallEntry("100.64.0.0/10")
|
|
];
|
|
|
|
private static readonly IFirewallEntry[] _privateNetworkV6 =
|
|
[
|
|
new CidrFirewallEntry("fc00::/7"),
|
|
new CidrFirewallEntry("fe80::/10")
|
|
];
|
|
|
|
private static bool IsPrivateNetworkV4(IPAddress ip) =>
|
|
_privateNetworkV4[0].IsBlocked(ip) ||
|
|
_privateNetworkV4[1].IsBlocked(ip) ||
|
|
_privateNetworkV4[2].IsBlocked(ip) ||
|
|
_privateNetworkV4[3].IsBlocked(ip) ||
|
|
_privateNetworkV4[4].IsBlocked(ip);
|
|
|
|
private static bool IsPrivateNetworkV6(IPAddress ip) =>
|
|
_privateNetworkV6[0].IsBlocked(ip) ||
|
|
_privateNetworkV6[1].IsBlocked(ip);
|
|
|
|
private const string _ipifyUrl = "https://api.ipify.org";
|
|
|
|
private static IPAddress FindPublicAddress()
|
|
{
|
|
const int count = 3;
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
try
|
|
{
|
|
// This isn't called often so we don't need to optimize
|
|
using HttpClient hc = new HttpClient();
|
|
hc.Timeout = TimeSpan.FromSeconds(1); // Only wait 1 second
|
|
var ipAddress = hc.GetStringAsync(_ipifyUrl).Result;
|
|
return IPAddress.Parse(ipAddress);
|
|
}
|
|
catch
|
|
{
|
|
// ignored
|
|
}
|
|
}
|
|
|
|
logger.Warning("Attempted to get a public IP address {Count} times from {RemoteIPService} and failed.", count, _ipifyUrl);
|
|
return null;
|
|
}
|
|
}
|
|
}
|