Skip to content

Commit 55be9aa

Browse files
authored
Release 4.1.0 (#361)
* Update nugets * Update example * Claude pass 1 * Claude pass 2 * Claude pass 3 * Validate at tests on Linux * Coverage pass * Coverage pass 2 * Extra windows firewall cleanup * Add note to check logfile.txt * Performance boost * Speed up tests * Performance boost/test speed up * Change log level of missing keys * Trim warning fixes * More trim warning fix * Trim warnings * New test * New string * Another test * New RDP failed login * FQDN fixes * New string, another test * .NET 10 * Fix file casing * Fix rule name on firewalld * Fix doubled up prefix * Update log file path from auth.txt to auth.log * Reduce firewall task debug * Update nugets * Update nuget * New mailenable expression
1 parent 38fd281 commit 55be9aa

108 files changed

Lines changed: 11374 additions & 367 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/settings.local.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash",
5+
"Powershell",
6+
"Python",
7+
"Write",
8+
"Edit",
9+
"MultiEdit"
10+
],
11+
"defaultMode": "dontAsk"
12+
}
13+
}

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ bin/
77
obj/
88
package/
99
packages/
10+
results/
11+
report/
12+
.nuget-packages/
1013
*.suo
1114
*.cachefile
1215
*.user

IPBan/IPBan.csproj

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
<ServerGarbageCollection>false</ServerGarbageCollection>
2727
<TrimMode>partial</TrimMode>
2828
<JsonSerializerIsReflectionEnabledByDefault>true</JsonSerializerIsReflectionEnabledByDefault>
29+
<!-- IL2104: third-party assemblies produce trim warnings internally; suppressed as unfixable -->
30+
<!-- IL2026: runtime-internal COM activation (BuiltInComInteropSupport) is not trim-compatible; unfixable from user code -->
31+
<NoWarn>$(NoWarn);IL2104;IL2026</NoWarn>
2932
</PropertyGroup>
3033

3134
<ItemGroup>
@@ -45,6 +48,6 @@
4548
<TrimmerRootAssembly Include="System.Runtime" />
4649
<TrimmerRootAssembly Include="mscorlib" />
4750
<TrimmerRootAssembly Include="netstandard" />
48-
</ItemGroup>
51+
</ItemGroup>
4952

5053
</Project>

