feat: HTTP-based Yield routing (no DNS server required)
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled
- Updated yield_dns.py to support A-record verification (simplest method!) - A-record pointing to 46.235.147.194 is now the primary verification method - Added Nginx catch-all config for yield domains - DNS schema updated with method and actual_a fields - CoreDNS installed but Port 53 blocked by hosting provider
This commit is contained in:
@ -527,8 +527,10 @@ async def verify_domain_dns(
|
|||||||
return DNSVerificationResult(
|
return DNSVerificationResult(
|
||||||
domain=domain.domain,
|
domain=domain.domain,
|
||||||
verified=verified,
|
verified=verified,
|
||||||
|
method=check.method,
|
||||||
expected_ns=settings.yield_nameserver_list,
|
expected_ns=settings.yield_nameserver_list,
|
||||||
actual_ns=actual_ns,
|
actual_ns=check.actual_ns,
|
||||||
|
actual_a=check.actual_a,
|
||||||
cname_ok=check.cname_ok if verified else False,
|
cname_ok=check.cname_ok if verified else False,
|
||||||
error=error,
|
error=error,
|
||||||
checked_at=datetime.utcnow(),
|
checked_at=datetime.utcnow(),
|
||||||
|
|||||||
@ -242,9 +242,11 @@ class DNSVerificationResult(BaseModel):
|
|||||||
"""Result of DNS verification check."""
|
"""Result of DNS verification check."""
|
||||||
domain: str
|
domain: str
|
||||||
verified: bool
|
verified: bool
|
||||||
|
method: Optional[str] = None # "a_record" | "cname" | "nameserver"
|
||||||
|
|
||||||
expected_ns: list[str]
|
expected_ns: list[str]
|
||||||
actual_ns: list[str]
|
actual_ns: list[str]
|
||||||
|
actual_a: list[str] = [] # A-records found for the domain
|
||||||
|
|
||||||
cname_ok: bool = False
|
cname_ok: bool = False
|
||||||
|
|
||||||
|
|||||||
255
backend/app/services/dns_zone_manager.py
Normal file
255
backend/app/services/dns_zone_manager.py
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
"""
|
||||||
|
DNS Zone Manager for Pounce Yield.
|
||||||
|
|
||||||
|
Manages CoreDNS zone files for yield domains.
|
||||||
|
When a domain is activated for yield, we add it to the zone file.
|
||||||
|
When deactivated, we remove it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from app.config import get_settings
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
settings = get_settings()
|
||||||
|
|
||||||
|
# CoreDNS zone file path
|
||||||
|
ZONE_FILE = Path("/opt/coredns/zones/db.yield")
|
||||||
|
SERVER_IP = "46.235.147.194"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_serial() -> str:
|
||||||
|
"""Generate zone serial in YYYYMMDDNN format."""
|
||||||
|
today = datetime.utcnow().strftime("%Y%m%d")
|
||||||
|
# Read current serial and increment if same day
|
||||||
|
if ZONE_FILE.exists():
|
||||||
|
content = ZONE_FILE.read_text()
|
||||||
|
match = re.search(r"(\d{8})(\d{2})\s*;\s*Serial", content)
|
||||||
|
if match:
|
||||||
|
existing_date = match.group(1)
|
||||||
|
existing_nn = int(match.group(2))
|
||||||
|
if existing_date == today:
|
||||||
|
return f"{today}{(existing_nn + 1):02d}"
|
||||||
|
return f"{today}01"
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_zone_file(domains: list[str]) -> str:
|
||||||
|
"""Generate the complete zone file content."""
|
||||||
|
serial = _get_serial()
|
||||||
|
|
||||||
|
zone_content = f"""; Pounce Yield DNS Zone
|
||||||
|
; Auto-generated by dns_zone_manager.py
|
||||||
|
; Last updated: {datetime.utcnow().isoformat()}Z
|
||||||
|
|
||||||
|
$TTL 300
|
||||||
|
$ORIGIN yield.pounce.ch.
|
||||||
|
|
||||||
|
@ IN SOA ns1.pounce.ch. admin.pounce.ch. (
|
||||||
|
{serial} ; Serial (YYYYMMDDNN)
|
||||||
|
3600 ; Refresh (1 hour)
|
||||||
|
600 ; Retry (10 minutes)
|
||||||
|
604800 ; Expire (1 week)
|
||||||
|
300 ; Minimum TTL (5 minutes)
|
||||||
|
)
|
||||||
|
|
||||||
|
; Nameservers
|
||||||
|
@ IN NS ns1.pounce.ch.
|
||||||
|
@ IN NS ns2.pounce.ch.
|
||||||
|
|
||||||
|
; A record for the zone apex
|
||||||
|
@ IN A {SERVER_IP}
|
||||||
|
|
||||||
|
; Wildcard - all subdomains point to our server
|
||||||
|
* IN A {SERVER_IP}
|
||||||
|
|
||||||
|
; ============================================
|
||||||
|
; YIELD DOMAINS (auto-managed)
|
||||||
|
; ============================================
|
||||||
|
"""
|
||||||
|
|
||||||
|
for domain in sorted(set(domains)):
|
||||||
|
# Ensure domain ends with a dot (FQDN format)
|
||||||
|
fqdn = domain.lower().strip()
|
||||||
|
if not fqdn.endswith("."):
|
||||||
|
fqdn += "."
|
||||||
|
zone_content += f"{fqdn:<30} IN A {SERVER_IP}\n"
|
||||||
|
|
||||||
|
return zone_content
|
||||||
|
|
||||||
|
|
||||||
|
def add_yield_domain(domain: str) -> bool:
|
||||||
|
"""
|
||||||
|
Add a domain to the yield DNS zone.
|
||||||
|
|
||||||
|
Returns True if successful, False otherwise.
|
||||||
|
"""
|
||||||
|
if not ZONE_FILE.exists():
|
||||||
|
logger.warning(f"Zone file not found: {ZONE_FILE}. CoreDNS may not be installed.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Read current domains from zone file
|
||||||
|
content = ZONE_FILE.read_text()
|
||||||
|
domains = _parse_domains_from_zone(content)
|
||||||
|
|
||||||
|
# Add new domain
|
||||||
|
domain_clean = domain.lower().strip().rstrip(".")
|
||||||
|
if domain_clean not in domains:
|
||||||
|
domains.append(domain_clean)
|
||||||
|
|
||||||
|
# Write updated zone
|
||||||
|
new_content = _generate_zone_file(domains)
|
||||||
|
ZONE_FILE.write_text(new_content)
|
||||||
|
|
||||||
|
# Reload CoreDNS
|
||||||
|
_reload_coredns()
|
||||||
|
|
||||||
|
logger.info(f"Added yield domain to DNS: {domain_clean}")
|
||||||
|
else:
|
||||||
|
logger.info(f"Domain already in DNS zone: {domain_clean}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to add domain {domain} to DNS: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def remove_yield_domain(domain: str) -> bool:
|
||||||
|
"""
|
||||||
|
Remove a domain from the yield DNS zone.
|
||||||
|
|
||||||
|
Returns True if successful, False otherwise.
|
||||||
|
"""
|
||||||
|
if not ZONE_FILE.exists():
|
||||||
|
logger.warning(f"Zone file not found: {ZONE_FILE}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
content = ZONE_FILE.read_text()
|
||||||
|
domains = _parse_domains_from_zone(content)
|
||||||
|
|
||||||
|
domain_clean = domain.lower().strip().rstrip(".")
|
||||||
|
if domain_clean in domains:
|
||||||
|
domains.remove(domain_clean)
|
||||||
|
|
||||||
|
new_content = _generate_zone_file(domains)
|
||||||
|
ZONE_FILE.write_text(new_content)
|
||||||
|
|
||||||
|
_reload_coredns()
|
||||||
|
|
||||||
|
logger.info(f"Removed yield domain from DNS: {domain_clean}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to remove domain {domain} from DNS: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def sync_yield_domains(domains: list[str]) -> bool:
|
||||||
|
"""
|
||||||
|
Sync all yield domains to the DNS zone.
|
||||||
|
|
||||||
|
Replaces all existing yield domain entries with the provided list.
|
||||||
|
"""
|
||||||
|
if not ZONE_FILE.parent.exists():
|
||||||
|
logger.warning(f"Zone directory not found: {ZONE_FILE.parent}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
clean_domains = [d.lower().strip().rstrip(".") for d in domains]
|
||||||
|
new_content = _generate_zone_file(clean_domains)
|
||||||
|
|
||||||
|
# Ensure directory exists
|
||||||
|
ZONE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
ZONE_FILE.write_text(new_content)
|
||||||
|
|
||||||
|
_reload_coredns()
|
||||||
|
|
||||||
|
logger.info(f"Synced {len(clean_domains)} yield domains to DNS")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to sync yield domains: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_domains_from_zone(content: str) -> list[str]:
|
||||||
|
"""Extract yield domain names from zone file content."""
|
||||||
|
domains = []
|
||||||
|
|
||||||
|
# Look for lines after "YIELD DOMAINS" marker
|
||||||
|
in_yield_section = False
|
||||||
|
for line in content.split("\n"):
|
||||||
|
if "YIELD DOMAINS" in line:
|
||||||
|
in_yield_section = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
if in_yield_section and line.strip() and not line.strip().startswith(";"):
|
||||||
|
# Parse: domain.tld. IN A IP
|
||||||
|
match = re.match(r"^([a-z0-9.-]+)\.\s+IN\s+A\s+", line, re.IGNORECASE)
|
||||||
|
if match:
|
||||||
|
domain = match.group(1).rstrip(".")
|
||||||
|
# Skip wildcards and pounce domains
|
||||||
|
if domain != "*" and "pounce" not in domain:
|
||||||
|
domains.append(domain)
|
||||||
|
|
||||||
|
return domains
|
||||||
|
|
||||||
|
|
||||||
|
def _reload_coredns() -> bool:
|
||||||
|
"""Reload CoreDNS to pick up zone changes."""
|
||||||
|
try:
|
||||||
|
# Send SIGUSR1 to reload zone files without restart
|
||||||
|
result = subprocess.run(
|
||||||
|
["systemctl", "reload", "coredns"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
# Try restart as fallback
|
||||||
|
subprocess.run(
|
||||||
|
["systemctl", "restart", "coredns"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Could not reload CoreDNS: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def check_coredns_status() -> dict:
|
||||||
|
"""Check if CoreDNS is running and healthy."""
|
||||||
|
status = {
|
||||||
|
"installed": ZONE_FILE.parent.exists(),
|
||||||
|
"running": False,
|
||||||
|
"zone_file_exists": ZONE_FILE.exists(),
|
||||||
|
"domain_count": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["systemctl", "is-active", "coredns"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=5
|
||||||
|
)
|
||||||
|
status["running"] = result.stdout.strip() == "active"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if status["zone_file_exists"]:
|
||||||
|
content = ZONE_FILE.read_text()
|
||||||
|
status["domain_count"] = len(_parse_domains_from_zone(content))
|
||||||
|
|
||||||
|
return status
|
||||||
@ -2,8 +2,9 @@
|
|||||||
Yield DNS verification helpers.
|
Yield DNS verification helpers.
|
||||||
|
|
||||||
Production-grade DNS checks for the Yield Connect flow:
|
Production-grade DNS checks for the Yield Connect flow:
|
||||||
- Option A (recommended): Nameserver delegation to our nameservers
|
- Option A: A-record pointing directly to our server IP (simplest!)
|
||||||
- Option B (simpler): CNAME/ALIAS to a shared target
|
- Option B: CNAME/ALIAS to yield.pounce.ch
|
||||||
|
- Option C: Nameserver delegation to our nameservers (requires DNS server)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@ -14,11 +15,16 @@ from typing import Optional
|
|||||||
import dns.resolver
|
import dns.resolver
|
||||||
|
|
||||||
|
|
||||||
|
# Our server IP - domains with A-record pointing here are verified
|
||||||
|
YIELD_SERVER_IP = "46.235.147.194"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class YieldDNSCheckResult:
|
class YieldDNSCheckResult:
|
||||||
verified: bool
|
verified: bool
|
||||||
method: Optional[str] # "nameserver" | "cname" | None
|
method: Optional[str] # "a_record" | "cname" | "nameserver" | None
|
||||||
actual_ns: list[str]
|
actual_ns: list[str]
|
||||||
|
actual_a: list[str]
|
||||||
cname_ok: bool
|
cname_ok: bool
|
||||||
error: Optional[str]
|
error: Optional[str]
|
||||||
|
|
||||||
@ -37,7 +43,6 @@ def _normalize_host(host: str) -> str:
|
|||||||
def _resolve_ns(domain: str) -> list[str]:
|
def _resolve_ns(domain: str) -> list[str]:
|
||||||
r = _resolver()
|
r = _resolver()
|
||||||
answers = r.resolve(domain, "NS")
|
answers = r.resolve(domain, "NS")
|
||||||
# NS answers are RRset with .target
|
|
||||||
return sorted({_normalize_host(str(rr.target)) for rr in answers})
|
return sorted({_normalize_host(str(rr.target)) for rr in answers})
|
||||||
|
|
||||||
|
|
||||||
@ -57,112 +62,108 @@ def verify_yield_dns(domain: str, expected_nameservers: list[str], cname_target:
|
|||||||
"""
|
"""
|
||||||
Verify that a domain is connected for Yield.
|
Verify that a domain is connected for Yield.
|
||||||
|
|
||||||
We accept:
|
We accept (in order of simplicity):
|
||||||
- Nameserver delegation (NS contains all expected nameservers), OR
|
1. A-record pointing directly to our server IP (46.235.147.194)
|
||||||
- CNAME/ALIAS to `cname_target` (either CNAME matches, or A records match target A records)
|
2. CNAME/ALIAS to `cname_target` (yield.pounce.ch)
|
||||||
|
3. Nameserver delegation (NS contains all expected nameservers)
|
||||||
"""
|
"""
|
||||||
domain = _normalize_host(domain)
|
domain = _normalize_host(domain)
|
||||||
expected_ns = sorted({_normalize_host(ns) for ns in expected_nameservers if ns})
|
expected_ns = sorted({_normalize_host(ns) for ns in expected_nameservers if ns})
|
||||||
target = _normalize_host(cname_target)
|
target = _normalize_host(cname_target)
|
||||||
|
actual_ns: list[str] = []
|
||||||
|
actual_a: list[str] = []
|
||||||
|
|
||||||
if not domain:
|
if not domain:
|
||||||
return YieldDNSCheckResult(
|
return YieldDNSCheckResult(
|
||||||
verified=False,
|
verified=False,
|
||||||
method=None,
|
method=None,
|
||||||
actual_ns=[],
|
actual_ns=[],
|
||||||
|
actual_a=[],
|
||||||
cname_ok=False,
|
cname_ok=False,
|
||||||
error="Domain is empty",
|
error="Domain is empty",
|
||||||
)
|
)
|
||||||
if not expected_ns and not target:
|
|
||||||
return YieldDNSCheckResult(
|
|
||||||
verified=False,
|
|
||||||
method=None,
|
|
||||||
actual_ns=[],
|
|
||||||
cname_ok=False,
|
|
||||||
error="Yield DNS is not configured on server",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Option A: NS delegation
|
# Option A: Direct A-record to our server IP (SIMPLEST!)
|
||||||
try:
|
try:
|
||||||
actual_ns = _resolve_ns(domain)
|
actual_a = _resolve_a(domain)
|
||||||
if expected_ns and set(expected_ns).issubset(set(actual_ns)):
|
if YIELD_SERVER_IP in actual_a:
|
||||||
return YieldDNSCheckResult(
|
return YieldDNSCheckResult(
|
||||||
verified=True,
|
verified=True,
|
||||||
method="nameserver",
|
method="a_record",
|
||||||
actual_ns=actual_ns,
|
actual_ns=[],
|
||||||
|
actual_a=actual_a,
|
||||||
cname_ok=False,
|
cname_ok=False,
|
||||||
error=None,
|
error=None,
|
||||||
)
|
)
|
||||||
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
|
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
|
||||||
actual_ns = []
|
pass
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return YieldDNSCheckResult(
|
return YieldDNSCheckResult(
|
||||||
verified=False,
|
verified=False,
|
||||||
method=None,
|
method=None,
|
||||||
actual_ns=[],
|
actual_ns=[],
|
||||||
|
actual_a=[],
|
||||||
cname_ok=False,
|
cname_ok=False,
|
||||||
error=str(e),
|
error=str(e),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Option B: CNAME / ALIAS
|
# Option B: CNAME to yield.pounce.ch
|
||||||
if not target:
|
if target:
|
||||||
return YieldDNSCheckResult(
|
|
||||||
verified=False,
|
|
||||||
method=None,
|
|
||||||
actual_ns=actual_ns,
|
|
||||||
cname_ok=False,
|
|
||||||
error="Yield CNAME target is not configured on server",
|
|
||||||
)
|
|
||||||
|
|
||||||
# 1) Direct CNAME check (works for subdomain CNAME setups)
|
|
||||||
try:
|
try:
|
||||||
cnames = _resolve_cname(domain)
|
cnames = _resolve_cname(domain)
|
||||||
if any(c == target for c in cnames):
|
if any(c == target for c in cnames):
|
||||||
return YieldDNSCheckResult(
|
return YieldDNSCheckResult(
|
||||||
verified=True,
|
verified=True,
|
||||||
method="cname",
|
method="cname",
|
||||||
actual_ns=actual_ns,
|
actual_ns=[],
|
||||||
|
actual_a=actual_a,
|
||||||
cname_ok=True,
|
cname_ok=True,
|
||||||
error=None,
|
error=None,
|
||||||
)
|
)
|
||||||
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
|
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
|
||||||
pass
|
pass
|
||||||
except Exception as e:
|
except Exception:
|
||||||
return YieldDNSCheckResult(
|
pass
|
||||||
verified=False,
|
|
||||||
method=None,
|
|
||||||
actual_ns=actual_ns,
|
|
||||||
cname_ok=False,
|
|
||||||
error=str(e),
|
|
||||||
)
|
|
||||||
|
|
||||||
# 2) ALIAS/ANAME flattening: compare A records against target A records
|
# Also check if A-records match target's A-records (ALIAS/ANAME flattening)
|
||||||
try:
|
try:
|
||||||
target_as = set(_resolve_a(target))
|
target_as = set(_resolve_a(target))
|
||||||
domain_as = set(_resolve_a(domain))
|
if target_as and actual_a and set(actual_a).issubset(target_as):
|
||||||
if target_as and domain_as and domain_as.issubset(target_as):
|
|
||||||
return YieldDNSCheckResult(
|
return YieldDNSCheckResult(
|
||||||
verified=True,
|
verified=True,
|
||||||
method="cname",
|
method="cname",
|
||||||
actual_ns=actual_ns,
|
actual_ns=[],
|
||||||
|
actual_a=actual_a,
|
||||||
cname_ok=True,
|
cname_ok=True,
|
||||||
error=None,
|
error=None,
|
||||||
)
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Option C: NS delegation (requires DNS server on Port 53)
|
||||||
|
if expected_ns:
|
||||||
|
try:
|
||||||
|
actual_ns = _resolve_ns(domain)
|
||||||
|
if set(expected_ns).issubset(set(actual_ns)):
|
||||||
|
return YieldDNSCheckResult(
|
||||||
|
verified=True,
|
||||||
|
method="nameserver",
|
||||||
|
actual_ns=actual_ns,
|
||||||
|
actual_a=actual_a,
|
||||||
|
cname_ok=False,
|
||||||
|
error=None,
|
||||||
|
)
|
||||||
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
|
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
|
||||||
pass
|
pass
|
||||||
except Exception as e:
|
except Exception:
|
||||||
return YieldDNSCheckResult(
|
pass
|
||||||
verified=False,
|
|
||||||
method=None,
|
|
||||||
actual_ns=actual_ns,
|
|
||||||
cname_ok=False,
|
|
||||||
error=str(e),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
# Not verified
|
||||||
return YieldDNSCheckResult(
|
return YieldDNSCheckResult(
|
||||||
verified=False,
|
verified=False,
|
||||||
method=None,
|
method=None,
|
||||||
actual_ns=actual_ns,
|
actual_ns=actual_ns,
|
||||||
|
actual_a=actual_a,
|
||||||
cname_ok=False,
|
cname_ok=False,
|
||||||
error=None,
|
error=None,
|
||||||
)
|
)
|
||||||
|
|||||||
180
backend/scripts/setup_dns_server.sh
Executable file
180
backend/scripts/setup_dns_server.sh
Executable file
@ -0,0 +1,180 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================================================
|
||||||
|
# Pounce DNS Server Setup (CoreDNS)
|
||||||
|
# ============================================================================
|
||||||
|
# This script sets up CoreDNS as an authoritative DNS server for Yield domains.
|
||||||
|
# Users point their domains' NS records to ns1.pounce.ch and ns2.pounce.ch,
|
||||||
|
# which both resolve to this server's IP.
|
||||||
|
#
|
||||||
|
# Usage: sudo bash setup_dns_server.sh
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Pounce DNS Server Setup (CoreDNS)"
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
|
# Check if running as root
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
echo "ERROR: Please run as root (sudo)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SERVER_IP="46.235.147.194"
|
||||||
|
COREDNS_VERSION="1.11.1"
|
||||||
|
COREDNS_DIR="/opt/coredns"
|
||||||
|
ZONES_DIR="/opt/coredns/zones"
|
||||||
|
|
||||||
|
echo "[1/6] Installing dependencies..."
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y -qq wget curl jq
|
||||||
|
|
||||||
|
echo "[2/6] Downloading CoreDNS ${COREDNS_VERSION}..."
|
||||||
|
mkdir -p "$COREDNS_DIR"
|
||||||
|
cd "$COREDNS_DIR"
|
||||||
|
|
||||||
|
if [ ! -f "coredns" ]; then
|
||||||
|
wget -q "https://github.com/coredns/coredns/releases/download/v${COREDNS_VERSION}/coredns_${COREDNS_VERSION}_linux_amd64.tgz"
|
||||||
|
tar -xzf "coredns_${COREDNS_VERSION}_linux_amd64.tgz"
|
||||||
|
rm "coredns_${COREDNS_VERSION}_linux_amd64.tgz"
|
||||||
|
chmod +x coredns
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[3/6] Creating zone directory..."
|
||||||
|
mkdir -p "$ZONES_DIR"
|
||||||
|
|
||||||
|
echo "[4/6] Creating CoreDNS config (Corefile)..."
|
||||||
|
cat > "$COREDNS_DIR/Corefile" << 'COREFILE'
|
||||||
|
# CoreDNS Configuration for Pounce Yield
|
||||||
|
# Serves authoritative DNS for delegated yield domains
|
||||||
|
|
||||||
|
# Default zone - serves A record pointing to our server
|
||||||
|
. {
|
||||||
|
# Log all queries for debugging
|
||||||
|
log
|
||||||
|
|
||||||
|
# Serve zones from files
|
||||||
|
file /opt/coredns/zones/db.yield {
|
||||||
|
reload 30s
|
||||||
|
}
|
||||||
|
|
||||||
|
# Health check endpoint
|
||||||
|
health :8053
|
||||||
|
|
||||||
|
# Prometheus metrics
|
||||||
|
prometheus :9153
|
||||||
|
|
||||||
|
# Forward unknown queries (shouldn't happen for authoritative)
|
||||||
|
forward . 8.8.8.8 8.8.4.4 {
|
||||||
|
max_concurrent 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cache responses
|
||||||
|
cache 300
|
||||||
|
|
||||||
|
# Error handling
|
||||||
|
errors
|
||||||
|
}
|
||||||
|
COREFILE
|
||||||
|
|
||||||
|
echo "[5/6] Creating initial zone file..."
|
||||||
|
cat > "$ZONES_DIR/db.yield" << ZONEFILE
|
||||||
|
; Pounce Yield DNS Zone
|
||||||
|
; This file is dynamically updated by the Pounce backend
|
||||||
|
; DO NOT EDIT MANUALLY - changes will be overwritten
|
||||||
|
|
||||||
|
\$TTL 300
|
||||||
|
\$ORIGIN yield.pounce.ch.
|
||||||
|
|
||||||
|
@ IN SOA ns1.pounce.ch. admin.pounce.ch. (
|
||||||
|
$(date +%Y%m%d)01 ; Serial (YYYYMMDDNN)
|
||||||
|
3600 ; Refresh (1 hour)
|
||||||
|
600 ; Retry (10 minutes)
|
||||||
|
604800 ; Expire (1 week)
|
||||||
|
300 ; Minimum TTL (5 minutes)
|
||||||
|
)
|
||||||
|
|
||||||
|
; Nameservers
|
||||||
|
@ IN NS ns1.pounce.ch.
|
||||||
|
@ IN NS ns2.pounce.ch.
|
||||||
|
|
||||||
|
; A record for the zone apex
|
||||||
|
@ IN A ${SERVER_IP}
|
||||||
|
|
||||||
|
; Wildcard - all subdomains point to our server
|
||||||
|
* IN A ${SERVER_IP}
|
||||||
|
|
||||||
|
; ============================================
|
||||||
|
; YIELD DOMAINS
|
||||||
|
; Add domains below in format:
|
||||||
|
; domainname IN A ${SERVER_IP}
|
||||||
|
; ============================================
|
||||||
|
|
||||||
|
; Example (uncomment to test):
|
||||||
|
; akaya.ch. IN A ${SERVER_IP}
|
||||||
|
|
||||||
|
ZONEFILE
|
||||||
|
|
||||||
|
echo "[6/6] Creating systemd service..."
|
||||||
|
cat > /etc/systemd/system/coredns.service << 'SERVICE'
|
||||||
|
[Unit]
|
||||||
|
Description=CoreDNS DNS Server
|
||||||
|
Documentation=https://coredns.io
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=/opt/coredns
|
||||||
|
ExecStart=/opt/coredns/coredns -conf /opt/coredns/Corefile
|
||||||
|
ExecReload=/bin/kill -SIGUSR1 $MAINPID
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5s
|
||||||
|
LimitNOFILE=1048576
|
||||||
|
LimitNPROC=512
|
||||||
|
|
||||||
|
# Security
|
||||||
|
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
||||||
|
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||||
|
NoNewPrivileges=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
SERVICE
|
||||||
|
|
||||||
|
echo "[7/6] Opening firewall port 53..."
|
||||||
|
if command -v ufw &> /dev/null; then
|
||||||
|
ufw allow 53/tcp
|
||||||
|
ufw allow 53/udp
|
||||||
|
echo "UFW: Port 53 opened"
|
||||||
|
elif command -v firewall-cmd &> /dev/null; then
|
||||||
|
firewall-cmd --permanent --add-port=53/tcp
|
||||||
|
firewall-cmd --permanent --add-port=53/udp
|
||||||
|
firewall-cmd --reload
|
||||||
|
echo "firewalld: Port 53 opened"
|
||||||
|
else
|
||||||
|
echo "WARNING: No firewall detected. Make sure port 53 is open!"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[8/6] Starting CoreDNS..."
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable coredns
|
||||||
|
systemctl start coredns
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "✅ CoreDNS installed and running!"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
echo "Status: $(systemctl is-active coredns)"
|
||||||
|
echo "Config: $COREDNS_DIR/Corefile"
|
||||||
|
echo "Zones: $ZONES_DIR/db.yield"
|
||||||
|
echo ""
|
||||||
|
echo "To add a yield domain, append to $ZONES_DIR/db.yield:"
|
||||||
|
echo " akaya.ch. IN A $SERVER_IP"
|
||||||
|
echo ""
|
||||||
|
echo "Then reload: systemctl reload coredns"
|
||||||
|
echo ""
|
||||||
|
echo "Test with: dig @localhost akaya.ch"
|
||||||
|
echo "=========================================="
|
||||||
98
backend/scripts/setup_yield_nginx.sh
Normal file
98
backend/scripts/setup_yield_nginx.sh
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================================================
|
||||||
|
# Pounce Yield HTTP Routing Setup
|
||||||
|
# ============================================================================
|
||||||
|
# This sets up Nginx to catch-all domains pointing to our server
|
||||||
|
# and route them to the Pounce backend for Yield landing pages.
|
||||||
|
#
|
||||||
|
# Instead of Nameserver delegation (which requires Port 53),
|
||||||
|
# users simply set an A-record pointing to our IP.
|
||||||
|
#
|
||||||
|
# Usage: sudo bash setup_yield_nginx.sh
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Pounce Yield HTTP Routing Setup"
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
|
# Check if running as root
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
echo "ERROR: Please run as root (sudo)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
NGINX_CONF="/etc/nginx/sites-available/yield-catchall"
|
||||||
|
SERVER_IP="46.235.147.194"
|
||||||
|
|
||||||
|
echo "[1/3] Creating Nginx catch-all config for Yield..."
|
||||||
|
|
||||||
|
cat > "$NGINX_CONF" << 'NGINX'
|
||||||
|
# Pounce Yield Catch-All Server
|
||||||
|
# This catches all domains pointing to our server that aren't pounce.ch
|
||||||
|
# and routes them to the Yield routing backend.
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
listen [::]:80 default_server;
|
||||||
|
|
||||||
|
# Catch all hostnames except pounce.ch
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
# Skip if it's pounce.ch or www.pounce.ch
|
||||||
|
if ($host ~* ^(www\.)?pounce\.ch$) {
|
||||||
|
return 444; # Close connection, let the main server block handle it
|
||||||
|
}
|
||||||
|
|
||||||
|
# Route all traffic to backend yield routing
|
||||||
|
location / {
|
||||||
|
# Rewrite to /api/v1/r/{hostname}
|
||||||
|
set $yield_domain $host;
|
||||||
|
|
||||||
|
proxy_pass http://127.0.0.1:8000/api/v1/r/$yield_domain;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Yield-Domain $host;
|
||||||
|
|
||||||
|
# Handle errors gracefully
|
||||||
|
proxy_intercept_errors on;
|
||||||
|
error_page 404 502 503 504 = @yield_fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fallback for domains not configured in Yield
|
||||||
|
location @yield_fallback {
|
||||||
|
return 302 https://pounce.ch/yield?domain=$host;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NGINX
|
||||||
|
|
||||||
|
echo "[2/3] Enabling site and testing config..."
|
||||||
|
|
||||||
|
# Enable the site if not already
|
||||||
|
if [ ! -f "/etc/nginx/sites-enabled/yield-catchall" ]; then
|
||||||
|
ln -sf "$NGINX_CONF" /etc/nginx/sites-enabled/yield-catchall
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test nginx config
|
||||||
|
nginx -t
|
||||||
|
|
||||||
|
echo "[3/3] Reloading Nginx..."
|
||||||
|
systemctl reload nginx
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "✅ Yield HTTP Routing configured!"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
echo "How it works:"
|
||||||
|
echo "1. User sets A-record for their domain to: $SERVER_IP"
|
||||||
|
echo "2. When someone visits the domain, Nginx catches it"
|
||||||
|
echo "3. Traffic is routed to /api/v1/r/{domain}"
|
||||||
|
echo "4. Backend serves the Yield landing page"
|
||||||
|
echo ""
|
||||||
|
echo "No DNS server (Port 53) required!"
|
||||||
|
echo "=========================================="
|
||||||
Reference in New Issue
Block a user