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(
|
||||
domain=domain.domain,
|
||||
verified=verified,
|
||||
method=check.method,
|
||||
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,
|
||||
error=error,
|
||||
checked_at=datetime.utcnow(),
|
||||
|
||||
@ -242,9 +242,11 @@ class DNSVerificationResult(BaseModel):
|
||||
"""Result of DNS verification check."""
|
||||
domain: str
|
||||
verified: bool
|
||||
method: Optional[str] = None # "a_record" | "cname" | "nameserver"
|
||||
|
||||
expected_ns: list[str]
|
||||
actual_ns: list[str]
|
||||
actual_a: list[str] = [] # A-records found for the domain
|
||||
|
||||
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.
|
||||
|
||||
Production-grade DNS checks for the Yield Connect flow:
|
||||
- Option A (recommended): Nameserver delegation to our nameservers
|
||||
- Option B (simpler): CNAME/ALIAS to a shared target
|
||||
- Option A: A-record pointing directly to our server IP (simplest!)
|
||||
- Option B: CNAME/ALIAS to yield.pounce.ch
|
||||
- Option C: Nameserver delegation to our nameservers (requires DNS server)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@ -14,11 +15,16 @@ from typing import Optional
|
||||
import dns.resolver
|
||||
|
||||
|
||||
# Our server IP - domains with A-record pointing here are verified
|
||||
YIELD_SERVER_IP = "46.235.147.194"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class YieldDNSCheckResult:
|
||||
verified: bool
|
||||
method: Optional[str] # "nameserver" | "cname" | None
|
||||
method: Optional[str] # "a_record" | "cname" | "nameserver" | None
|
||||
actual_ns: list[str]
|
||||
actual_a: list[str]
|
||||
cname_ok: bool
|
||||
error: Optional[str]
|
||||
|
||||
@ -37,7 +43,6 @@ def _normalize_host(host: str) -> str:
|
||||
def _resolve_ns(domain: str) -> list[str]:
|
||||
r = _resolver()
|
||||
answers = r.resolve(domain, "NS")
|
||||
# NS answers are RRset with .target
|
||||
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.
|
||||
|
||||
We accept:
|
||||
- Nameserver delegation (NS contains all expected nameservers), OR
|
||||
- CNAME/ALIAS to `cname_target` (either CNAME matches, or A records match target A records)
|
||||
We accept (in order of simplicity):
|
||||
1. A-record pointing directly to our server IP (46.235.147.194)
|
||||
2. CNAME/ALIAS to `cname_target` (yield.pounce.ch)
|
||||
3. Nameserver delegation (NS contains all expected nameservers)
|
||||
"""
|
||||
domain = _normalize_host(domain)
|
||||
expected_ns = sorted({_normalize_host(ns) for ns in expected_nameservers if ns})
|
||||
target = _normalize_host(cname_target)
|
||||
actual_ns: list[str] = []
|
||||
actual_a: list[str] = []
|
||||
|
||||
if not domain:
|
||||
return YieldDNSCheckResult(
|
||||
verified=False,
|
||||
method=None,
|
||||
actual_ns=[],
|
||||
actual_a=[],
|
||||
cname_ok=False,
|
||||
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:
|
||||
actual_ns = _resolve_ns(domain)
|
||||
if expected_ns and set(expected_ns).issubset(set(actual_ns)):
|
||||
actual_a = _resolve_a(domain)
|
||||
if YIELD_SERVER_IP in actual_a:
|
||||
return YieldDNSCheckResult(
|
||||
verified=True,
|
||||
method="nameserver",
|
||||
actual_ns=actual_ns,
|
||||
method="a_record",
|
||||
actual_ns=[],
|
||||
actual_a=actual_a,
|
||||
cname_ok=False,
|
||||
error=None,
|
||||
)
|
||||
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
|
||||
actual_ns = []
|
||||
pass
|
||||
except Exception as e:
|
||||
return YieldDNSCheckResult(
|
||||
verified=False,
|
||||
method=None,
|
||||
actual_ns=[],
|
||||
actual_a=[],
|
||||
cname_ok=False,
|
||||
error=str(e),
|
||||
)
|
||||
|
||||
# Option B: CNAME / ALIAS
|
||||
if not target:
|
||||
return YieldDNSCheckResult(
|
||||
verified=False,
|
||||
method=None,
|
||||
actual_ns=actual_ns,
|
||||
cname_ok=False,
|
||||
error="Yield CNAME target is not configured on server",
|
||||
)
|
||||
# Option B: CNAME to yield.pounce.ch
|
||||
if target:
|
||||
try:
|
||||
cnames = _resolve_cname(domain)
|
||||
if any(c == target for c in cnames):
|
||||
return YieldDNSCheckResult(
|
||||
verified=True,
|
||||
method="cname",
|
||||
actual_ns=[],
|
||||
actual_a=actual_a,
|
||||
cname_ok=True,
|
||||
error=None,
|
||||
)
|
||||
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Also check if A-records match target's A-records (ALIAS/ANAME flattening)
|
||||
try:
|
||||
target_as = set(_resolve_a(target))
|
||||
if target_as and actual_a and set(actual_a).issubset(target_as):
|
||||
return YieldDNSCheckResult(
|
||||
verified=True,
|
||||
method="cname",
|
||||
actual_ns=[],
|
||||
actual_a=actual_a,
|
||||
cname_ok=True,
|
||||
error=None,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 1) Direct CNAME check (works for subdomain CNAME setups)
|
||||
try:
|
||||
cnames = _resolve_cname(domain)
|
||||
if any(c == target for c in cnames):
|
||||
return YieldDNSCheckResult(
|
||||
verified=True,
|
||||
method="cname",
|
||||
actual_ns=actual_ns,
|
||||
cname_ok=True,
|
||||
error=None,
|
||||
)
|
||||
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
|
||||
pass
|
||||
except Exception as e:
|
||||
return YieldDNSCheckResult(
|
||||
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
|
||||
try:
|
||||
target_as = set(_resolve_a(target))
|
||||
domain_as = set(_resolve_a(domain))
|
||||
if target_as and domain_as and domain_as.issubset(target_as):
|
||||
return YieldDNSCheckResult(
|
||||
verified=True,
|
||||
method="cname",
|
||||
actual_ns=actual_ns,
|
||||
cname_ok=True,
|
||||
error=None,
|
||||
)
|
||||
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
|
||||
pass
|
||||
except Exception as e:
|
||||
return YieldDNSCheckResult(
|
||||
verified=False,
|
||||
method=None,
|
||||
actual_ns=actual_ns,
|
||||
cname_ok=False,
|
||||
error=str(e),
|
||||
)
|
||||
# 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):
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Not verified
|
||||
return YieldDNSCheckResult(
|
||||
verified=False,
|
||||
method=None,
|
||||
actual_ns=actual_ns,
|
||||
actual_a=actual_a,
|
||||
cname_ok=False,
|
||||
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