Skip to content

Commit 3ad29f4

Browse files
committed
1 parent b197a94 commit 3ad29f4

File tree

9 files changed

+179
-20
lines changed

9 files changed

+179
-20
lines changed

IPBanCore/Core/IPBan/IPBanConfig.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ public void Dispose()
113113
private readonly TimeSpan cycleTime = TimeSpan.FromMinutes(1.0d);
114114
private readonly TimeSpan minimumTimeBetweenFailedLoginAttempts = TimeSpan.FromSeconds(5.0);
115115
private readonly TimeSpan minimumTimeBetweenSuccessfulLoginAttempts = TimeSpan.FromSeconds(5.0);
116+
117+
private readonly string ipThreatApiKey = string.Empty;
116118
private readonly int failedLoginAttemptsBeforeBan = 5;
117119
private readonly bool resetFailedLoginCountForUnbannedIPAddresses;
118120
private readonly string firewallRulePrefix = "IPBan_";
@@ -180,6 +182,7 @@ private IPBanConfig(XmlDocument doc, IDnsLookup dns = null, IDnsServerList dnsLi
180182
appSettings[node.Attributes["key"].Value] = node.Attributes["value"].Value;
181183
}
182184

185+
GetConfig<string>("IPThreatApiKey", ref ipThreatApiKey);
183186
GetConfig<int>("FailedLoginAttemptsBeforeBan", ref failedLoginAttemptsBeforeBan, 1, 50);
184187
GetConfig<bool>("ResetFailedLoginCountForUnbannedIPAddresses", ref resetFailedLoginCountForUnbannedIPAddresses);
185188
GetConfigArray<TimeSpan>("BanTime", ref banTimes, emptyTimeSpanArray);
@@ -881,6 +884,11 @@ appSettingsOverride is not null &&
881884
/// </summary>
882885
public IReadOnlyDictionary<string, string> AppSettings => appSettings;
883886

