fix: Tailscale detection reads DNSName from Self + suppress cryptography deprecation

Bug 1: detect_tailscale() looked for DNSName at top level of JSON but
tailscale status --self --json nests it inside Self.DNSName. Fixed to
read from Self first with top-level fallback.

Bug 2: getattr(cert, 'not_valid_after_utc', cert.not_valid_after)
eagerly evaluated the fallback, triggering CryptographyDeprecationWarning.
Replaced with try/except AttributeError pattern in all three occurrences
(generate_self_signed and get_cert_info).
This commit is contained in:
Brian Krabach
2026-04-04 03:07:13 -07:00
parent ddba3baeb1
commit cb2d425fa7
2 changed files with 82 additions and 4 deletions
+64
View File
@@ -511,3 +511,67 @@ def test_generate_mkcert_includes_tailscale_sans(monkeypatch, tmp_path):
assert "spark-1.tail8f3c4e.ts.net" in gen_calls[0], (
f"'spark-1.tail8f3c4e.ts.net' must appear in mkcert command args, got: {gen_calls[0]!r}"
)
# ---------------------------------------------------------------------------
# 24-25. Bug-fix regression tests
# ---------------------------------------------------------------------------
def test_detect_tailscale_reads_dns_name_from_self(monkeypatch):
"""detect_tailscale() must read DNSName from Self nested object (actual Tailscale JSON structure)."""
import json
import shutil
import subprocess
import muxplex.tls as tls_mod
class MockResult:
returncode = 0
stdout = json.dumps(
{
"BackendState": "Running",
"TailscaleIPs": ["100.124.126.19"],
"CertDomains": ["spark-1.tail8f3c4e.ts.net"],
"Self": {
"DNSName": "spark-1.tail8f3c4e.ts.net.",
"HostName": "spark-1",
},
}
)
monkeypatch.setattr(subprocess, "run", lambda *a, **kw: MockResult())
monkeypatch.setattr(shutil, "which", lambda x: "/usr/bin/tailscale")
result = tls_mod.detect_tailscale()
assert result is not None, (
"detect_tailscale() must not return None when DNSName is in Self"
)
assert result["hostname"] == "spark-1.tail8f3c4e.ts.net", (
f"hostname must be 'spark-1.tail8f3c4e.ts.net' (stripped from Self.DNSName), got: {result['hostname']!r}"
)
def test_generate_self_signed_no_deprecation_warning(tmp_path):
"""generate_self_signed() and get_cert_info() must not emit CryptographyDeprecationWarning."""
import warnings
from muxplex.tls import generate_self_signed, get_cert_info
cert_path = tmp_path / "cert.pem"
key_path = tmp_path / "key.pem"
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
generate_self_signed(cert_path, key_path, hostnames=["localhost"])
get_cert_info(cert_path)
deprecation_warnings = [
str(warning.message)
for warning in w
if issubclass(warning.category, (DeprecationWarning, PendingDeprecationWarning))
and "not_valid_after" in str(warning.message).lower()
]
assert deprecation_warnings == [], (
f"CryptographyDeprecationWarning must not be raised, got: {deprecation_warnings}"
)
+18 -4
View File
@@ -91,7 +91,10 @@ def generate_self_signed(
.sign(private_key, hashes.SHA256())
)
expires = getattr(cert, "not_valid_after_utc", cert.not_valid_after)
try:
expires = cert.not_valid_after_utc # type: ignore[attr-defined]
except AttributeError:
expires = cert.not_valid_after # type: ignore[attr-defined]
# Write key PEM — create file with restricted permissions before writing
key_pem = private_key.private_bytes(
@@ -152,7 +155,8 @@ def detect_tailscale() -> dict | None:
except json.JSONDecodeError:
return None
dns_name = data.get("DNSName", "")
self_info = data.get("Self") or {}
dns_name = self_info.get("DNSName", "") or data.get("DNSName", "")
cert_domains = data.get("CertDomains") or []
ips = data.get("TailscaleIPs") or []
@@ -358,9 +362,19 @@ def get_cert_info(cert_path) -> dict | None:
except ExtensionNotFound:
pass
try:
expires = cert.not_valid_after_utc # type: ignore[attr-defined]
except AttributeError:
expires = cert.not_valid_after # type: ignore[attr-defined]
try:
not_before = cert.not_valid_before_utc # type: ignore[attr-defined]
except AttributeError:
not_before = cert.not_valid_before # type: ignore[attr-defined]
return {
"expires": getattr(cert, "not_valid_after_utc", cert.not_valid_after),
"not_before": getattr(cert, "not_valid_before_utc", cert.not_valid_before),
"expires": expires,
"not_before": not_before,
"hostnames": hostnames,
"serial": cert.serial_number,
}