Skip to content

Commit 922e994

Browse files
committed
feat: handled ipv6 link local zone index in probe serivce
1 parent 3da2366 commit 922e994

File tree

3 files changed

+68
-39
lines changed

3 files changed

+68
-39
lines changed

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ public IEnumerable<string> ExpandWildcard(string wildcard, int startRange = 1, i
151151
}
152152

153153
/// <summary>
154-
/// Expands a single IPv6 host into compressed (::) and full (0000:0000:...) forms.
154+
/// Expands a single IPv6 host into compressed (::) and full (xxxx:xxxx:....) forms.
155155
/// Handles optional zone index.
156156
/// </summary>
157157
private IEnumerable<string> ExpandIPv6Host(string host)
@@ -172,11 +172,10 @@ private IEnumerable<string> ExpandIPv6Host(string host)
172172
// Compressed form
173173
string compressed = ip.ToString();
174174

175-
// Full form
176-
string full = string.Join(":", ip.GetAddressBytes()
177-
.Select((b, i) => i % 2 == 0 ? $"{b:X2}" : $"{b:X2}")
178-
.Select((s, i) => i % 2 == 1 ? s : s) // ensure proper grouping
179-
);
175+
// Full form (always 8 groups of 4 hex digits)
176+
string full = string.Join(":", Enumerable.Range(0, 8)
177+
.Select(i => ((ushort)((ip.GetAddressBytes()[i * 2] << 8) |
178+
ip.GetAddressBytes()[i * 2 + 1])).ToString("x4")));
180179

