`doctor`'s checkSecretHost and the proxy-startup warnSNIMismatch each
carried their own copy of the same logic: resolve the secret hostname,
determine the server's public IPv4/IPv6 (config first, getIP fallback),
and compare the two sets.
Extract that data-gathering into runSNICheck (internal/cli/sni_check.go),
returning an sniCheckResult. The success decision stays with each caller
because the rules genuinely differ — `doctor` reports OK when any family
matches, while the startup warning requires every detected family to
match — so only the gathering is shared, not the verdict.
No behavior change: both callers produce byte-identical output and the
same return values as before.