From d48c7f55b60ca9b1452230a82f6dee6a951855d6 Mon Sep 17 00:00:00 2001 From: Fabian Date: Sun, 21 May 2023 21:30:11 +0000 Subject: [PATCH] Filter the game portal host for specific CIDR ranges: - This will make sure that the developer mode is disabled on most public hosts. - Enable Developer mode by default. --- src/LaunchOptions.cs | 10 ++++-- src/Launcher.cs | 27 +++++++++++++-- src/Misc/Helpers.cs | 38 +++++++++++++++++++++ src/Misc/IPFilter.cs | 81 ++++++++++++++++++++++++++++++++++++++++++++ src/Program.cs | 35 ++++++++++++++++++- 5 files changed, 186 insertions(+), 5 deletions(-) create mode 100644 src/Misc/IPFilter.cs diff --git a/src/LaunchOptions.cs b/src/LaunchOptions.cs index e521b3f..4ab6977 100644 --- a/src/LaunchOptions.cs +++ b/src/LaunchOptions.cs @@ -8,12 +8,17 @@ namespace Arctium.WoW.Launcher; static class LaunchOptions { + public static bool IsDevModeAllowed { get; set; } + public static Option Version = new("--version", () => GameVersion.Retail); public static Option GamePath = new("--path"); public static Option GameBinary = new("--binary"); public static Option KeepCache = new("--keepcache", () => true); public static Option UseStaticAuthSeed = new("--staticseed"); - public static Option DevMode = new("--dev", () => false); + public static Option DevMode = new("--dev", () => true); + + // Game command line options. + public static Option GameConfig = new("-config", () => "Config.wtf"); public static Parser Instance => new CommandLineBuilder(ConfigureCommandLine(RootCommand)) .UseHelp() @@ -30,7 +35,8 @@ static class LaunchOptions GameBinary, KeepCache, UseStaticAuthSeed, - DevMode + DevMode, + GameConfig }; static Command ConfigureCommandLine(Command rootCommand) diff --git a/src/Launcher.cs b/src/Launcher.cs index 1bb1799..18a5353 100644 --- a/src/Launcher.cs +++ b/src/Launcher.cs @@ -10,7 +10,7 @@ static class Launcher { public static readonly CancellationTokenSource CancellationTokenSource = new(); - public static string PrepareGameLaunch(ParseResult commandLineResult) + public static string PrepareGameLaunch(ParseResult commandLineResult, IPFilter ipFilter) { var gameVersion = commandLineResult.GetValueForOption(LaunchOptions.Version); var (SubFolder, BinaryName, MajorGameVersion, MinGameBuild) = gameVersion switch @@ -86,6 +86,27 @@ static class Launcher } } + var configPath = $"{gameFolder}/WTF/{commandLineResult.GetValueForOption(LaunchOptions.GameConfig)}"; + + if (!File.Exists(configPath)) + LaunchOptions.IsDevModeAllowed = false; + else + { + var config = File.ReadAllText(configPath); + + LaunchOptions.IsDevModeAllowed = IsDevModeAllowed(ipFilter, config); + } + + if (!LaunchOptions.IsDevModeAllowed) + LaunchOptions.DevMode = new("--dev", () => false); + + var devModeEnabled = commandLineResult.GetValueForOption(LaunchOptions.DevMode) && LaunchOptions.IsDevModeAllowed; + + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"Developer mode: {(devModeEnabled ? "Enabled" : "Disabled")}"); + Console.WriteLine(); + Console.ForegroundColor = ConsoleColor.Gray; + return gameBinaryPath; } @@ -177,7 +198,7 @@ static class Launcher memory.QueuePatch(Patterns.Windows.CertCommonName, Patches.Windows.CertCommonName, "CertCommonName", 5) }, CancellationTokenSource.Token); } - else if (commandLineResult.GetValueForOption(LaunchOptions.DevMode)) + else if (LaunchOptions.IsDevModeAllowed && commandLineResult.GetValueForOption(LaunchOptions.DevMode)) { Task.WaitAll(new[] { @@ -274,6 +295,8 @@ static class Launcher return false; } + static bool IsDevModeAllowed(IPFilter ipFilter, string config) => ipFilter.IsInRange(ParsePortal(config)); + static long GenerateAuthSeedFunctionPatch(WinMemory memory, long modulusOffset) { #if x64 diff --git a/src/Misc/Helpers.cs b/src/Misc/Helpers.cs index e54d9ee..cd448cb 100644 --- a/src/Misc/Helpers.cs +++ b/src/Misc/Helpers.cs @@ -1,6 +1,8 @@ // Copyright (c) Arctium. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Net; + namespace Arctium.WoW.Launcher.Misc; static class Helpers @@ -41,4 +43,40 @@ static class Helpers Console.WriteLine(); Console.WriteLine($"Operating System: {RuntimeInformation.OSDescription}"); } + + public static ReadOnlySpan ParsePortal(string config) + { + const string portalKey = "SET portal"; + + var portalIndex = config.IndexOf(portalKey, StringComparison.Ordinal); + + if (portalIndex == -1) + throw new ArgumentException("Config file does not contain the portal variable."); + + var startQuoteIndex = config.IndexOf('"', portalIndex); + + if (startQuoteIndex == -1) + throw new ArgumentException("Invalid format for the portal variable."); + + var endQuoteIndex = config.IndexOf('"', startQuoteIndex + 1); + + if (endQuoteIndex == -1) + throw new ArgumentException("Invalid format for the portal variable."); + + var portalLength = endQuoteIndex - startQuoteIndex - 1; + var portalSpan = config.AsSpan(startQuoteIndex + 1, portalLength); + var colonIndex = portalSpan.IndexOf(':'); + var ipSpan = colonIndex != -1 ? portalSpan[..colonIndex] : portalSpan; + var portalString = ipSpan.ToString().Trim(); + + if (IPAddress.TryParse(portalString, out var ipAddress)) + return ipAddress.ToString().AsSpan(); + + var ipv4Address = Dns.GetHostAddresses(portalString).FirstOrDefault(a => a.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork); + + if (ipv4Address == null) + throw new Exception("No IPv4 address found for the provided hostname."); + + return ipv4Address.ToString().AsSpan(); + } } diff --git a/src/Misc/IPFilter.cs b/src/Misc/IPFilter.cs new file mode 100644 index 0000000..600ad26 --- /dev/null +++ b/src/Misc/IPFilter.cs @@ -0,0 +1,81 @@ +// Copyright (c) Arctium. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Net; + +namespace Arctium.WoW.Launcher.Misc; + +public class IPFilter +{ + readonly ICollection<(IPAddress, IPAddress)> _ipRanges; + + public IPFilter() + { + _ipRanges = new List<(IPAddress, IPAddress)>(); + } + + public void AddCidrRange(ReadOnlySpan cidr) + { + var separatorIndex = cidr.IndexOf('/'); + + if (separatorIndex < 0) + return; + + var subnetMaskLengthSpan = cidr[(separatorIndex + 1)..]; + + if (!int.TryParse(subnetMaskLengthSpan, out var subnetMaskLength)) + return; + + Span subnetMaskBytes = stackalloc byte[4]; + + for (var i = 0; i < subnetMaskLength; i++) + subnetMaskBytes[i / 8] |= (byte)(1 << (7 - i % 8)); + + if (!IPAddress.TryParse(cidr[..separatorIndex], out var ip)) + return; + + ReadOnlySpan ipBytes = ip.GetAddressBytes(); + + Span networkAddressBytes = stackalloc byte[4]; + Span broadcastAddressBytes = stackalloc byte[4]; + + for (var i = 0; i < 4; i++) + { + networkAddressBytes[i] = (byte)(ipBytes[i] & subnetMaskBytes[i]); + broadcastAddressBytes[i] = (byte)(networkAddressBytes[i] | ~subnetMaskBytes[i]); + } + + _ipRanges.Add((new(networkAddressBytes), new(broadcastAddressBytes))); + } + + public bool IsInRange(ReadOnlySpan targetIP) + { + if (!IPAddress.TryParse(targetIP, out var ip)) + return false; + + ReadOnlySpan targetBytes = ip.GetAddressBytes(); + + foreach (var range in _ipRanges) + { + ReadOnlySpan networkAddressBytes = range.Item1.GetAddressBytes(); + ReadOnlySpan broadcastAddressBytes = range.Item2.GetAddressBytes(); + + var isInRange = true; + + for (var i = 0; i < 4; i++) + { + if ((targetBytes[i] & networkAddressBytes[i]) != networkAddressBytes[i] || + (targetBytes[i] & broadcastAddressBytes[i]) != targetBytes[i]) + { + isInRange = false; + break; + } + } + + if (isInRange) + return true; + } + + return false; + } +} diff --git a/src/Program.cs b/src/Program.cs index 7aa63e6..92997ab 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -13,15 +13,48 @@ PrintHeader("WoW Client Launcher"); LaunchOptions.RootCommand.SetHandler(context => { - var appPath = Launcher.PrepareGameLaunch(context.ParseResult); + CreateDevIPFilter(out var ipFilter); + + var appPath = Launcher.PrepareGameLaunch(context.ParseResult, ipFilter); var gameCommandLine = string.Join(" ", context.ParseResult.UnmatchedTokens); + // Add config parameter to the game command line. + gameCommandLine += $" -config {context.ParseResult.GetValueForOption(LaunchOptions.GameConfig)}"; + if (string.IsNullOrEmpty(appPath) || !Launcher.LaunchGame(appPath, gameCommandLine, context.ParseResult)) WaitAndExit(5000); }); await LaunchOptions.Instance.InvokeAsync(args); +void CreateDevIPFilter(out IPFilter ipFilter) +{ + ipFilter = new IPFilter(); + + ipFilter.AddCidrRange("0.0.0.0/8"); + ipFilter.AddCidrRange("10.0.0.0/8"); + ipFilter.AddCidrRange("100.64.0.0/10"); + ipFilter.AddCidrRange("127.0.0.0/8"); + ipFilter.AddCidrRange("169.254.0.0/16"); + ipFilter.AddCidrRange("172.16.0.0/12"); + ipFilter.AddCidrRange("192.0.0.0/24"); + ipFilter.AddCidrRange("192.0.0.0/29"); + ipFilter.AddCidrRange("192.0.0.8/32"); + ipFilter.AddCidrRange("192.0.0.9/32"); + ipFilter.AddCidrRange("192.0.0.170/32"); + ipFilter.AddCidrRange("192.0.0.171/32"); + ipFilter.AddCidrRange("192.0.2.0/24"); + ipFilter.AddCidrRange("192.31.196.0/24"); + ipFilter.AddCidrRange("192.52.193.0/24"); + ipFilter.AddCidrRange("192.88.99.0/24"); + ipFilter.AddCidrRange("192.168.0.0/16"); + ipFilter.AddCidrRange("192.175.48.0/24"); + ipFilter.AddCidrRange("198.18.0.0/15"); + ipFilter.AddCidrRange("198.51.100.0/24"); + ipFilter.AddCidrRange("203.0.113.0/24"); + ipFilter.AddCidrRange("240.0.0.0/4"); + ipFilter.AddCidrRange("255.255.255.255/32"); +} static void WaitAndExit(int ms = 2000) {