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:
@@ -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")
|
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"))
|
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 configuration
|
||||||
POOL_COUNT: int = int(os.getenv("HAPROXY_POOL_COUNT", "100"))
|
POOL_COUNT: int = int(os.getenv("HAPROXY_POOL_COUNT", "100"))
|
||||||
MAX_SLOTS: int = int(os.getenv("HAPROXY_MAX_SLOTS", "10"))
|
MAX_SLOTS: int = int(os.getenv("HAPROXY_MAX_SLOTS", "10"))
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from ..config import (
|
|||||||
MAX_SLOTS,
|
MAX_SLOTS,
|
||||||
SUBPROCESS_TIMEOUT,
|
SUBPROCESS_TIMEOUT,
|
||||||
CERTS_DIR,
|
CERTS_DIR,
|
||||||
|
CUSTOM_TLDS,
|
||||||
REMOTE_MODE,
|
REMOTE_MODE,
|
||||||
logger,
|
logger,
|
||||||
)
|
)
|
||||||
@@ -37,13 +38,43 @@ from ..db import db_load_certs
|
|||||||
from ..utils import parse_servers_state, disable_server_slot
|
from ..utils import parse_servers_state, disable_server_slot
|
||||||
|
|
||||||
|
|
||||||
def _check_subdomain(domain: str, registered_domains: set[str]) -> tuple[bool, Optional[str]]:
|
def _get_base_domain(domain: str) -> Optional[str]:
|
||||||
"""Check if a domain is a subdomain of an existing registered domain or certificate domain.
|
"""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.
|
Examples (with CUSTOM_TLDS={"it.com"}):
|
||||||
nocodb.inouter.com is a subdomain if inouter.com has a certificate.
|
inouter.com -> inouter.com (base domain itself)
|
||||||
Subdomains should not have wildcard entries added to avoid conflicts,
|
nocodb.inouter.com -> inouter.com
|
||||||
because wildcard certs (*.example.com) only cover one level deep.
|
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:
|
Args:
|
||||||
domain: Domain name to check (e.g., "api.example.com").
|
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:
|
Returns:
|
||||||
Tuple of (is_subdomain, parent_domain or None).
|
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())
|
cert_domains = set(db_load_certs())
|
||||||
known_domains = registered_domains | cert_domains
|
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:])
|
candidate = ".".join(parts[i:])
|
||||||
if candidate in known_domains:
|
if candidate in known_domains:
|
||||||
return True, candidate
|
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
|
return False, None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user