887+
/// <summary>
888+
/// Api key from https://ipthreat.net, if any
889+
/// </summary>
890+
public string IPThreatApiKey { get { return ipThreatApiKey; } }
891+
884892
/// <summary>
885893
/// Number of failed login attempts before a ban is initiated
886894
/// </summary>
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
using System;
2+
using System.Net.Http;
3+
using System.Threading.Tasks;
4+
using System.Threading;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Globalization;
8+
using System.Reflection;
9+
using System.Diagnostics.CodeAnalysis;
10+
11+
namespace DigitalRuby.IPBanCore;
12+
13+
/// <summary>
14+
/// Sync failed logins to ipthreat api
15+
/// </summary>
16+
public sealed class IPBanIPThreatUploader : IUpdater, IIPAddressEventHandler
17+
{
18+
private static readonly Uri ipThreatReportApiUri = new("https://api.ipthreat.net/api/bulkreport");
19+
20+
private readonly IIPBanService service;
21+
private readonly Random random = new();
22+
private readonly List<IPAddressLogEvent> events = new();
23+
24+
private DateTime nextRun;
25+
26+
/// <summary>
27+
/// Constructor
28+
/// </summary>
29+
/// <param name="service">Service</param>
30+
public IPBanIPThreatUploader(IIPBanService service)
31+
{
32+
this.service = service;
33+
nextRun = IPBanService.UtcNow;//.AddMinutes(random.Next(30, 91));
34+
}
35+
36+
/// <inheritdoc />
37+
public void Dispose()
38+
{
39+
40+
}
41+
42+
/// <inheritdoc />
43+
public async Task Update(CancellationToken cancelToken = default)
44+
{
45+
// ready to run?
46+
var now = IPBanService.UtcNow;
47+
if (now < nextRun)
48+
{
49+
return;
50+
}
51+
52+
// do we have an api key?
53+
var apiKey = (service.Config.IPThreatApiKey ?? string.Empty).Trim();
54+
if (string.IsNullOrWhiteSpace(apiKey))
55+
{
56+
return;
57+
}
58+
// api key can be read from env var if starts and ends with %
59+
else if (apiKey.StartsWith("%") && apiKey.EndsWith("%"))
60+
{
61+
apiKey = Environment.GetEnvironmentVariable(apiKey.Trim('%'));
62+
if (string.IsNullOrWhiteSpace(apiKey))
63+
{
64+
return;
65+
}
66+
}
67+
68+
// copy events
69+
IReadOnlyCollection<IPAddressLogEvent> eventsCopy;
70+
lock (events)
71+
{
72+
eventsCopy = events.ToArray();
73+
events.Clear();
74+
}
75+
if (eventsCopy.Count == 0)
76+
{
77+
return;
78+
}
79+
80+
// post json
81+
try
82+
{
83+
/*
84+
[{
85+
"ip": "1.2.3.4",
86+
"flags": "None",
87+
"system": "SMTP",
88+
"notes": "Failed password",
89+
"ts": "2022-09-02T15:24:07.842Z",
90+
"count": 1
91+
}]
92+
*/
93+
var transform =
94+
eventsCopy.Select(e => new
95+
{
96+
ip = e.IPAddress,
97+
flags = "BruteForce",
98+
system = e.Source,
99+
notes = "ipban " + Assembly.GetEntryAssembly()?.GetName().Version?.ToString(3),
100+
ts = e.Timestamp.ToString("s", CultureInfo.InvariantCulture) + "Z",
101+
count = e.Count
102+
});
103+
var jsonObj = new { items = transform };
104+
// have to use newtonsoft here
105+
var postJson = System.Text.Encoding.UTF8.GetBytes(Newtonsoft.Json.JsonConvert.SerializeObject(jsonObj));
106+
await service.RequestMaker.MakeRequestAsync(ipThreatReportApiUri,
107+
postJson,
108+
new KeyValuePair<string, object>[] { new KeyValuePair<string, object>("X-API-KEY", apiKey) },
109+
cancelToken);
110+
}
111+
catch (Exception ex)
112+
{
113+
Logger.Error(ex, "Failed to post json to ipthreat api, please double check your IPThreatApiKey setting");
114+
}
115+
116+
// set next run time
117+
nextRun = now.AddMinutes(random.Next(30, 91));
118+
}
119+
120+
/// <inheritdoc />
121+
public void AddIPAddressLogEvents(IEnumerable<IPAddressLogEvent> events)
122+
{
123+
lock (events)
124+
{
125+
this.events.AddRange(events.Where(e => e.Type == IPAddressEventType.FailedLogin &&
126+
!service.Config.IsWhitelisted(e.IPAddress)));
127+
}
128+
}
129+
}

IPBanCore/Core/IPBan/IPBanService.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ public void AddIPAddressLogEvents(IEnumerable<IPAddressLogEvent> events)
129129
{
130130
pendingLogEvents.AddRange(eventsArray);
131131
}
132+
IPThreatUploader.AddIPAddressLogEvents(eventsArray);
132133
}
133134

134135
/// <summary>
@@ -374,6 +375,7 @@ public async Task RunAsync(CancellationToken cancelToken)
374375
AddUpdater(new IPBanUnblockIPAddressesUpdater(this, Path.Combine(AppContext.BaseDirectory, "unban*.txt")));
375376
AddUpdater(new IPBanBlockIPAddressesUpdater(this, Path.Combine(AppContext.BaseDirectory, "ban*.txt")));
376377
AddUpdater(DnsList);
378+
AddUpdater(IPThreatUploader ??= new IPBanIPThreatUploader(this));
377379

378380
// start delegate if we have one
379381
IPBanDelegate?.Start(this);

IPBanCore/Core/IPBan/IPBanService_Fields.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,11 @@ public string ConfigFilePath
104104
/// </summary>
105105
public IDnsServerList DnsList { get; set; } = new IPBanDnsServerList();
106106