181180
if (zoneIndex != null)
182181
{

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

Lines changed: 61 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -60,18 +60,32 @@ public async Task<CheckResult> PingAsync(Guid endpointId, string host, int timeo
6060

6161
try
6262
{
63-
// Use overload that accepts cancellation token
64-
PingReply reply = await ping.SendPingAsync(host, TimeSpan.FromMilliseconds(timeoutMs), cancellationToken: combinedCts.Token);
65-
stopwatch.Stop();
63+
PingReply reply;
6664

67-
if (reply.Status == IPStatus.Success)
65+
// Handle IPv6 link-local with scope
66+
if (IPAddress.TryParse(host, out var ip))
6867
{
69-
return CheckResult.Success(endpointId, timestamp, reply.RoundtripTime);
68+
if (ip.AddressFamily == AddressFamily.InterNetworkV6 && host.Contains('%'))
69+
{
70+
var parts = host.Split('%');
71+
ip = IPAddress.Parse(parts[0]);
72+
ip.ScopeId = long.Parse(parts[1]);
73+
}
74+
75+
reply = await ping.SendPingAsync(ip, timeoutMs);
7076
}
7177
else
7278
{
73-
return CheckResult.Failure(endpointId, timestamp, $"Ping failed: {reply.Status}");
79+
// Hostname fallback
80+
reply = await ping.SendPingAsync(host, timeoutMs);
7481
}
82+
83+
stopwatch.Stop();
84+
85+
if (reply.Status == IPStatus.Success)
86+
return CheckResult.Success(endpointId, timestamp, reply.RoundtripTime);
87+
else
88+
return CheckResult.Failure(endpointId, timestamp, $"Ping failed: {reply.Status}");
7589
}
7690
catch (OperationCanceledException) when (timeoutCts.Token.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
7791
{
@@ -105,12 +119,25 @@ public async Task<CheckResult> TcpConnectAsync(Guid endpointId, string host, int
105119
using var timeoutCts = new CancellationTokenSource(timeoutMs);
106120
using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
107121

108-
// Use the combined cancellation token for the connection
109-
Task connectTask = tcpClient.ConnectAsync(host, port, combinedCts.Token).AsTask();
110-
111122
try
112123
{
113-
await connectTask;
124+
// Handle IPv6 link-local with scope
125+
if (IPAddress.TryParse(host, out var ip))
126+
{
127+
if (ip.AddressFamily == AddressFamily.InterNetworkV6 && host.Contains('%'))
128+
{
129+
var parts = host.Split('%');
130+
ip = IPAddress.Parse(parts[0]);
131+
ip.ScopeId = long.Parse(parts[1]);
132+
}
133+
134+
await tcpClient.ConnectAsync(ip, port, combinedCts.Token);
135+
}
136+
else
137+
{
138+
await tcpClient.ConnectAsync(host, port, combinedCts.Token);
139+
}
140+
114141
stopwatch.Stop();
115142
return CheckResult.Success(endpointId, timestamp, stopwatch.ElapsedMilliseconds);
116143
}
@@ -196,10 +223,28 @@ public async Task<CheckResult> HttpCheckAsync(Guid endpointId, string host, int
196223

197224
/// <summary>
198225
/// Builds HTTP URL with proper protocol detection and validation.
199-
/// Handles cases where host already contains protocol or needs port-based detection.
226+
/// Handles IPv6 addresses, scope IDs, existing protocols, ports, and optional path.
200227
/// </summary>
201228
private static string BuildHttpUrl(string host, int port, string? path)
202229
{
230+
// Wrap IPv6 in brackets and escape scope IDs for URLs
231+
if (IPAddress.TryParse(host, out var ip) && ip.AddressFamily == AddressFamily.InterNetworkV6)
232+
{
233+
// Handle scope index (zone)
234+
if (host.Contains('%'))
235+
{
236+
var parts = host.Split('%');
237+
string baseHost = parts[0];
238+
string scope = parts[1];
239+
// Per RFC 6874: must escape "%" as "%25" in URLs
240+
host = $"[{baseHost}%25{scope}]";
241+
}
242+
else
243+
{
244+
host = $"[{host}]";
245+
}
246+
}
247+
203248
string url;
204249

205250
// Check if host already contains a protocol
@@ -229,49 +274,32 @@ private static string BuildHttpUrl(string host, int port, string? path)
229274
else
230275
{
231276
// Host doesn't contain protocol, determine from port and context
232-
string scheme;
233-
234277
// Use smart defaults: 443 and common HTTPS ports default to HTTPS, others to HTTP
235-
if (port == 443 || IsCommonHttpsPort(port))
236-
{
237-
scheme = "https";
238-
}
239-
else
240-
{
241-
scheme = "http";
242-
}
243-
244-
url = $"{scheme}://{host}";
278+
string scheme = (port == 443 || IsCommonHttpsPort(port)) ? "https" : "http";
245279

246280
// Add port if not standard for the chosen protocol
247281
int standardPort = scheme == "https" ? 443 : 80;
282+
283+
url = $"{scheme}://{host}";
248284
if (port != standardPort)
249-
{
250285
url += $":{port}";
251-
}
252286
}
253-
254287
// Add path if specified
255288
if (!string.IsNullOrEmpty(path))
256289
{
257290
// Ensure URL ends with host/port and path starts with /
258291
if (!url.EndsWith("/") && !path.StartsWith("/"))
259-
{
260292
url += "/";
261-
}
262293
else if (url.EndsWith("/") && path.StartsWith("/"))
263-
{
264-
// Remove duplicate slash
265-
path = path.Substring(1);
266-
}
294+
path = path.Substring(1); // Remove duplicate slash
267295

268296
url += path;
269297
}
270298

271299
// Validate the final URL
272300
try
273301
{
274-
var validationUri = new Uri(url);
302+
_ = new Uri(url);
275303
return url;
276304
}
277305
catch (UriFormatException ex)

ThingConnect.Pulse.Server/config.schema.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,12 @@
8585
},
8686
"cidr": {
8787
"type": "string",
88+
"minLength": 1,
8889
"pattern": "^(?:(?:(?:25[0-5]|2[0-4]\\d|1?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1?\\d?\\d)\\/(?:[0-9]|[12][0-9]|3[0-2]))|(?:(?:[0-9A-Fa-f:]+)\\/(?:[0-9]|[1-9]\\d|1[01]\\d|12[0-8]))$"
8990
},
9091
"wildcard": {
9192
"type": "string",
93+
"minLength": 1,
9294
"description": "IPv4 wildcard like 10.10.1.* (expands 1..254 by default)",
9395
"pattern": "^(?:(?:25[0-5]|2[0-4]\\d|1?\\d?\\d)\\.){3}\\*$"
9496
},

0 commit comments

Comments
 (0)