IPBanCore/Core/IPBan/IPBanConfig.cs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ namespace DigitalRuby.IPBanCore
4242
/// <summary>
4343
/// Configuration for ip ban app
4444
/// </summary>
45+
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Configuration XML models are runtime-deserialized and preserved by IPBanCore usage patterns.")]
4546
public sealed class IPBanConfig : IIsWhitelisted
4647
{
4748
/// <summary>
@@ -134,6 +135,7 @@ public void Dispose()
134135
private readonly string processToRunOnUnban = string.Empty;
135136
private readonly bool useDefaultBannedIPAddressHandler;
136137
private readonly string getUrlUpdate = string.Empty;
138+
private readonly string getUrlUpdateSha256 = string.Empty;
137139
private readonly string getUrlStart = string.Empty;
138140
private readonly string getUrlStop = string.Empty;
139141
private readonly string getUrlConfig = string.Empty;
@@ -238,6 +240,7 @@ private IPBanConfig(XmlDocument doc, IDnsLookup dns = null, IDnsServerList dnsLi
238240
TryGetConfig<int>("UserNameWhitelistMinimumEditDistance", ref userNameWhitelistMaximumEditDistance);
239241
TryGetConfig<int>("FailedLoginAttemptsBeforeBanUserNameWhitelist", ref failedLoginAttemptsBeforeBanUserNameWhitelist);
240242
TryGetConfig<string>("GetUrlUpdate", ref getUrlUpdate);
243+
TryGetConfig<string>("GetUrlUpdateSha256", ref getUrlUpdateSha256);
241244
TryGetConfig<string>("GetUrlStart", ref getUrlStart);
242245
TryGetConfig<string>("GetUrlStop", ref getUrlStop);
243246
TryGetConfig<string>("GetUrlConfig", ref getUrlConfig);
@@ -260,16 +263,19 @@ private string GetAppSettingsValue(string key, bool logMissing = true)
260263
{
261264
if (string.IsNullOrWhiteSpace(key))
262265
{
263-
// bad key
264-
Logger.Warn("Ignoring null/empty key");
266+
if (logMissing)
267+
{
268+
// bad key
269+
Logger.Debug("Ignoring null/empty key");
270+
}
265271
return null;
266272
}
267273

268274
if (!appSettings.TryGetValue(key, out var stringValue) || stringValue is null)
269275
{
270276
if (logMissing)
271277
{
272-
Logger.Warn("Ignoring key {0}, not found in appSettings", key);
278+
Logger.Debug("Ignoring key {0}, not found in appSettings", key);
273279
}
274280
return null; // skip trying to convert
275281
}
@@ -638,7 +644,7 @@ public bool IsUserNameWithinMaximumEditDistanceOfUserNameWhitelist(string userNa
638644
foreach (string userNameToCheckAgainst in userNameWhitelist)
639645
{
640646
int distance = LevenshteinUnsafe.Distance(userName, userNameToCheckAgainst);
641-
if (distance <= userNameWhitelistMaximumEditDistance)
647+
if (distance >= 0 && distance <= userNameWhitelistMaximumEditDistance)
642648
{
643649
return true;
644650
}
@@ -1152,6 +1158,14 @@ public static string ValidateFirewallUriRules(string firewallUriRules)
11521158
/// </summary>
11531159
public string GetUrlUpdate { get { return getUrlUpdate; } }
11541160

1161+
/// <summary>
1162+
/// Expected SHA-256 hash (hex, case-insensitive) of the binary returned by GetUrlUpdate.
1163+
/// If empty, the auto-update download is fetched but NOT executed — this is the safe default
1164+
/// and protects against a malicious/MITMed update server. Operators must explicitly set this
1165+
/// hash to opt in to automated update execution.
1166+
/// </summary>
1167+
public string GetUrlUpdateSha256 { get { return getUrlUpdateSha256; } }
1168+
11551169
/// <summary>
11561170
/// A url to get when the service starts, empty for none. See ReplaceUrl of IPBanService for place-holders.
11571171
/// </summary>

IPBanCore/Core/IPBan/IPBanDB.cs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -139,18 +139,24 @@ private static long GetInt64(object value)
139139
/// </summary>
140140
public static IPAddressEntry ParseIPAddressEntry(SqliteDataReader reader)
141141
{
142-
string ipAddress = reader.GetString(0);
143-
long lastFailedLogin = reader.GetInt64(1);
144-
long failedLoginCount = reader.GetInt64(2);
142+
// Older DB schemas (created before BanEndDate / UserName / Source columns were added)
143+
// can return NULL even though the column declarations now have defaults — read with
144+
// IsDBNull guards so a stale schema doesn't crash every query that hits a legacy row.
145+
string ipAddress = reader.IsDBNull(0) ? string.Empty : reader.GetString(0);
146+
long lastFailedLogin = reader.IsDBNull(1) ? 0L : reader.GetInt64(1);
147+
long failedLoginCount = reader.IsDBNull(2) ? 0L : reader.GetInt64(2);
145148
object banDateObj = reader.GetValue(3);
146-
IPAddressState state = (IPAddressState)(int)reader.GetInt32(4);
149+
IPAddressState state = reader.IsDBNull(4) ? IPAddressState.Active : (IPAddressState)(int)reader.GetInt32(4);
147150
object banEndDateObj = reader.GetValue(5);
148-
string userName = reader.GetString(6);
149-
string source = reader.GetString(7);
151+
string userName = reader.IsDBNull(6) ? string.Empty : reader.GetString(6);
152+
string source = reader.IsDBNull(7) ? string.Empty : reader.GetString(7);
150153
long banDateLong = GetInt64(banDateObj);
151154
long banEndDateLong = GetInt64(banEndDateObj);
152155
DateTime? banDate = (banDateLong == 0 ? (DateTime?)null : banDateLong.ToDateTimeUnixMilliseconds());
153-
DateTime? banEndDate = (banDateLong == 0 ? (DateTime?)null : banEndDateLong.ToDateTimeUnixMilliseconds());
156+
// Each ban-date column is independent: BanDate may be set while BanEndDate is NULL
157+
// (legacy rows from before BanEndDate existed, or in-flight transitions). Each guard
158+
// must check its own column.
159+
DateTime? banEndDate = (banEndDateLong == 0 ? (DateTime?)null : banEndDateLong.ToDateTimeUnixMilliseconds());
154160
DateTime lastFailedLoginDt = lastFailedLogin.ToDateTimeUnixMilliseconds();
155161
return new IPAddressEntry
156162
{

IPBanCore/Core/IPBan/IPBanFirewallUtility.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2525
using System;
2626
using System.Collections.Generic;
2727
using System.Diagnostics;
28+
using System.Diagnostics.CodeAnalysis;
2829
using System.IO;
2930
using System.IO.Pipelines;
3031
using System.Linq;
@@ -57,6 +58,7 @@ private static void AppendRange(StringBuilder b, PortRange range)
5758
/// <param name="rulePrefix">Rule prefix or null for default</param>
5859
/// <param name="previousFirewall">Current firewall</param>
5960
/// <returns>Firewall</returns>
61+
[UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "Firewall implementations are selected and activated dynamically at runtime by design.")]
6062
public static IIPBanFirewall CreateFirewall(IReadOnlyCollection<Type> allTypes,
6163
string rulePrefix = null,
6264
IIPBanFirewall previousFirewall = null)
@@ -611,6 +613,11 @@ public static int RunProcess(string program, object input, object output, params
611613
inputStream.CopyTo(p.StandardInput.BaseStream);
612614
}
613615
}
616+
catch (IOException)
617+
{
618+
// the process may have already exited and closed stdin (broken pipe);
619+
// feeding stdin is best-effort, so ignore the write failure
620+
}
614621
finally
615622
{
616623
try { p.StandardInput.Close(); } catch { /* ignore */ }

IPBanCore/Core/IPBan/IPBanIPThreatUploader.cs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.Globalization;
44
using System.Linq;
5+
using System.Diagnostics.CodeAnalysis;
56
using System.Threading;
67
using System.Threading.Tasks;
78

@@ -31,6 +32,7 @@ public void Dispose()
3132
}
3233

3334
/// <inheritdoc />
35+
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Anonymous payload shape is fixed and used only for IPThreat API upload.")]
3436
public async Task Update(CancellationToken cancelToken = default)
3537
{
3638
// ready to run?
@@ -104,12 +106,22 @@ await service.RequestMaker.MakeRequestAsync(ipThreatReportApiUri,
104106
/// <inheritdoc />
105107
public void AddIPAddressLogEvents(IEnumerable<IPAddressLogEvent> events)
106108
{
107-
lock (events)
109+
// Run the filter outside the lock — the predicate calls into service.Config which we
110+
// don't want to hold the events lock across. Only the AddRange happens inside.
111+
var filtered = events.Where(e => e.Type == IPAddressEventType.Blocked &&
112+
e.Count > 0 &&
113+
!e.External &&
114+
!service.Config.IsWhitelisted(e.IPAddress, out _)).ToArray();
115+
if (filtered.Length == 0)
116+
{
117+
return;
118+
}
119+
// Qualify with `this.` so the lock targets the field — the parameter is also named
120+
// `events` and would otherwise shadow it, locking an unrelated caller-supplied object
121+
// while the field itself stayed unprotected.
122+
lock (this.events)
108123
{
109-
this.events.AddRange(events.Where(e => e.Type == IPAddressEventType.Blocked &&
110-
e.Count > 0 &&
111-
!e.External &&
112-
!service.Config.IsWhitelisted(e.IPAddress, out _)));
124+
this.events.AddRange(filtered);
113125
}
114126
}
115127
}

IPBanCore/Core/IPBan/IPBanLogManager.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ public IPBanLogManager(IIPBanService service)
5757
/// <inheritdoc />
5858
public Task Update(CancellationToken cancelToken)
5959
{
60-
UpdateLogFiles(service.Config);
6160
if (service.ManualCycle)
6261
{
6362
foreach (var scanner in logsToParse)

IPBanCore/Core/IPBan/IPBanMemoryFirewall.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -695,7 +695,7 @@ public override string GetPorts(string ruleName)
695695
{
696696
return ruleRanges.Ports;
697697
}
698-
else if (!allowRuleRanges.TryGetValue(ruleName, out ruleRanges))
698+
else if (allowRuleRanges.TryGetValue(ruleName, out ruleRanges))
699699
{
700700
return ruleRanges.Ports;
701701
}

IPBanCore/Core/IPBan/IPBanService.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,11 +153,12 @@ public void AddIPAddressLogEvents(IEnumerable<IPAddressLogEvent> events)
153153
}
154154

155155
/// <summary>
156-
/// Write a new config file
156+
/// Write a new config file. Virtual so tests can intercept the call without touching
157+
/// the on-disk config.
157158
/// </summary>
158159
/// <param name="xml">Xml of the new config file</param>
159160
/// <returns>Task</returns>
160-
public async Task WriteConfigAsync(string xml)
161+
public virtual async Task WriteConfigAsync(string xml)
161162
{
162163
// Ensure valid xml before writing the file
163164
XmlDocument doc = new();
@@ -488,6 +489,13 @@ public static T CreateAndStartIPBanTestService<T>(string directory = null, strin
488489
ExtensionMethods.FileWriteAllTextWithRetry(configFileOverridePath, configFileOverrideText);
489490
T service = IPBanService.CreateService<T>();
490491
service.ConfigFilePath = configFilePath;
492+
service.ConfigReaderWriter.UseFile = false;
493+
service.ConfigReaderWriter.GlobalConfigString = configFileText;
494+
service.ConfigOverrideReaderWriter.UseFile = false;
495+
service.ConfigOverrideReaderWriter.GlobalConfigString = configFileOverrideText;
496+
service.LocalIPAddressString = "127.0.0.1";
497+
service.RemoteIPAddressString = "127.0.0.1";
498+
service.OtherIPAddressesString = "127.0.0.1";
491499
service.MultiThreaded = false;
492500
service.ManualCycle = true;
493501
service.DnsList = null; // too slow for tests, turn off
@@ -552,7 +560,6 @@ public static void DisposeIPBanTestService(IPBanService service)
552560
Directory.Delete(appDataCache, true);
553561
}
554562
service.Firewall.Truncate();
555-
service.RunCycleAsync().Sync();
556563
service.IPBanDelegate = null;
557564
service.Dispose();
558565
IPBanService.CleanupIPBanTestFiles();

0 commit comments

Comments
 (0)