107+
/// <summary>
108+
/// If an api key is specified in the IPThreatApiKey app setting
109+
/// </summary>
110+
public IPBanIPThreatUploader IPThreatUploader { get; set; }
111+
107112
/// <summary>
108113
/// Extra handler for banned ip addresses (optional)
109114
/// </summary>

IPBanCore/Core/Interfaces/IHttpRequestMaker.cs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public interface IHttpRequestMaker
4949
/// <param name="headers">Optional http headers</param>
5050
/// <param name="cancelToken">Cancel token</param>
5151
/// <returns>Task of response byte[]</returns>
52-
Task<byte[]> MakeRequestAsync(Uri uri, string postJson = null, IEnumerable<KeyValuePair<string, object>> headers = null,
52+
Task<byte[]> MakeRequestAsync(Uri uri, byte[] postJson = null, IEnumerable<KeyValuePair<string, object>> headers = null,
5353
CancellationToken cancelToken = default) => throw new NotImplementedException();
5454
}
5555

@@ -83,7 +83,7 @@ public class DefaultHttpRequestMaker : IHttpRequestMaker
8383
public static long LocalRequestCount { get { return localRequestCount; } }
8484

8585
/// <inheritdoc />
86-
public async Task<byte[]> MakeRequestAsync(Uri uri, string postJson = null, IEnumerable<KeyValuePair<string, object>> headers = null,
86+
public async Task<byte[]> MakeRequestAsync(Uri uri, byte[] postJson = null, IEnumerable<KeyValuePair<string, object>> headers = null,
8787
CancellationToken cancelToken = default)
8888
{
8989
if (uri.Host.StartsWith("localhost", StringComparison.OrdinalIgnoreCase) ||
@@ -122,15 +122,16 @@ public async Task<byte[]> MakeRequestAsync(Uri uri, string postJson = null, IEnu
122122
}
123123
}
124124
byte[] response;
125-
if (string.IsNullOrWhiteSpace(postJson))
125+
if (postJson is null || postJson.Length == 0)
126126
{
127127
msg.Method = HttpMethod.Get;
128128
}
129129
else
130130
{
131-
msg.Headers.Add("Cache-Control", "no-cache");
132131
msg.Method = HttpMethod.Post;
133-
msg.Content = new StringContent(postJson, Encoding.UTF8, "application/json");
132+
msg.Headers.Add("Cache-Control", "no-cache");
133+
msg.Content = new ByteArrayContent(postJson);
134+
msg.Content.Headers.Add("Content-Type", "application/json; charset=utf-8");
134135
}
135136

136137
var responseMsg = await client.SendAsync(msg, cancelToken);
@@ -139,17 +140,19 @@ public async Task<byte[]> MakeRequestAsync(Uri uri, string postJson = null, IEnu
139140
{
140141
throw new WebException("Request to url " + uri + " failed, status: " + responseMsg.StatusCode + ", response: " + Encoding.UTF8.GetString(response));
141142
}
142-
if (uri.AbsolutePath.EndsWith(".gz", StringComparison.OrdinalIgnoreCase))
143+
else if (response is not null &&
144+
response.Length != 0 &&
145+
uri.AbsolutePath.EndsWith(".gz", StringComparison.OrdinalIgnoreCase))
143146
{
144147
try
145148
{
146149
// in case response somehow got gzip decompressed already, catch exception and keep response as is
147-
MemoryStream decompressdStream = new();
150+
MemoryStream decompressStream = new();
148151
{
149152
using GZipStream gz = new(new MemoryStream(response), CompressionMode.Decompress, true);
150-
gz.CopyTo(decompressdStream);
153+
gz.CopyTo(decompressStream);
151154
}
152-
response = decompressdStream.ToArray();
155+
response = decompressStream.ToArray();
153156
}
154157
catch
155158
{

IPBanCore/IPBanCore.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
<PreserveCompilationContext>true</PreserveCompilationContext>
77
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
88
<PackageId>DigitalRuby.IPBanCore</PackageId>
9-
<Version>1.7.4</Version>
10-
<AssemblyVersion>1.7.4</AssemblyVersion>
11-
<FileVersion>1.7.4</FileVersion>
9+
<Version>1.8.0</Version>
10+
<AssemblyVersion>1.8.0</AssemblyVersion>
11+
<FileVersion>1.8.0</FileVersion>
1212
<Authors>Jeff Johnson</Authors>
1313
<Company>Digital Ruby, LLC</Company>
1414
<Product>IPBan</Product>

IPBanCore/ipban.config

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -767,6 +767,16 @@ Login failed for user 'NT AUTHORITY\ANONYMOUS LOGON'. Reason: Could not find a l
767767

768768
<appSettings>
769769

770+
<!--
771+
Enter your https://ipthreat.net api key here to submit failed logins to the 100% free ipthreat site and service
772+
1] Create an account on the ipthreat website : https://ipthreat.net/account/signup
773+
2] Go to https://ipthreat.net/requestpermissions to get bulk report permission
774+
3] Once you get an email back verifying permissions, create an api key at https://ipthreat.net/account?tab=apikeys
775+
4] Enter your api key below (no need to restart the service)
776+
Note: You can use %env_var_name% to read the api key from an environment variable to avoid exposing it in the config
777+
-->
778+
<add key="IPThreatApiKey" value="" />
779+
770780
<!-- Number of failed logins before banning the ip address -->
771781
<add key="FailedLoginAttemptsBeforeBan" value="5"/>
772782

IPBanTests/IPBanUriFirewallRuleTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ private async Task TestFileInternal(string uri)
8282
}
8383
}
8484

