Add live TLS certificate probing and improve restart error handling.

Configure CertificateCheckUrl per container for curl-like TLS checks, classify Sonic permission errors, and extend setup wizard for container management.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-03 13:16:21 +02:00
co-authored by Cursor
parent d20e0df618
commit 58b7826162
1430 changed files with 358394 additions and 71 deletions
@@ -34,7 +34,20 @@ public sealed class CertificateProbeService
+ string.Join(", ", SupportedExtensions));
}
if (!File.Exists(normalized))
PathReachability reachability = AssessPathReachability(normalized);
if (reachability == PathReachability.AccessDenied)
{
return CertificateProbeResult.ForAccessDenied(normalized);
}
if (reachability == PathReachability.Unreachable)
{
return CertificateProbeResult.Failed(
"Pfad nicht erreichbar.");
}
if (reachability == PathReachability.FileMissing)
{
return CertificateProbeResult.Missing(
normalized,
@@ -72,6 +85,14 @@ public sealed class CertificateProbeService
"Das Kennwort ist falsch oder die Datei ist beschädigt.");
}
}
catch (UnauthorizedAccessException)
{
return CertificateProbeResult.ForAccessDenied(normalized);
}
catch (IOException ex) when (IsAccessOrLogonFailure(ex))
{
return CertificateProbeResult.ForAccessDenied(normalized);
}
catch (Exception ex)
{
return CertificateProbeResult.Failed(
@@ -151,6 +172,154 @@ public sealed class CertificateProbeService
|| extension.Equals(".p12", StringComparison.OrdinalIgnoreCase);
}
private enum PathReachability
{
FileExists,
FileMissing,
AccessDenied,
Unreachable
}
/// <summary>
/// Unterscheidet „Datei fehlt“ von „Share/Pfad ohne Admin-Konto nicht erreichbar“.
/// File.Exists liefert bei fehlenden Credentials oft nur false.
/// </summary>
private static PathReachability AssessPathReachability(string filePath)
{
try
{
if (File.Exists(filePath))
{
return PathReachability.FileExists;
}
}
catch (UnauthorizedAccessException)
{
return PathReachability.AccessDenied;
}
catch (IOException ex) when (IsAccessOrLogonFailure(ex))
{
return PathReachability.AccessDenied;
}
catch (IOException)
{
return PathReachability.Unreachable;
}
string? directory = Path.GetDirectoryName(filePath);
if (string.IsNullOrWhiteSpace(directory))
{
return PathReachability.Unreachable;
}
return AssessDirectoryReachability(directory);
}
private static PathReachability AssessDirectoryReachability(string directory)
{
string current = directory.TrimEnd('\\', '/');
while (!string.IsNullOrWhiteSpace(current))
{
try
{
if (Directory.Exists(current))
{
// Exists kann bei UNC ohne Rechte „lügen“ Auflisten erzwingen.
_ = Directory.EnumerateFileSystemEntries(current)
.Any();
return PathReachability.FileMissing;
}
if (current.StartsWith(@"\\", StringComparison.Ordinal))
{
// Exists=false: echter Zugriffsfehler oder Ordner fehlt.
_ = Directory.GetFileSystemEntries(current);
return PathReachability.FileMissing;
}
// Lokaler Ordner fehlt → für Erst-Deployment als „Datei fehlt“ werten.
return PathReachability.FileMissing;
}
catch (UnauthorizedAccessException)
{
return PathReachability.AccessDenied;
}
catch (IOException ex) when (IsAccessOrLogonFailure(ex))
{
return PathReachability.AccessDenied;
}
catch (DirectoryNotFoundException)
{
string? parent = GetParentPath(current);
if (parent is null || parent == current)
{
return current.StartsWith(@"\\", StringComparison.Ordinal)
? PathReachability.Unreachable
: PathReachability.FileMissing;
}
current = parent;
continue;
}
catch (IOException)
{
return PathReachability.Unreachable;
}
}
return PathReachability.Unreachable;
}
private static string? GetParentPath(string path)
{
string trimmed = path.TrimEnd('\\', '/');
if (trimmed.StartsWith(@"\\", StringComparison.Ordinal))
{
// \\server\share → Stop; darunter weiter nach oben.
string withoutPrefix = trimmed[2..];
int slash = withoutPrefix.IndexOfAny(['\\', '/']);
if (slash < 0)
{
return null;
}
int second = withoutPrefix.IndexOfAny(['\\', '/'], slash + 1);
if (second < 0)
{
// Bereits Share-Root \\server\share
return null;
}
}
string? parent = Path.GetDirectoryName(trimmed);
return string.IsNullOrWhiteSpace(parent) ? null : parent;
}
private static bool IsAccessOrLogonFailure(IOException ex)
{
// HRESULT-Lower-Word = Win32-Fehlercode
int win32 = ex.HResult & 0xFFFF;
return win32 is
5 or // ERROR_ACCESS_DENIED
53 or // ERROR_BAD_NETPATH
67 or // ERROR_BAD_NET_NAME
86 or // ERROR_INVALID_PASSWORD
1326 or // ERROR_LOGON_FAILURE
59 or // ERROR_UNEXP_NET_ERR
64 or // ERROR_NETNAME_DELETED
1219 or // ERROR_SESSION_CREDENTIAL_CONFLICT
1240 or // ERROR_LOGIN_WKSTA_RESTRICTION
1245 or // ERROR_ACCOUNT_RESTRICTION
1396; // ERROR_WRONG_TARGET_NAME
}
private static string? NormalizePath(string? path)
{
if (string.IsNullOrWhiteSpace(path))
@@ -233,10 +402,14 @@ public sealed class CertificateProbeService
public sealed class CertificateProbeResult
{
public const string AccessDeniedShortText = "Admin nötig";
public bool Success { get; init; }
public bool FileExists { get; init; }
public bool AccessDenied { get; init; }
public string Path { get; init; } = string.Empty;
public string Message { get; init; } = string.Empty;
@@ -289,6 +462,19 @@ public sealed class CertificateProbeResult
};
}
public static CertificateProbeResult ForAccessDenied(string path)
{
return new CertificateProbeResult
{
Success = false,
FileExists = false,
AccessDenied = true,
Path = path,
Message =
"Kein Zugriff auf den Pfad Admin-Konto nötig."
};
}
public static CertificateProbeResult PasswordRequired(string path)
{
return new CertificateProbeResult