public IEnumerable <string> EnumerateAllowedIPAddresses() { string tempFile = OSUtility.GetTempFileName(); try { RunProcess("ipset", true, $"save > \"{tempFile}\""); bool inAllow = true; foreach (string line in File.ReadLines(tempFile)) { string[] pieces = line.Split(' '); if (pieces.Length > 1 && pieces[0].Equals("create", StringComparison.OrdinalIgnoreCase)) { inAllow = (pieces[1].Equals(AllowRuleName)); } else if (inAllow && pieces.Length > 2 && pieces[0] == "add") { yield return(pieces[2]); } } } finally { ExtensionMethods.FileDeleteWithRetry(tempFile); } }
/// <summary> /// Constructor /// </summary> /// <param name="onRun">Action to execute for run</param> /// <param name="onStop">Action to execute on stop</param> private IPBanServiceRunner(Func <CancellationToken, Task> onRun, Func <CancellationToken, Task> onStop) { Logger.Warn("Initializing service"); Directory.SetCurrentDirectory(AppContext.BaseDirectory); OSUtility.AddAppDomainExceptionHandlers(AppDomain.CurrentDomain); var hostBuilder = new HostBuilder() .ConfigureServices((hostContext, services) => { services.AddHostedService <IPBanServiceRunner>(provider => this); }); this.onRun = onRun; this.onStop = onStop; if (Microsoft.Extensions.Hosting.WindowsServices.WindowsServiceHelpers.IsWindowsService()) { Logger.Warn("Running as a Windows service"); hostBuilder.UseWindowsService(); } else if (Microsoft.Extensions.Hosting.Systemd.SystemdHelpers.IsSystemdService()) { Logger.Warn("Running as a systemd service"); hostBuilder.UseSystemd(); } else { // adding console lifetime wrecks things if actually running under a service Logger.Warn("Running as a console app"); hostBuilder.UseConsoleLifetime(); } hostBuilder.UseContentRoot(AppContext.BaseDirectory); host = hostBuilder.Build(); }
public override IEnumerable<string> EnumerateBannedIPAddresses() { string tempFile = OSUtility.GetTempFileName(); try { RunProcess("ipset", true, $"save > \"{tempFile}\""); bool inBlockRule = true; foreach (string line in File.ReadLines(tempFile)) { string[] pieces = line.Split(' '); if (pieces.Length > 1 && pieces[0].Equals("create", StringComparison.OrdinalIgnoreCase)) { inBlockRule = (!pieces[1].Equals(AllowRuleName) && (pieces[1].StartsWith(BlockRulePrefix) || pieces[1].StartsWith(RulePrefix + "6_Block_"))); } else if (inBlockRule && pieces.Length > 2 && pieces[0] == "add") { yield return pieces[2]; } } } finally { ExtensionMethods.FileDeleteWithRetry(tempFile); } }
public IEnumerable <IPAddressRange> EnumerateIPAddresses(string ruleNamePrefix = null) { string tempFile = OSUtility.GetTempFileName(); try { string prefix = RulePrefix + (ruleNamePrefix ?? string.Empty); RunProcess("ipset", true, $"save > \"{tempFile}\""); bool inSet = false; foreach (string line in File.ReadLines(tempFile)) { string[] pieces = line.Split(' '); if (pieces.Length > 1 && pieces[0].Equals("create", StringComparison.OrdinalIgnoreCase)) { inSet = (pieces[1].StartsWith(prefix, StringComparison.OrdinalIgnoreCase)); } else if (inSet && pieces.Length > 2 && pieces[0] == "add") { yield return(IPAddressRange.Parse(pieces[2])); } } } finally { ExtensionMethods.FileDeleteWithRetry(tempFile); } }
private static void LoadVersionFromWmic() { string tempFile = OSUtility.GetTempFileName(); // .net core WMI has a strange bug where WMI will not initialize on some systems // since this is the only place where WMI is used, we can just work-around it // with the wmic executable, which exists (as of 2018) on all supported Windows. // this process can hang and fail to run after windows update or other cases on system restart, // so put a short timeout in and fallback to WMI api if needed Exception lastError = null; Logger.Info("Attempting to load os info from wmic"); // attempt to run wmic to generate the info we want StartProcessAndWait(5000, "cmd", "/C wmic path Win32_OperatingSystem get Caption,Version /format:table > \"" + tempFile + "\""); // try up to 10 times to read the file in case file is still in use try { for (int i = 0; i < 10; i++) { try { // if this throws, we will try again string[] lines = File.ReadAllLines(tempFile); // if we have enough lines, try to parse them out if (lines.Length > 1) { int versionIndex = lines[0].IndexOf("Version"); if (versionIndex >= 0) { FriendlyName = lines[1].Substring(0, versionIndex - 1).Trim(); Version = lines[1].Substring(versionIndex).Trim(); return; } } throw new InvalidDataException("Invalid file generated by wmic"); } catch (Exception ex) { lastError = ex; if (ex is InvalidDataException) { break; } } Thread.Sleep(200); } } finally { ExtensionMethods.FileDeleteWithRetry(tempFile); } throw new ApplicationException("Unable to load os version using wmic", lastError); }
private async Task RunWindowsService(string[] args) { // if we have no console input and we are not in IIS, run as windows service if (Console.IsInputRedirected && !OSUtility.IsRunningInProcessIIS()) { // create and start using Windows service APIs windowsService = new IPBanWindowsServiceRunner(this, args); await windowsService.Run(); } else { await RunConsoleService(args); } }
private async Task RunWindowsService(string[] args) { // if we have no console input and we are not in IIS and not running an installer, run as windows service if (Console.IsInputRedirected && !OSUtility.IsRunningInProcessIIS() && !args.Any(a => a.StartsWith("-install", StringComparison.OrdinalIgnoreCase))) { // create and start using Windows service APIs windowsService = new IPBanWindowsServiceRunner(this, args); await windowsService.Run(); } else { await RunConsoleService(args); } }
/// <summary> /// Constructor /// </summary> /// <param name="onRun">Action to execute for run</param> /// <param name="onStop">Action to execute on stop</param> private IPBanServiceRunner(Func <CancellationToken, Task> onRun, Func <CancellationToken, Task> onStop) { Logger.Warn("Initializing service"); Directory.SetCurrentDirectory(AppContext.BaseDirectory); OSUtility.AddAppDomainExceptionHandlers(AppDomain.CurrentDomain); var hostBuilder = new HostBuilder() .ConfigureServices((hostContext, services) => { services.AddHostedService <IPBanServiceRunner>(provider => this); }); this.onRun = onRun; this.onStop = onStop; hostBuilder.UseContentRoot(AppContext.BaseDirectory); SetupHostService(hostBuilder); host = hostBuilder.Build(); }
/// <summary> /// Create a detached process /// </summary> /// <param name="fileName">File name to execute</param> /// <param name="arguments">Arguments</param> public static void CreateDetachedProcess(string fileName, string arguments) { if (OSUtility.IsWindows) { Logger.Warn("Running detached process {0} {1}", fileName, arguments); var processInformation = new ProcessUtility.PROCESS_INFORMATION(); var startupInfo = new ProcessUtility.STARTUPINFO(); var sa = new ProcessUtility.SECURITY_ATTRIBUTES(); sa.Length = Marshal.SizeOf(sa); CreateProcess(null, "\"" + fileName + "\" " + arguments, ref sa, ref sa, false, DETACHED_PROCESS, IntPtr.Zero, Path.GetDirectoryName(fileName), ref startupInfo, out processInformation); } else { // ensure process is executable OSUtility.StartProcessAndWait("sudo", "chmod +x \"" + fileName + "\""); // use Linux at, should have been installed earlier ProcessStartInfo info = new() { // Linux uses nohup and " &" to detach the process // sudo -b to force it into the background Arguments = "-c \"echo sudo \\\"" + fileName + "\\\" " + arguments.Replace("\"", "\\\"") + " | at now\"", CreateNoWindow = true, FileName = "/bin/bash", UseShellExecute = false, WindowStyle = ProcessWindowStyle.Hidden, WorkingDirectory = Path.GetDirectoryName(fileName) }; Logger.Warn("Running detached process {0} {1}", info.FileName, info.Arguments); // do not get a reference and do not dispose, this process is orphaned from this process Process.Start(info); } } }
/// <summary> /// Run the service /// </summary> /// <param name="requireAdministrator">True to require administrator, false otherwise</param> /// <returns>Exit code</returns> public async Task RunAsync(bool requireAdministrator = true) { if (requireAdministrator) { ExtensionMethods.RequireAdministrator(); } if (args.Length != 0 && (args[0].Equals("info", StringComparison.OrdinalIgnoreCase) || args[0].Equals("-info", StringComparison.OrdinalIgnoreCase))) { Logger.Warn("System info: {0}", OSUtility.OSString()); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { await RunWindowsService(args); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { await RunLinuxService(args); } else { throw new PlatformNotSupportedException(); } }
/// <summary> /// Initialize and start the service /// </summary> public async Task StartAsync() { if (IsRunning) { return; } try { IsRunning = true; ipDB = new IPBanDB(DatabasePath ?? "ipban.sqlite"); AddWindowsEventViewer(); AddUpdater(new IPBanUnblockIPAddressesUpdater(this, Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "unban.txt"))); AddUpdater(new IPBanBlockIPAddressesUpdater(this, Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ban.txt"))); AssemblyVersion = IPBanService.IPBanAssembly.GetName().Version.ToString(); await ReadAppSettings(); UpdateBannedIPAddressesOnStart(); IPBanDelegate?.Start(this); if (!ManualCycle) { if (RunFirstCycleRightAway) { await RunCycle(); // run one cycle right away } cycleTimer = new System.Timers.Timer(Config.CycleTime.TotalMilliseconds); cycleTimer.Elapsed += async(sender, e) => await CycleTimerElapsed(sender, e); cycleTimer.Start(); } Logger.Warn("IPBan {0} service started and initialized. Operating System: {1}", OSUtility.Name, OSUtility.OSString()); Logger.WriteLogLevels(); } catch (Exception ex) { Logger.Error("Critical error in IPBanService.Start", ex); } }
// deleteRule will drop the rule and matching set before creating the rule and set, use this is you don't care to update the rule and set in place protected bool UpdateRuleDelta(string ruleName, string action, IEnumerable <IPBanFirewallIPAddressDelta> deltas, string hashType, int maxCount, bool deleteRule, IEnumerable <PortRange> allowPorts, CancellationToken cancelToken) { string ipFileTemp = OSUtility.GetTempFileName(); try { // add and remove the appropriate ip addresses from the set using (StreamWriter writer = File.CreateText(ipFileTemp)) { if (cancelToken.IsCancellationRequested) { throw new OperationCanceledException(cancelToken); } writer.WriteLine($"create {ruleName} hash:{hashType} family {INetFamily} hashsize {hashSize} maxelem {maxCount} -exist"); foreach (IPBanFirewallIPAddressDelta delta in deltas) { if (cancelToken.IsCancellationRequested) { throw new OperationCanceledException(cancelToken); } if (IPAddressRange.TryParse(delta.IPAddress, out IPAddressRange range) && range.Begin.AddressFamily == addressFamily && range.End.AddressFamily == addressFamily) { try { if (delta.Added) { if (range.Begin.Equals(range.End)) { writer.WriteLine($"add {ruleName} {range.Begin} -exist"); } else { writer.WriteLine($"add {ruleName} {range.ToCidrString()} -exist"); } } else { if (range.Begin.Equals(range.End)) { writer.WriteLine($"del {ruleName} {range.Begin} -exist"); } else { writer.WriteLine($"del {ruleName} {range.ToCidrString()} -exist"); } } } catch { // ignore invalid cidr ranges } } } } if (cancelToken.IsCancellationRequested) { throw new OperationCanceledException(cancelToken); } else { // restore the deltas into the existing set bool result = (RunProcess("ipset", true, $"restore < \"{ipFileTemp}\"") == 0); CreateOrUpdateRule(ruleName, action, hashType, maxCount, allowPorts, cancelToken); return(result); } } finally { ExtensionMethods.FileDeleteWithRetry(ipFileTemp); } }
/// <summary> /// Static constructor /// </summary> static OSUtility() { try { tempFolder = Path.GetTempPath(); if (string.IsNullOrWhiteSpace(tempFolder)) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { tempFolder = "c:\\temp"; } else { tempFolder = "/tmp"; } } Directory.CreateDirectory(tempFolder); // start off with built in version info, this is not as detailed or nice as we like, // so we try some other ways to get more detailed information Version = Environment.OSVersion.VersionString; Description = RuntimeInformation.OSDescription; // attempt to get detailed version info if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { isLinux = true; string tempFile = OSUtility.GetTempFileName(); Process.Start("/bin/bash", "-c \"cat /etc/*release* > " + tempFile + "\"").WaitForExit(); System.Threading.Tasks.Task.Delay(100); // wait a small bit for file to really be closed string versionText = File.ReadAllText(tempFile).Trim(); ExtensionMethods.FileDeleteWithRetry(tempFile); if (string.IsNullOrWhiteSpace(versionText)) { Logger.Error(new IOException("Unable to load os version from /etc/*release* ...")); } else { Name = OSUtility.Linux; FriendlyName = ExtractRegex(versionText, "^(Id|Distrib_Id)=(?<value>.*?)$", string.Empty); if (FriendlyName.Length != 0) { string codeName = ExtractRegex(versionText, "^(Name|Distrib_CodeName)=(?<value>.+)$", string.Empty); if (codeName.Length != 0) { FriendlyName += " - " + codeName; } Version = ExtractRegex(versionText, "^Version_Id=(?<value>.+)$", Version); } } } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { isWindows = true; processVerb = "runas"; Name = OSUtility.Windows; string tempFile = OSUtility.GetTempFileName(); // .net core WMI has a strange bug where WMI will not initialize on some systems // since this is the only place where WMI is used, we can just work-around it // with the wmic executable, which exists (as of 2018) on all supported Windows. StartProcessAndWait("cmd", "/C wmic path Win32_OperatingSystem get Caption,Version /format:table > \"" + tempFile + "\""); if (File.Exists(tempFile)) { // try up to 10 times to read the file for (int i = 0; i < 10; i++) { try { string[] lines = File.ReadAllLines(tempFile); ExtensionMethods.FileDeleteWithRetry(tempFile); if (lines.Length > 1) { int versionIndex = lines[0].IndexOf("Version"); if (versionIndex >= 0) { FriendlyName = lines[1].Substring(0, versionIndex - 1).Trim(); Version = lines[1].Substring(versionIndex).Trim(); break; } } throw new IOException("Invalid file generated from wmic"); } catch (Exception ex) { if (i < 9) { System.Threading.Tasks.Task.Delay(200).Wait(); } else { Logger.Error(ex, "Unable to load os version using wmic, trying wmi api..."); // last resort, try wmi api LoadVersionFromWmiApi(); } } } } else { // last resort, try wmi api LoadVersionFromWmiApi(); } } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { // TODO: Implement better for MAC isMac = true; Name = OSUtility.Mac; FriendlyName = "OSX"; } else { Name = OSUtility.Unknown; FriendlyName = "Unknown"; } } catch (Exception ex) { Logger.Error("Error determining platform info", ex); } }
// deleteRule will drop the rule and matching set before creating the rule and set, use this is you don't care to update the rule and set in place protected bool UpdateRule(string ruleName, string action, IEnumerable <string> ipAddresses, string hashType, int maxCount, IEnumerable <PortRange> allowPorts, CancellationToken cancelToken) { #if ENABLE_FIREWALL_PROFILING Stopwatch timer = Stopwatch.StartNew(); #endif string ipFileTemp = OSUtility.GetTempFileName(); try { // add and remove the appropriate ip addresses from the set using (StreamWriter writer = File.CreateText(ipFileTemp)) { if (cancelToken.IsCancellationRequested) { throw new OperationCanceledException(cancelToken); } RunProcess("ipset", true, out IReadOnlyList <string> sets, "-L -n"); if (sets.Contains(ruleName)) { writer.WriteLine($"flush {ruleName}");// hash:{hashType} family {INetFamily} hashsize {hashSize} maxelem {maxCount} -exist"); } writer.WriteLine($"create {ruleName} hash:{hashType} family {INetFamily} hashsize {hashSize} maxelem {maxCount} -exist"); foreach (string ipAddress in ipAddresses) { if (cancelToken.IsCancellationRequested) { throw new OperationCanceledException(cancelToken); } if (IPAddressRange.TryParse(ipAddress, out IPAddressRange range) && range.Begin.AddressFamily == addressFamily && range.End.AddressFamily == addressFamily) { try { if (hashType != hashTypeCidrMask || range.Begin.Equals(range.End)) { writer.WriteLine($"add {ruleName} {range.Begin} -exist"); } else { writer.WriteLine($"add {ruleName} {range.ToCidrString()} -exist"); } } catch { // ignore invalid cidr ranges } } } } if (cancelToken.IsCancellationRequested) { throw new OperationCanceledException(cancelToken); } else { // restore the set bool result = (RunProcess("ipset", true, $"restore < \"{ipFileTemp}\"") == 0); CreateOrUpdateRule(ruleName, action, hashType, maxCount, allowPorts, cancelToken); return(result); } } finally { ExtensionMethods.FileDeleteWithRetry(ipFileTemp); #if ENABLE_FIREWALL_PROFILING timer.Stop(); Logger.Warn("BlockIPAddressesDelta rule '{0}' took {1:0.00}ms with {2} ips", ruleName, timer.Elapsed.TotalMilliseconds, ipAddresses.Count()); #endif } }
private async Task ProcessPendingFailedLogins(IReadOnlyList <IPAddressLogEvent> ipAddresses) { List <IPAddressLogEvent> bannedIpAddresses = new List <IPAddressLogEvent>(); object transaction = BeginTransaction(); try { foreach (IPAddressLogEvent failedLogin in ipAddresses) { try { string ipAddress = failedLogin.IPAddress; string userName = failedLogin.UserName; string source = failedLogin.Source; if (IsWhitelisted(ipAddress)) { Logger.Warn("Login failure, ignoring whitelisted ip address {0}, {1}, {2}", ipAddress, userName, source); } else { int maxFailedLoginAttempts; if (Config.IsWhitelisted(userName)) { maxFailedLoginAttempts = Config.FailedLoginAttemptsBeforeBanUserNameWhitelist; } else { // see if there is an override for max failed login attempts maxFailedLoginAttempts = (failedLogin.FailedLoginThreshold > 0 ? failedLogin.FailedLoginThreshold : Config.FailedLoginAttemptsBeforeBan); } DateTime now = failedLogin.Timestamp; // check for the target user name for additional blacklisting checks bool ipBlacklisted = Config.IsBlackListed(ipAddress); bool userBlacklisted = (ipBlacklisted ? false : Config.IsBlackListed(userName)); bool userFailsWhitelistRegex = (userBlacklisted ? false : Config.UserNameFailsUserNameWhitelistRegex(userName)); bool editDistanceBlacklisted = (ipBlacklisted || userBlacklisted || userFailsWhitelistRegex ? false : !Config.IsUserNameWithinMaximumEditDistanceOfUserNameWhitelist(userName)); bool configBlacklisted = ipBlacklisted || userBlacklisted || userFailsWhitelistRegex || editDistanceBlacklisted; // if the event came in with a count of 0 that means it is an automatic ban int incrementCount = (failedLogin.Count < 1 ? maxFailedLoginAttempts : failedLogin.Count); int newCount = ipDB.IncrementFailedLoginCount(ipAddress, userName, source, UtcNow, incrementCount, transaction); Logger.Warn(now, "Login failure: {0}, {1}, {2}, {3}", ipAddress, userName, source, newCount); // if the ip address is black listed or the ip address has reached the maximum failed login attempts before ban, ban the ip address if (configBlacklisted || newCount >= maxFailedLoginAttempts) { Logger.Info("IP blacklisted: {0}, user name blacklisted: {1}, fails user name white list regex: {2}, user name edit distance blacklisted: {3}", ipBlacklisted, userBlacklisted, userFailsWhitelistRegex, editDistanceBlacklisted); if (ipDB.TryGetIPAddressState(ipAddress, out IPBanDB.IPAddressState? state, transaction) && (state.Value == IPBanDB.IPAddressState.Active || state.Value == IPBanDB.IPAddressState.AddPending)) { Logger.Warn(now, "IP {0}, {1}, {2} ban pending.", ipAddress, userName, source); } else { Logger.Debug("Failed login count {0} >= ban count {1}{2}", newCount, maxFailedLoginAttempts, (configBlacklisted ? " config blacklisted" : string.Empty)); // if delegate and non-zero count, forward on - count of 0 means it was from external source, like a delegate if (IPBanDelegate != null && !failedLogin.External) { await IPBanDelegate.LoginAttemptFailed(ipAddress, source, userName, MachineGuid, OSName, OSVersion, UtcNow); } AddBannedIPAddress(ipAddress, source, userName, bannedIpAddresses, now, configBlacklisted, newCount, string.Empty, transaction, failedLogin.External); } } else { Logger.Debug("Failed login count {0} <= ban count {1}", newCount, maxFailedLoginAttempts); if (OSUtility.UserIsActive(userName)) { Logger.Warn("Login failed for known active user {0}", userName); } // if delegate and non-zero count, forward on - count of 0 means it was from external source, like a delegate if (IPBanDelegate != null && !failedLogin.External) { await IPBanDelegate.LoginAttemptFailed(ipAddress, source, userName, MachineGuid, OSName, OSVersion, UtcNow); } } } } catch (Exception ex) { Logger.Error(ex); } } CommitTransaction(transaction); ExecuteExternalProcessForBannedIPAddresses(bannedIpAddresses); } catch (Exception ex) { RollbackTransaction(transaction); Logger.Error(ex); } }
private static void LoadOSInfo() { tempFolder = Path.GetTempPath(); if (string.IsNullOrWhiteSpace(tempFolder)) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { tempFolder = "c:\\temp"; } else { tempFolder = "/tmp"; } } Directory.CreateDirectory(tempFolder); // start off with built in version info, this is not as detailed or nice as we like, // so we try some other ways to get more detailed information CpuArchitecture = RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant(); Version = Environment.OSVersion.Version.ToString(); Description = RuntimeInformation.OSDescription; // attempt to get detailed version info if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { Name = FriendlyName = OSUtility.Linux; isLinux = true; string tempFile = OSUtility.GetTempFileName(); Process.Start("/bin/bash", "-c \"cat /etc/*release* > " + tempFile + "\"").WaitForExit(); System.Threading.Tasks.Task.Delay(100); // wait a small bit for file to really be closed string versionText = File.ReadAllText(tempFile).Trim(); ExtensionMethods.FileDeleteWithRetry(tempFile); if (string.IsNullOrWhiteSpace(versionText)) { Logger.Error(new IOException("Unable to load os version from /etc/*release* ...")); } else { FriendlyName = ExtractRegex(versionText, "^(Id|Distrib_Id)=(?<value>.*?)$", string.Empty); if (FriendlyName.Length != 0) { string codeName = ExtractRegex(versionText, "^(Name|Distrib_CodeName)=(?<value>.+)$", string.Empty); if (codeName.Length != 0) { FriendlyName += " - " + codeName; } Version = ExtractRegex(versionText, "^Version_Id=(?<value>.+)$", Version); } } } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { Name = FriendlyName = OSUtility.Windows; isWindows = true; processVerb = "runas"; try { LoadVersionFromWmic(); } catch (Exception ex) { Logger.Error(ex, "Failed to load os info from wmic"); // last resort, try wmi api LoadVersionFromWmiApi(); } } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { // TODO: Implement better for MAC Name = FriendlyName = OSUtility.Mac; FriendlyName = "OSX"; isMac = true; } else { Name = OSUtility.Unknown; FriendlyName = "Unknown"; } }