85-
public Task<byte[]> MakeRequestAsync(Uri uri, string postJson = null, IEnumerable<KeyValuePair<string, object>> headers = null,
85+
public Task<byte[]> MakeRequestAsync(Uri uri, byte[] postJson = null, IEnumerable<KeyValuePair<string, object>> headers = null,
8686
CancellationToken cancelToken = default)
8787
{
8888
return Task.FromResult(Encoding.UTF8.GetBytes(GetTestFile()));

README.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
IPBan - Block out attackers quickly and easily on Linux and Windows
22
-----
33
[![Github Sponsorship](.github/github_sponsor_btn.svg)](https://github.com/sponsors/jjxtra)
4-
54
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=7EJ3K33SRLU9E)
6-
75
[![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)
86

9-
Get a discount on IPBan Pro by visiting <a href='https://ipban.com/upgrade-to-ipban-pro/'>https://ipban.com/upgrade-to-ipban-pro/</a>.
10-
11-
You can also visit the ipban discord at https://discord.gg/GRmbCcKFNR to chat with other IPBan users.
12-
13-
<a href="https://ipban.com/newsletter">Sign up for the IPBan Mailing List</a>
7+
**Helpful Links**
8+
- Get a discount on IPBan Pro by visiting <a href='https://ipban.com/upgrade-to-ipban-pro/'>https://ipban.com/upgrade-to-ipban-pro/</a>.
9+
- <a href='https://ipthreat.net/integrations/ipban'>Integrate IPBan with IPThreat</a>, 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.
10+
- You can also visit the ipban discord at https://discord.gg/GRmbCcKFNR to chat with other IPBan users.
11+
- <a href="https://ipban.com/newsletter">Sign up for the IPBan Mailing List</a>
1412

1513
**Requirements**
1614
- 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,
7573

7674
Get a discount on IPBan Pro by visiting <a href='https://ipban.com/upgrade-to-ipban-pro/'>https://ipban.com/upgrade-to-ipban-pro/</a>.
7775

76+
**Other Services**
77+
78+
<a href='https://ipthreat.net/integrations/ipban'>Integrate IPBan with IPThreat</a>, 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.
79+
7880
**Dontations**
7981
If the free IPBan has helped you and you feel so inclined, please consider donating...
8082

0 commit comments

Comments
 (0)