diff --git a/IPBanCore/Core/IPBan/IPBanConfig.cs b/IPBanCore/Core/IPBan/IPBanConfig.cs index 89a7e4a2..47df9436 100644 --- a/IPBanCore/Core/IPBan/IPBanConfig.cs +++ b/IPBanCore/Core/IPBan/IPBanConfig.cs @@ -113,6 +113,8 @@ public void Dispose() private readonly TimeSpan cycleTime = TimeSpan.FromMinutes(1.0d); private readonly TimeSpan minimumTimeBetweenFailedLoginAttempts = TimeSpan.FromSeconds(5.0); private readonly TimeSpan minimumTimeBetweenSuccessfulLoginAttempts = TimeSpan.FromSeconds(5.0); + + private readonly string ipThreatApiKey = string.Empty; private readonly int failedLoginAttemptsBeforeBan = 5; private readonly bool resetFailedLoginCountForUnbannedIPAddresses; private readonly string firewallRulePrefix = "IPBan_"; @@ -180,6 +182,7 @@ private IPBanConfig(XmlDocument doc, IDnsLookup dns = null, IDnsServerList dnsLi appSettings[node.Attributes["key"].Value] = node.Attributes["value"].Value; } + GetConfig("IPThreatApiKey", ref ipThreatApiKey); GetConfig("FailedLoginAttemptsBeforeBan", ref failedLoginAttemptsBeforeBan, 1, 50); GetConfig("ResetFailedLoginCountForUnbannedIPAddresses", ref resetFailedLoginCountForUnbannedIPAddresses); GetConfigArray("BanTime", ref banTimes, emptyTimeSpanArray); @@ -881,6 +884,11 @@ appSettingsOverride is not null && /// public IReadOnlyDictionary AppSettings => appSettings; + /// + /// Api key from https://ipthreat.net, if any + /// + public string IPThreatApiKey { get { return ipThreatApiKey; } } + /// /// Number of failed login attempts before a ban is initiated /// diff --git a/IPBanCore/Core/IPBan/IPBanIPThreatUploader.cs b/IPBanCore/Core/IPBan/IPBanIPThreatUploader.cs new file mode 100644 index 00000000..8aa56096 --- /dev/null +++ b/IPBanCore/Core/IPBan/IPBanIPThreatUploader.cs @@ -0,0 +1,129 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using System.Threading; +using System.Collections.Generic; +using System.Linq; +using System.Globalization; +using System.Reflection; +using System.Diagnostics.CodeAnalysis; + +namespace DigitalRuby.IPBanCore; + +/// +/// Sync failed logins to ipthreat api +/// +public sealed class IPBanIPThreatUploader : IUpdater, IIPAddressEventHandler +{ + private static readonly Uri ipThreatReportApiUri = new("https://api.ipthreat.net/api/bulkreport"); + + private readonly IIPBanService service; + private readonly Random random = new(); + private readonly List events = new(); + + private DateTime nextRun; + + /// + /// Constructor + /// + /// Service + public IPBanIPThreatUploader(IIPBanService service) + { + this.service = service; + nextRun = IPBanService.UtcNow;//.AddMinutes(random.Next(30, 91)); + } + + /// + public void Dispose() + { + + } + + /// + public async Task Update(CancellationToken cancelToken = default) + { + // ready to run? + var now = IPBanService.UtcNow; + if (now < nextRun) + { + return; + } + + // do we have an api key? + var apiKey = (service.Config.IPThreatApiKey ?? string.Empty).Trim(); + if (string.IsNullOrWhiteSpace(apiKey)) + { + return; + } + // api key can be read from env var if starts and ends with % + else if (apiKey.StartsWith("%") && apiKey.EndsWith("%")) + { + apiKey = Environment.GetEnvironmentVariable(apiKey.Trim('%')); + if (string.IsNullOrWhiteSpace(apiKey)) + { + return; + } + } + + // copy events + IReadOnlyCollection eventsCopy; + lock (events) + { + eventsCopy = events.ToArray(); + events.Clear(); + } + if (eventsCopy.Count == 0) + { + return; + } + + // post json + try + { + /* + [{ + "ip": "1.2.3.4", + "flags": "None", + "system": "SMTP", + "notes": "Failed password", + "ts": "2022-09-02T15:24:07.842Z", + "count": 1 + }] + */ + var transform = + eventsCopy.Select(e => new + { + ip = e.IPAddress, + flags = "BruteForce", + system = e.Source, + notes = "ipban " + Assembly.GetEntryAssembly()?.GetName().Version?.ToString(3), + ts = e.Timestamp.ToString("s", CultureInfo.InvariantCulture) + "Z", + count = e.Count + }); + var jsonObj = new { items = transform }; + // have to use newtonsoft here + var postJson = System.Text.Encoding.UTF8.GetBytes(Newtonsoft.Json.JsonConvert.SerializeObject(jsonObj)); + await service.RequestMaker.MakeRequestAsync(ipThreatReportApiUri, + postJson, + new KeyValuePair[] { new KeyValuePair("X-API-KEY", apiKey) }, + cancelToken); + } + catch (Exception ex) + { + Logger.Error(ex, "Failed to post json to ipthreat api, please double check your IPThreatApiKey setting"); + } + + // set next run time + nextRun = now.AddMinutes(random.Next(30, 91)); + } + + /// + public void AddIPAddressLogEvents(IEnumerable events) + { + lock (events) + { + this.events.AddRange(events.Where(e => e.Type == IPAddressEventType.FailedLogin && + !service.Config.IsWhitelisted(e.IPAddress))); + } + } +} diff --git a/IPBanCore/Core/IPBan/IPBanService.cs b/IPBanCore/Core/IPBan/IPBanService.cs index 053bb073..70cdcfdb 100644 --- a/IPBanCore/Core/IPBan/IPBanService.cs +++ b/IPBanCore/Core/IPBan/IPBanService.cs @@ -129,6 +129,7 @@ public void AddIPAddressLogEvents(IEnumerable events) { pendingLogEvents.AddRange(eventsArray); } + IPThreatUploader.AddIPAddressLogEvents(eventsArray); } /// @@ -374,6 +375,7 @@ public async Task RunAsync(CancellationToken cancelToken) AddUpdater(new IPBanUnblockIPAddressesUpdater(this, Path.Combine(AppContext.BaseDirectory, "unban*.txt"))); AddUpdater(new IPBanBlockIPAddressesUpdater(this, Path.Combine(AppContext.BaseDirectory, "ban*.txt"))); AddUpdater(DnsList); + AddUpdater(IPThreatUploader ??= new IPBanIPThreatUploader(this)); // start delegate if we have one IPBanDelegate?.Start(this); diff --git a/IPBanCore/Core/IPBan/IPBanService_Fields.cs b/IPBanCore/Core/IPBan/IPBanService_Fields.cs index 6b18ac2d..bfb73799 100644 --- a/IPBanCore/Core/IPBan/IPBanService_Fields.cs +++ b/IPBanCore/Core/IPBan/IPBanService_Fields.cs @@ -104,6 +104,11 @@ public string ConfigFilePath /// public IDnsServerList DnsList { get; set; } = new IPBanDnsServerList(); + /// + /// If an api key is specified in the IPThreatApiKey app setting + /// + public IPBanIPThreatUploader IPThreatUploader { get; set; } + /// /// Extra handler for banned ip addresses (optional) /// diff --git a/IPBanCore/Core/Interfaces/IHttpRequestMaker.cs b/IPBanCore/Core/Interfaces/IHttpRequestMaker.cs index a774646f..a1ff8718 100644 --- a/IPBanCore/Core/Interfaces/IHttpRequestMaker.cs +++ b/IPBanCore/Core/Interfaces/IHttpRequestMaker.cs @@ -49,7 +49,7 @@ public interface IHttpRequestMaker /// Optional http headers /// Cancel token /// Task of response byte[] - Task MakeRequestAsync(Uri uri, string postJson = null, IEnumerable> headers = null, + Task MakeRequestAsync(Uri uri, byte[] postJson = null, IEnumerable> headers = null, CancellationToken cancelToken = default) => throw new NotImplementedException(); } @@ -83,7 +83,7 @@ public class DefaultHttpRequestMaker : IHttpRequestMaker public static long LocalRequestCount { get { return localRequestCount; } } /// - public async Task MakeRequestAsync(Uri uri, string postJson = null, IEnumerable> headers = null, + public async Task MakeRequestAsync(Uri uri, byte[] postJson = null, IEnumerable> headers = null, CancellationToken cancelToken = default) { if (uri.Host.StartsWith("localhost", StringComparison.OrdinalIgnoreCase) || @@ -122,15 +122,16 @@ public async Task MakeRequestAsync(Uri uri, string postJson = null, IEnu } } byte[] response; - if (string.IsNullOrWhiteSpace(postJson)) + if (postJson is null || postJson.Length == 0) { msg.Method = HttpMethod.Get; } else { - msg.Headers.Add("Cache-Control", "no-cache"); msg.Method = HttpMethod.Post; - msg.Content = new StringContent(postJson, Encoding.UTF8, "application/json"); + msg.Headers.Add("Cache-Control", "no-cache"); + msg.Content = new ByteArrayContent(postJson); + msg.Content.Headers.Add("Content-Type", "application/json; charset=utf-8"); } var responseMsg = await client.SendAsync(msg, cancelToken); @@ -139,17 +140,19 @@ public async Task MakeRequestAsync(Uri uri, string postJson = null, IEnu { throw new WebException("Request to url " + uri + " failed, status: " + responseMsg.StatusCode + ", response: " + Encoding.UTF8.GetString(response)); } - if (uri.AbsolutePath.EndsWith(".gz", StringComparison.OrdinalIgnoreCase)) + else if (response is not null && + response.Length != 0 && + uri.AbsolutePath.EndsWith(".gz", StringComparison.OrdinalIgnoreCase)) { try { // in case response somehow got gzip decompressed already, catch exception and keep response as is - MemoryStream decompressdStream = new(); + MemoryStream decompressStream = new(); { using GZipStream gz = new(new MemoryStream(response), CompressionMode.Decompress, true); - gz.CopyTo(decompressdStream); + gz.CopyTo(decompressStream); } - response = decompressdStream.ToArray(); + response = decompressStream.ToArray(); } catch { diff --git a/IPBanCore/IPBanCore.csproj b/IPBanCore/IPBanCore.csproj index c5efff00..4aae9e6c 100644 --- a/IPBanCore/IPBanCore.csproj +++ b/IPBanCore/IPBanCore.csproj @@ -6,9 +6,9 @@ true true DigitalRuby.IPBanCore - 1.7.4 - 1.7.4 - 1.7.4 + 1.8.0 + 1.8.0 + 1.8.0 Jeff Johnson Digital Ruby, LLC IPBan diff --git a/IPBanCore/ipban.config b/IPBanCore/ipban.config index 101eae94..25996a2f 100644 --- a/IPBanCore/ipban.config +++ b/IPBanCore/ipban.config @@ -767,6 +767,16 @@ Login failed for user 'NT AUTHORITY\ANONYMOUS LOGON'. Reason: Could not find a l + + + diff --git a/IPBanTests/IPBanUriFirewallRuleTests.cs b/IPBanTests/IPBanUriFirewallRuleTests.cs index 6cddf9c5..885ec8dc 100644 --- a/IPBanTests/IPBanUriFirewallRuleTests.cs +++ b/IPBanTests/IPBanUriFirewallRuleTests.cs @@ -82,7 +82,7 @@ private async Task TestFileInternal(string uri) } } - public Task MakeRequestAsync(Uri uri, string postJson = null, IEnumerable> headers = null, + public Task MakeRequestAsync(Uri uri, byte[] postJson = null, IEnumerable> headers = null, CancellationToken cancelToken = default) { return Task.FromResult(Encoding.UTF8.GetBytes(GetTestFile())); diff --git a/README.md b/README.md index 9e658028..b96095bb 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,14 @@ IPBan - Block out attackers quickly and easily on Linux and Windows ----- [![Github Sponsorship](.github/github_sponsor_btn.svg)](https://github.com/sponsors/jjxtra) - [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=7EJ3K33SRLU9E) - [![Build Status](https://dev.azure.com/DigitalRuby/DigitalRuby/_apis/build/status/DigitalRuby_IPBan?branchName=master)](https://dev.azure.com/DigitalRuby/DigitalRuby/_build/latest?definitionId=4&branchName=master) -Get a discount on IPBan Pro by visiting https://ipban.com/upgrade-to-ipban-pro/. - -You can also visit the ipban discord at https://discord.gg/GRmbCcKFNR to chat with other IPBan users. - -Sign up for the IPBan Mailing List +**Helpful Links** +- Get a discount on IPBan Pro by visiting https://ipban.com/upgrade-to-ipban-pro/. +- Integrate IPBan with IPThreat, a 100% free to use website and service. Unlike some other sites and services, we don't charge a high subscription fee. Keeping your servers protected should not cost a fortune. +- You can also visit the ipban discord at https://discord.gg/GRmbCcKFNR to chat with other IPBan users. +- Sign up for the IPBan Mailing List **Requirements** - IPBan requires .NET 6 SDK to build and debug code. For an IDE, I suggest Visual Studio Community for Windows, or VS code for Linux. All are free. You can build a self contained executable to eliminate the need for dotnet core on the server machine, or just download the precompiled binaries. @@ -75,6 +73,10 @@ To disable anonymously sending banned ip addresses to the global ipban database, Get a discount on IPBan Pro by visiting https://ipban.com/upgrade-to-ipban-pro/. +**Other Services** + +Integrate IPBan with IPThreat, a 100% free to use website and service. Unlike some other sites and services, we don't charge high subscription fee. Keeping your servers protected should not cost a fortune. + **Dontations** If the free IPBan has helped you and you feel so inclined, please consider donating...