Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions ThingConnect.Pulse.Server/Services/ConfigurationParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,46 @@ public static async Task<ConfigurationParser> CreateAsync(ILogger<ConfigurationP

// First, deserialize the YAML
ConfigurationYaml config = _yamlDeserializer.Deserialize<ConfigurationYaml>(yamlContent);
var ipv6Errors = new List<ValidationError>();

foreach (var target in config.Targets)
{
if (!string.IsNullOrEmpty(target.Cidr) && target.Cidr.Contains(":"))
{
string[] parts = target.Cidr.Split('/');
if (parts.Length != 2 || !int.TryParse(parts[1], out int prefix))
{
ipv6Errors.Add(new ValidationError
{
Path = "cidr",
Message = $"Invalid IPv6 CIDR format: {target.Cidr}",
Value = target.Cidr
});
continue;
}

if (prefix < 120 || prefix > 128)
{
ipv6Errors.Add(new ValidationError
{
Path = "cidr",
Message = $"IPv6 prefix /{prefix} too large to expand practically (supported: /120–/128)",
Value = target.Cidr
});
}
}
}

if (ipv6Errors.Any())
{
return Task.FromResult<(ConfigurationYaml?, ValidationErrorsDto?)>(
(null,
new ValidationErrorsDto
{
Message = "IPv6 expansion validation failed",
Errors = ipv6Errors
}));
}

// Convert back to JSON for schema validation
ISerializer serializer = new SerializerBuilder()
Expand Down
138 changes: 105 additions & 33 deletions ThingConnect.Pulse.Server/Services/Monitoring/DiscoveryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@
namespace ThingConnect.Pulse.Server.Services.Monitoring;

/// <summary>
/// Implementation of discovery service for expanding network targets.
/// Implementation of discovery service for expanding network targets, now supporting IPv6.
/// </summary>
public sealed class DiscoveryService : IDiscoveryService
{
private readonly ILogger<DiscoveryService> _logger;

private static readonly Regex CidrRegex = new(@"^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/(\d{1,2})$",
// Regex for CIDR parsing
private static readonly Regex Ipv6CidrRegex = new Regex(@"^([0-9a-fA-F:]+)(?:/([0-9]{1,3}))?$",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex Ipv4CidrRegex = new Regex(@"^(\d{1,3}(?:\.\d{1,3}){3})(?:/([0-9]{1,2}))?$",
RegexOptions.Compiled);
private static readonly Regex WildcardRegex = new(@"^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.\*$",
RegexOptions.Compiled);
Expand All @@ -24,45 +27,56 @@ public DiscoveryService(ILogger<DiscoveryService> logger)

public IEnumerable<string> ExpandCidr(string cidr)
{
Match match = CidrRegex.Match(cidr);
if (!match.Success)
// IPv4 CIDR
Match matchV4 = Ipv4CidrRegex.Match(cidr);
if (matchV4.Success)
{
_logger.LogWarning("Invalid CIDR format: {Cidr}", cidr);
yield break;
}
string baseIp = matchV4.Groups[1].Value;
int prefixLength = int.Parse(matchV4.Groups[2].Value);

string baseIp = match.Groups[1].Value;
int prefixLength = int.Parse(match.Groups[2].Value);
if (prefixLength < 0 || prefixLength > 32)
{
_logger.LogWarning("Invalid CIDR prefix length: {PrefixLength}", prefixLength);
yield break;
}

if (prefixLength < 0 || prefixLength > 32)
{
_logger.LogWarning("Invalid CIDR prefix length: {PrefixLength}", prefixLength);
yield break;
}
if (!IPAddress.TryParse(baseIp, out IPAddress? ipAddress))
{
_logger.LogWarning("Invalid IP address in CIDR: {BaseIp}", baseIp);
yield break;
}

if (!IPAddress.TryParse(baseIp, out IPAddress? ipAddress))
{
_logger.LogWarning("Invalid IP address in CIDR: {BaseIp}", baseIp);
yield break;
}
byte[] addressBytes = ipAddress.GetAddressBytes();
uint addressInt = BitConverter.ToUInt32(addressBytes.Reverse().ToArray(), 0);
int hostBits = 32 - prefixLength;
uint hostCount = (uint)(1 << hostBits);
uint networkAddress = addressInt & (0xFFFFFFFF << hostBits);

byte[] addressBytes = ipAddress.GetAddressBytes();
uint addressInt = BitConverter.ToUInt32(addressBytes.Reverse().ToArray(), 0);
// Skip network and broadcast addresses for practical use
uint startAddress = networkAddress + 1;
uint endAddress = networkAddress + hostCount - 1;

int hostBits = 32 - prefixLength;
uint hostCount = (uint)(1 << hostBits);
uint networkAddress = addressInt & (0xFFFFFFFF << hostBits);
for (uint address = startAddress; address < endAddress && address > networkAddress; address++)
{
byte[] bytes = BitConverter.GetBytes(address).Reverse().ToArray();
yield return new IPAddress(bytes).ToString();
}

// Skip network and broadcast addresses for practical use
uint startAddress = networkAddress + 1;
uint endAddress = networkAddress + hostCount - 1;
yield break;
}

for (uint address = startAddress; address < endAddress && address > networkAddress; address++)
// IPv6 CIDR using helper expander
Match matchV6 = Ipv6CidrRegex.Match(cidr);
if (matchV6.Success)
{
byte[] bytes = BitConverter.GetBytes(address).Reverse().ToArray();
var ip = new IPAddress(bytes);
yield return ip.ToString();
foreach (var ip in Ipv6CidrExpander.Expand(cidr))
{
yield return ip;
}
yield break;
}

_logger.LogWarning("Invalid CIDR format: {Cidr}", cidr);
}

public IEnumerable<string> ExpandWildcard(string wildcard, int startRange = 1, int endRange = 254)
Expand Down Expand Up @@ -96,8 +110,12 @@ public async Task<IEnumerable<string>> ResolveHostnameAsync(string hostname, Can
{
IPAddress[] addresses = await Dns.GetHostAddressesAsync(hostname);
return addresses
.Where(addr => addr.AddressFamily == AddressFamily.InterNetwork) // IPv4 only for now
.Select(addr => addr.ToString())
.Select(addr =>
{
if (addr.AddressFamily == AddressFamily.InterNetworkV6 && addr.ScopeId != 0)
return $"{addr}%{addr.ScopeId}";
return addr.ToString();
})
.ToList();
}
catch (Exception ex)
Expand Down Expand Up @@ -208,4 +226,58 @@ private static Data.Endpoint CreateEndpointFromTarget(dynamic target, string hos

return endpoint;
}

private sealed class NetworkRange
{
public string BaseAddress { get; }
public int PrefixLength { get; }

public NetworkRange(string baseAddress, int prefixLength)
{
BaseAddress = baseAddress;
PrefixLength = prefixLength;
}
}

private sealed class Ipv6CidrExpander
{
public static IEnumerable<string> Expand(string cidr)
{
var match = Ipv6CidrRegex.Match(cidr);
if (!match.Success) throw new ArgumentException($"Invalid IPv6 CIDR: {cidr}");

string baseAddress = match.Groups[1].Value;
int prefixLength = match.Groups[2].Success ? int.Parse(match.Groups[2].Value) : 64;

// Handle zone/index if present (e.g., fe80::1%eth0)
string? zone = null;
int percentIndex = baseAddress.IndexOf('%');
if (percentIndex >= 0)
{
zone = baseAddress.Substring(percentIndex + 1);
baseAddress = baseAddress.Substring(0, percentIndex);
}

if (!IPAddress.TryParse(baseAddress, out IPAddress? ipAddress) || ipAddress.AddressFamily != AddressFamily.InterNetworkV6)
throw new ArgumentException($"Invalid IPv6 address: {baseAddress}");

byte[] bytes = ipAddress.GetAddressBytes();

string FormatAddress(byte[] addrBytes)
=> zone != null ? $"{new IPAddress(addrBytes)}%{zone}" : new IPAddress(addrBytes).ToString();

if (prefixLength < 120 || prefixLength > 128)
throw new ArgumentException($"IPv6 prefix /{prefixLength} not supported for practical expansion. Use /120–/128.");

int hostCount = 1 << (128 - prefixLength);
if (hostCount > 256) hostCount = 256;

for (int i = 0; i < hostCount; i++)
{
byte[] copy = (byte[])bytes.Clone();
copy[15] += (byte)i;
yield return FormatAddress(copy);
}
}
}
}
Loading
Loading