diff --git a/MediaBrowser.Common/Net/NetworkUtils.cs b/MediaBrowser.Common/Net/NetworkUtils.cs
index 5c854b39d5..71539b8b78 100644
--- a/MediaBrowser.Common/Net/NetworkUtils.cs
+++ b/MediaBrowser.Common/Net/NetworkUtils.cs
@@ -7,6 +7,7 @@ using System.Net.Sockets;
using System.Text.RegularExpressions;
using Jellyfin.Extensions;
using MediaBrowser.Model.Net;
+using Microsoft.Extensions.Logging;
namespace MediaBrowser.Common.Net;
@@ -166,8 +167,9 @@ public static partial class NetworkUtils
/// Input string array to be parsed.
/// Collection of .
/// Boolean signaling if negated or not negated values should be parsed.
+ /// Optional logger used to warn about entries that fail to parse.
/// True if parsing was successful.
- public static bool TryParseToSubnets(string[] values, [NotNullWhen(true)] out IReadOnlyList? result, bool negated = false)
+ public static bool TryParseToSubnets(string[] values, [NotNullWhen(true)] out IReadOnlyList? result, bool negated = false, ILogger? logger = null)
{
if (values is null || values.Length == 0)
{
@@ -182,12 +184,45 @@ public static partial class NetworkUtils
{
(tmpResult ??= new()).Add(innerResult);
}
+ else
+ {
+ LogInvalidSubnet(logger, values[a]);
+ }
}
result = tmpResult;
return result is not null;
}
+ private static void LogInvalidSubnet(ILogger? logger, string value)
+ {
+ if (logger is null)
+ {
+ return;
+ }
+
+ var trimmed = value.AsSpan().Trim();
+ if (trimmed.StartsWith('!'))
+ {
+ trimmed = trimmed[1..];
+ }
+
+ var slash = trimmed.IndexOf('/');
+ if (slash != -1
+ && trimmed.Contains(':')
+ && trimmed.IndexOf("::", StringComparison.Ordinal) == -1)
+ {
+ logger.LogWarning(
+ "Invalid IPv6 subnet '{Subnet}': IPv6 prefix-only notation is not supported. Use the full notation including '::' (e.g. '{Example}::/{Prefix}').",
+ value,
+ trimmed[..slash].ToString(),
+ trimmed[(slash + 1)..].ToString());
+ return;
+ }
+
+ logger.LogWarning("Invalid subnet '{Subnet}' will be ignored.", value);
+ }
+
///
/// Try parsing a string into an , respecting exclusions.
/// Inputs without a subnet mask will be represented as with a single IP.
diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs
index 6a8a91fa51..0fe2fc43ad 100644
--- a/src/Jellyfin.Networking/Manager/NetworkManager.cs
+++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs
@@ -316,7 +316,7 @@ public class NetworkManager : INetworkManager, IDisposable
var subnets = config.LocalNetworkSubnets;
// If no LAN addresses are specified, all private subnets and Loopback are deemed to be the LAN
- if (!NetworkUtils.TryParseToSubnets(subnets, out var lanSubnets, false) || lanSubnets.Count == 0)
+ if (!NetworkUtils.TryParseToSubnets(subnets, out var lanSubnets, false, _logger) || lanSubnets.Count == 0)
{
_logger.LogDebug("Using LAN interface addresses as user provided no LAN details.");
@@ -343,7 +343,7 @@ public class NetworkManager : INetworkManager, IDisposable
_lanSubnets = lanSubnets.Select(x => x.Subnet).ToArray();
}
- _excludedSubnets = NetworkUtils.TryParseToSubnets(subnets, out var excludedSubnets, true)
+ _excludedSubnets = NetworkUtils.TryParseToSubnets(subnets, out var excludedSubnets, true, _logger)
? excludedSubnets.Select(x => x.Subnet).ToArray()
: Array.Empty();
}
diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
index b63009d6a5..66eec077dc 100644
--- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
+++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs
@@ -7,6 +7,7 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Model.Net;
using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
@@ -94,9 +95,46 @@ namespace Jellyfin.Networking.Tests
[InlineData("256.128.0.0.0.1")]
[InlineData("fd23:184f:2029:0:3139:7386:67d7:d517:1231")]
[InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517:1231]")]
+ [InlineData("fd23:184f:2029:0100/56")]
public static void TryParseInvalidIPStringsFalse(string address)
=> Assert.False(NetworkUtils.TryParseToSubnet(address, out _));
+ ///
+ /// Verifies that emits a targeted warning
+ /// for IPv6 prefix-only notation and a generic warning for other malformed entries.
+ ///
+ [Fact]
+ public static void TryParseToSubnets_InvalidEntries_LogsWarnings()
+ {
+ var logger = new Mock();
+
+ var values = new[] { "10.0.0.0/8", "fd23:184f:2029:0100/56", "not-an-address" };
+ Assert.True(NetworkUtils.TryParseToSubnets(values, out var result, false, logger.Object));
+ Assert.NotNull(result);
+ Assert.Single(result);
+
+ // IPv6 prefix-only notation should produce a specific, actionable warning.
+ logger.Verify(
+ l => l.Log(
+ LogLevel.Warning,
+ It.IsAny(),
+ It.Is((state, _) => state.ToString()!.Contains("IPv6 prefix-only", StringComparison.Ordinal)
+ && state.ToString()!.Contains("fd23:184f:2029:0100/56", StringComparison.Ordinal)),
+ It.IsAny(),
+ It.IsAny>()),
+ Times.Once);
+
+ // Other malformed entries should still produce a generic warning.
+ logger.Verify(
+ l => l.Log(
+ LogLevel.Warning,
+ It.IsAny(),
+ It.Is((state, _) => state.ToString()!.Contains("not-an-address", StringComparison.Ordinal)),
+ It.IsAny(),
+ It.IsAny>()),
+ Times.Once);
+ }
+
///
/// Checks if IPv4 address is within a defined subnet.
///