Detect subdomains structurally to skip wildcard entries without certs

Add CUSTOM_TLDS config (HAPROXY_CUSTOM_TLDS env, default: "it.com")
and _get_base_domain() for eTLD+1 detection. _check_subdomain now uses
three layers: registered domains, certificate domains, and structural
analysis. This ensures nocodb.inouter.com never gets a *.nocodb wildcard
entry even when inouter.com has no cert or registration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-02-08 20:34:57 +09:00
parent 81737bb256
commit 170c48e257
2 changed files with 50 additions and 7 deletions

View File

@@ -39,6 +39,12 @@ CERTS_DIR: str = os.getenv("HAPROXY_CERTS_DIR", "/opt/haproxy/certs")
CERTS_DIR_CONTAINER: str = os.getenv("HAPROXY_CERTS_DIR_CONTAINER", "/etc/haproxy/certs")
ACME_HOME: str = os.getenv("ACME_HOME", os.path.expanduser("~/.acme.sh"))
# Custom multi-part TLDs (e.g., "it.com" treated as a TLD so "anvil.it.com" is a base domain)
# Comma-separated list via env var, or default
CUSTOM_TLDS: frozenset[str] = frozenset(
t.strip() for t in os.getenv("HAPROXY_CUSTOM_TLDS", "it.com").split(",") if t.strip()
)
# Pool configuration
POOL_COUNT: int = int(os.getenv("HAPROXY_POOL_COUNT", "100"))
MAX_SLOTS: int = int(os.getenv("HAPROXY_MAX_SLOTS", "10"))

View File

@@ -12,6 +12,7 @@ from ..config import (
MAX_SLOTS,
SUBPROCESS_TIMEOUT,
CERTS_DIR,
CUSTOM_TLDS,
REMOTE_MODE,
logger,
)
@@ -37,13 +38,43 @@ from ..db import db_load_certs
from ..utils import parse_servers_state, disable_server_slot
def _check_subdomain(domain: str, registered_domains: set[str]) -> tuple[bool, Optional[str]]:
"""Check if a domain is a subdomain of an existing registered domain or certificate domain.
def _get_base_domain(domain: str) -> Optional[str]:
"""Get the base domain (eTLD+1) considering custom multi-part TLDs.
For example, vault.anvil.it.com is a subdomain if anvil.it.com exists.
nocodb.inouter.com is a subdomain if inouter.com has a certificate.
Subdomains should not have wildcard entries added to avoid conflicts,
because wildcard certs (*.example.com) only cover one level deep.
Examples (with CUSTOM_TLDS={"it.com"}):
inouter.com -> inouter.com (base domain itself)
nocodb.inouter.com -> inouter.com
anvil.it.com -> anvil.it.com (base domain, it.com is TLD)
gitea.anvil.it.com -> anvil.it.com
Returns:
The base domain, or None if the domain is a TLD itself.
"""
parts = domain.split(".")
# Check custom multi-part TLDs first (e.g., it.com)
for tld in CUSTOM_TLDS:
tld_parts = tld.split(".")
if len(parts) > len(tld_parts) and domain.endswith("." + tld):
return ".".join(parts[-(len(tld_parts) + 1):])
# Standard single-part TLD (e.g., .com, .net, .org)
if len(parts) >= 2:
return ".".join(parts[-2:])
return None
def _check_subdomain(domain: str, registered_domains: set[str]) -> tuple[bool, Optional[str]]:
"""Check if a domain is a subdomain using structural analysis and known domains.
Uses three layers of detection:
1. Registered domains: vault.anvil.it.com is a subdomain if anvil.it.com is registered.
2. Certificate domains: nocodb.inouter.com is a subdomain if inouter.com has a cert.
3. Structural analysis: nocodb.inouter.com is deeper than eTLD+1 (inouter.com).
Wildcard entries are skipped for subdomains because wildcard certs
(*.example.com) only cover one level deep.
Args:
domain: Domain name to check (e.g., "api.example.com").
@@ -52,7 +83,7 @@ def _check_subdomain(domain: str, registered_domains: set[str]) -> tuple[bool, O
Returns:
Tuple of (is_subdomain, parent_domain or None).
"""
# Combine registered domains and certificate domains as known base domains
# Check against registered domains and certificate domains
cert_domains = set(db_load_certs())
known_domains = registered_domains | cert_domains
@@ -61,6 +92,12 @@ def _check_subdomain(domain: str, registered_domains: set[str]) -> tuple[bool, O
candidate = ".".join(parts[i:])
if candidate in known_domains:
return True, candidate
# Structural analysis: if domain is deeper than its base domain, it's a subdomain
base = _get_base_domain(domain)
if base and domain != base:
return True, base
return False, None