Skip to content

Commit 772e099

Browse files
committed
feat: added zone index in the schema json file and logic for expanding the ipv6 cidr
1 parent a59488c commit 772e099

File tree

2 files changed

+173
-23
lines changed

2 files changed

+173
-23
lines changed

ThingConnect.Pulse.Server/Services/Monitoring/DiscoveryService.cs

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
namespace ThingConnect.Pulse.Server.Services.Monitoring;
77

88
/// <summary>
9-
/// Implementation of discovery service for expanding network targets.
9+
/// Implementation of discovery service for expanding network targets, now with IPv6 support.
1010
/// </summary>
1111
public sealed class DiscoveryService : IDiscoveryService
1212
{
@@ -24,6 +24,11 @@ public DiscoveryService(ILogger<DiscoveryService> logger)
2424

2525
public IEnumerable<string> ExpandCidr(string cidr)
2626
{
27+
if (cidr.Contains(":")) // IPv6 CIDR
28+
{
29+
return ExpandCidrIPv6(cidr);
30+
}
31+
2732
Match match = CidrRegex.Match(cidr);
2833
if (!match.Success)
2934
{
@@ -53,7 +58,7 @@ public IEnumerable<string> ExpandCidr(string cidr)
5358
uint hostCount = (uint)(1 << hostBits);
5459
uint networkAddress = addressInt & (0xFFFFFFFF << hostBits);
5560

56-
// Skip network and broadcast addresses for practical use
61+
// Skip network and broadcast addresses
5762
uint startAddress = networkAddress + 1;
5863
uint endAddress = networkAddress + hostCount - 1;
5964

@@ -65,6 +70,61 @@ public IEnumerable<string> ExpandCidr(string cidr)
6570
}
6671
}
6772

73+
/// <summary>
74+
/// Expand IPv6 CIDR into addresses (limited to /128 or /127 or /64 to avoid flooding)
75+
/// </summary>
76+
private IEnumerable<string> ExpandCidrIPv6(string cidr)
77+
{
78+
try
79+
{
80+
string[] parts = cidr.Split('/');
81+
if (parts.Length != 2)
82+
{
83+
_logger.LogWarning("Invalid IPv6 CIDR format: {Cidr}", cidr);
84+
yield break;
85+
}
86+
87+
// Check for zone index (e.g., fe80::1%3)
88+
string hostPart = parts[0];
89+
string? zoneIndex = null;
90+
if (hostPart.Contains('%'))
91+
{
92+
var hostSplit = hostPart.Split('%');
93+
hostPart = hostSplit[0];
94+
zoneIndex = hostSplit[1];
95+
}
96+
97+
if (!IPAddress.TryParse(hostPart, out var ip)) yield break;
98+
if (ip.AddressFamily != AddressFamily.InterNetworkV6) yield break;
99+
100+
int prefix = int.Parse(parts[1]);
101+
102+
if (prefix == 128)
103+
yield return zoneIndex != null ? $"{ip}%{zoneIndex}" : ip.ToString();
104+
else if (prefix == 127)
105+
{
106+
byte[] bytes = ip.GetAddressBytes();
107+
yield return zoneIndex != null ? $"{ip}%{zoneIndex}" : ip.ToString();
108+
109+
bytes[15] |= 1;
110+
var ip2 = new IPAddress(bytes);
111+
yield return zoneIndex != null ? $"{ip2}%{zoneIndex}" : ip2.ToString();
112+
}
113+
else if (prefix == 64)
114+
yield return zoneIndex != null ? $"{ip}%{zoneIndex}" : ip.ToString();
115+
else
116+
{
117+
_logger.LogWarning(
118+
"Skipping IPv6 CIDR {Cidr} with prefix /{Prefix} to avoid massive expansion",
119+
cidr, prefix);
120+
}
121+
}
122+
catch (Exception ex)
123+
{
124+
_logger.LogWarning(ex, "Failed to expand IPv6 CIDR: {Cidr}", cidr);
125+
}
126+
}
127+
68128
public IEnumerable<string> ExpandWildcard(string wildcard, int startRange = 1, int endRange = 254)
69129
{
70130
Match match = WildcardRegex.Match(wildcard);
@@ -96,8 +156,14 @@ public async Task<IEnumerable<string>> ResolveHostnameAsync(string hostname, Can
96156
{
97157
IPAddress[] addresses = await Dns.GetHostAddressesAsync(hostname);
98158
return addresses
99-
.Where(addr => addr.AddressFamily == AddressFamily.InterNetwork) // IPv4 only for now
100-
.Select(addr => addr.ToString())
159+
.Where(addr => addr.AddressFamily == AddressFamily.InterNetwork || addr.AddressFamily == AddressFamily.InterNetworkV6)
160+
.Select(addr =>
161+
{
162+
// Include zone index for IPv6 link-local if needed
163+
if (addr.IsIPv6LinkLocal && addr.ScopeId != 0)
164+
return addr + "%" + addr.ScopeId;
165+
return addr.ToString();
166+
})
101167
.ToList();
102168
}
103169
catch (Exception ex)
@@ -157,7 +223,7 @@ public async Task<IEnumerable<string>> ResolveHostnameAsync(string hostname, Can
157223
else if (target.cidr != null)
158224
{
159225
string cidr = (string)target.cidr;
160-
hosts.AddRange(ExpandCidr(cidr));
226+
hosts.AddRange(ExpandCidr(cidr)); // automatically handles IPv4/IPv6
161227
}
162228
else if (target.wildcard != null)
163229
{

ThingConnect.Pulse.Server/config.schema.json

Lines changed: 102 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,29 @@
1111
"type": "object",
1212
"additionalProperties": false,
1313
"properties": {
14-
"interval_seconds": { "type": ["integer", "string"], "minimum": 1, "default": 10 },
15-
"timeout_ms": { "type": ["integer", "string"], "minimum": 100, "default": 1500 },
16-
"retries": { "type": ["integer", "string"], "minimum": 0, "default": 1 },
14+
"interval_seconds": {
15+
"type": ["integer", "string"],
16+
"minimum": 1,
17+
"default": 10
18+
},
19+
"timeout_ms": {
20+
"type": ["integer", "string"],
21+
"minimum": 100,
22+
"default": 1500
23+
},
24+
"retries": {
25+
"type": ["integer", "string"],
26+
"minimum": 0,
27+
"default": 1
28+
},
1729
"http": {
1830
"type": "object",
1931
"additionalProperties": false,
2032
"properties": {
21-
"user_agent": { "type": "string", "default": "ThingConnectPulse/1.0" },
33+
"user_agent": {
34+
"type": "string",
35+
"default": "ThingConnectPulse/1.0"
36+
},
2237
"expect_text": { "type": "string", "default": "" }
2338
},
2439
"default": {}
@@ -39,7 +54,10 @@
3954
},
4055
"name": { "type": "string", "minLength": 1 },
4156
"parent_id": { "type": ["string", "null"] },
42-
"color": { "type": "string", "pattern": "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$" },
57+
"color": {
58+
"type": "string",
59+
"pattern": "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$"
60+
},
4361
"sort_order": { "type": ["integer", "string"] }
4462
},
4563
"additionalProperties": false
@@ -62,7 +80,8 @@
6280
},
6381
"ipv6": {
6482
"type": "string",
65-
"pattern": "^[0-9A-Fa-f:]+$"
83+
"pattern": "^[0-9A-Fa-f:]+(%[0-9A-Za-z]+)?$",
84+
"description": "IPv6 address, optionally with zone index (e.g., fe80::1%3)"
6685
},
6786
"cidr": {
6887
"type": "string",
@@ -81,16 +100,39 @@
81100
"type": { "$ref": "#/definitions/probeType" },
82101
"group": { "type": "string" },
83102
"name": { "type": ["string", "null"] },
84-
"host": { "type": ["string", "null"] },
103+
"host": {
104+
"type": ["string", "null"],
105+
"anyOf": [
106+
{ "$ref": "#/definitions/hostname" },
107+
{ "$ref": "#/definitions/ipv4" },
108+
{ "$ref": "#/definitions/ipv6" }
109+
]
110+
},
85111
"cidr": { "type": ["string", "null"] },
86112
"wildcard": { "type": ["string", "null"] },
87-
"port": { "type": ["integer", "string", "null"], "minimum": 1, "maximum": 65535 },
113+
"port": {
114+
"type": ["integer", "string", "null"],
115+
"minimum": 1,
116+
"maximum": 65535
117+
},
88118
"http_path": { "type": ["string", "null"] },
89119
"http_match": { "type": ["string", "null"] },
90-
"interval_seconds": { "type": ["integer", "string", "null"], "minimum": 1 },
91-
"timeout_ms": { "type": ["integer", "string", "null"], "minimum": 100 },
92-
"retries": { "type": ["integer", "string", "null"], "minimum": 0 },
93-
"expected_rtt_ms": { "type": ["integer", "string", "null"], "minimum": 1 },
120+
"interval_seconds": {
121+
"type": ["integer", "string", "null"],
122+
"minimum": 1
123+
},
124+
"timeout_ms": {
125+
"type": ["integer", "string", "null"],
126+
"minimum": 100
127+
},
128+
"retries": {
129+
"type": ["integer", "string", "null"],
130+
"minimum": 0
131+
},
132+
"expected_rtt_ms": {
133+
"type": ["integer", "string", "null"],
134+
"minimum": 1
135+
},
94136
"enabled": { "type": ["boolean", "null"] },
95137
"notes": { "type": ["string", "null"] }
96138
},
@@ -110,13 +152,55 @@
110152
"examples": [
111153
{
112154
"version": 1,
113-
"defaults": { "interval_seconds": 10, "timeout_ms": 1500, "retries": 1, "http": { "user_agent": "ThingConnectPulse/1.0", "expect_text": "" } },
114-
"groups": [ { "id": "new-press", "name": "Press Shop" }, { "id": "new-lab", "name": "Quality Lab" } ],
155+
"defaults": {
156+
"interval_seconds": 10,
157+
"timeout_ms": 1500,
158+
"retries": 1,
159+
"http": { "user_agent": "ThingConnectPulse/1.0", "expect_text": "" }
160+
},
161+
"groups": [
162+
{ "id": "new-press", "name": "Press Shop" },
163+
{ "id": "new-lab", "name": "Quality Lab" }
164+
],
115165
"targets": [
116-
{ "type": "icmp", "host": "10.10.1.21", "name": "PLC-Press-01", "group": "press", "interval_seconds": 5 },
117-
{ "type": "tcp", "wildcard": "10.10.1.*", "port": 5900, "name": "HMI-VNC", "group": "press" },
118-
{ "type": "http", "cidr": "10.10.2.0/28", "http_path": "/health", "http_match": "OK", "name": "QMS-HTTP", "group": "lab" }
166+
{
167+
"type": "icmp",
168+
"host": "10.10.1.21",
169+
"name": "PLC-Press-01",
170+
"group": "press",
171+
"interval_seconds": 5
172+
},
173+
{
174+
"type": "tcp",
175+
"wildcard": "10.10.1.*",
176+
"port": 5900,
177+
"name": "HMI-VNC",
178+
"group": "press"
179+
},
180+
{
181+
"type": "http",
182+
"cidr": "10.10.2.0/28",
183+
"http_path": "/health",
184+
"http_match": "OK",
185+
"name": "QMS-HTTP",
186+
"group": "lab"
187+
},
188+
{
189+
"type": "icmp",
190+
"host": "fd7a:115c:a1e0::3801:160a",
191+
"name": "Tailscale-Node-01",
192+
"group": "network",
193+
"interval_seconds": 5
194+
},
195+
{
196+
"type": "http",
197+
"cidr": "2001:db8:abcd:1234::/64",
198+
"http_path": "/health",
199+
"http_match": "OK",
200+
"name": "IPv6-HTTP-Test",
201+
"group": "lab"
202+
}
119203
]
120204
}
121205
]
122-
}
206+
}

0 commit comments

Comments
 (0)