Remove 2>/dev/null from for-loop glob pattern which causes syntax error in BusyBox ash shell. The [ -f "$zf" ] check handles the case when no zone files exist. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
847 lines
23 KiB
Bash
847 lines
23 KiB
Bash
#!/bin/sh
|
|
# SecuBox Vortex DNS Controller
|
|
# Meshed multi-dynamic subdomain delegation system
|
|
#
|
|
# Copyright (C) 2026 CyberMind.fr
|
|
|
|
CONFIG="vortex-dns"
|
|
LIB_DIR="/usr/lib/vortex-dns"
|
|
STATE_DIR="/var/lib/vortex-dns"
|
|
LOG_TAG="vortex-dns"
|
|
|
|
# Source libraries
|
|
[ -f "$LIB_DIR/master.sh" ] && . "$LIB_DIR/master.sh"
|
|
[ -f "$LIB_DIR/slave.sh" ] && . "$LIB_DIR/slave.sh"
|
|
[ -f "$LIB_DIR/gossip.sh" ] && . "$LIB_DIR/gossip.sh"
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: vortexctl <command> [options]
|
|
|
|
Master Commands:
|
|
master init <wildcard_domain> Initialize as master node
|
|
master delegate <node> <zone> Delegate subzone to slave node
|
|
master revoke <zone> Revoke zone delegation
|
|
master list-slaves List delegated zones
|
|
|
|
Slave Commands:
|
|
slave join <master_ip> <token> Join as slave to master
|
|
slave leave Leave master hierarchy
|
|
slave status Show slave status
|
|
|
|
Mesh Commands:
|
|
mesh status Show mesh DNS status
|
|
mesh sync Force sync with peers
|
|
mesh publish <service> <domain> Publish service to mesh
|
|
mesh unpublish <service> Remove from mesh
|
|
|
|
Submaster Commands:
|
|
submaster promote Promote to submaster role
|
|
submaster demote Demote to regular slave
|
|
|
|
Zone Commands:
|
|
zone list List managed zones
|
|
zone dump <domain> Dump zone from external DNS
|
|
zone import <domain> Import zone and become authoritative
|
|
zone export <domain> Export zone file to stdout
|
|
zone reload [domain] Reload zone(s) in dnsmasq
|
|
|
|
Secondary DNS:
|
|
secondary list List secondary DNS providers
|
|
secondary add <provider> <domain> Configure provider as secondary
|
|
secondary remove <provider> <domain> Remove secondary configuration
|
|
|
|
General:
|
|
status Show overall status
|
|
daemon Run sync daemon
|
|
|
|
Configuration: /etc/config/vortex-dns
|
|
EOF
|
|
}
|
|
|
|
log_info() { logger -t "$LOG_TAG" "$*"; echo "[INFO] $*"; }
|
|
log_warn() { logger -t "$LOG_TAG" -p warning "$*"; echo "[WARN] $*" >&2; }
|
|
log_error() { logger -t "$LOG_TAG" -p err "$*"; echo "[ERROR] $*" >&2; }
|
|
|
|
uci_get() { uci -q get "${CONFIG}.$1"; }
|
|
uci_set() { uci set "${CONFIG}.$1=$2"; }
|
|
|
|
load_config() {
|
|
enabled=$(uci_get main.enabled)
|
|
mode=$(uci_get main.mode)
|
|
sync_interval=$(uci_get main.sync_interval)
|
|
|
|
master_enabled=$(uci_get master.enabled)
|
|
wildcard_domain=$(uci_get master.wildcard_domain)
|
|
dns_provider=$(uci_get master.dns_provider)
|
|
|
|
slave_enabled=$(uci_get slave.enabled)
|
|
parent_master=$(uci_get slave.parent_master)
|
|
delegated_zone=$(uci_get slave.delegated_zone)
|
|
|
|
gossip_enabled=$(uci_get mesh.gossip_enabled)
|
|
first_peek=$(uci_get mesh.first_peek)
|
|
auto_register=$(uci_get mesh.auto_register)
|
|
|
|
[ -z "$sync_interval" ] && sync_interval=300
|
|
|
|
mkdir -p "$STATE_DIR"
|
|
}
|
|
|
|
# =============================================================================
|
|
# STATUS
|
|
# =============================================================================
|
|
|
|
cmd_status() {
|
|
load_config
|
|
|
|
echo "=== Vortex DNS Status ==="
|
|
echo ""
|
|
echo "Enabled: $([ "$enabled" = "1" ] && echo "Yes" || echo "No")"
|
|
echo "Mode: ${mode:-standalone}"
|
|
echo "Sync Interval: ${sync_interval}s"
|
|
echo ""
|
|
|
|
if [ "$master_enabled" = "1" ]; then
|
|
echo "=== Master Node ==="
|
|
echo "Wildcard Domain: ${wildcard_domain:-not set}"
|
|
echo "DNS Provider: ${dns_provider:-not set}"
|
|
|
|
# Count delegated zones
|
|
local zones=$(uci show "$CONFIG" 2>/dev/null | grep -c "=delegation")
|
|
echo "Delegated Zones: $zones"
|
|
echo ""
|
|
fi
|
|
|
|
if [ "$slave_enabled" = "1" ]; then
|
|
echo "=== Slave Node ==="
|
|
echo "Parent Master: ${parent_master:-not set}"
|
|
echo "Delegated Zone: ${delegated_zone:-not set}"
|
|
echo ""
|
|
fi
|
|
|
|
echo "=== Mesh Status ==="
|
|
echo "Gossip: $([ "$gossip_enabled" = "1" ] && echo "Enabled" || echo "Disabled")"
|
|
echo "First Peek: $([ "$first_peek" = "1" ] && echo "Enabled" || echo "Disabled")"
|
|
echo "Auto Register: $([ "$auto_register" = "1" ] && echo "Enabled" || echo "Disabled")"
|
|
|
|
# Count mesh peers
|
|
if command -v secubox-p2p >/dev/null 2>&1; then
|
|
local peers=$(secubox-p2p peers 2>/dev/null | jsonfilter -e '@[*]' 2>/dev/null | wc -l)
|
|
echo "Mesh Peers: $peers"
|
|
fi
|
|
|
|
# Count published services
|
|
if [ -f "$STATE_DIR/published.json" ]; then
|
|
local services=$(jsonfilter -i "$STATE_DIR/published.json" -e '@[*]' 2>/dev/null | wc -l)
|
|
echo "Published Services: $services"
|
|
fi
|
|
}
|
|
|
|
# =============================================================================
|
|
# MASTER COMMANDS
|
|
# =============================================================================
|
|
|
|
cmd_master_init() {
|
|
local domain="$1"
|
|
|
|
[ -z "$domain" ] && {
|
|
echo "Usage: vortexctl master init <wildcard_domain>"
|
|
echo "Example: vortexctl master init secubox.io"
|
|
exit 1
|
|
}
|
|
|
|
load_config
|
|
|
|
log_info "Initializing as master for *.$domain"
|
|
|
|
# Get DNS provider from dns-provider config
|
|
local provider=$(uci -q get dns-provider.main.provider)
|
|
[ -z "$provider" ] && {
|
|
log_error "DNS provider not configured. Run: uci set dns-provider.main.provider=<ovh|gandi|cloudflare>"
|
|
exit 1
|
|
}
|
|
|
|
# Configure master mode
|
|
uci_set master.enabled 1
|
|
uci_set master.wildcard_domain "$domain"
|
|
uci_set master.dns_provider "$provider"
|
|
uci_set main.mode "master"
|
|
uci commit "$CONFIG"
|
|
|
|
# Create wildcard DNS record pointing to this node
|
|
local wan_ip=$(curl -s -4 ifconfig.me 2>/dev/null || wget -q -O - ifconfig.me 2>/dev/null)
|
|
if [ -n "$wan_ip" ]; then
|
|
log_info "Creating wildcard A record: *.$domain -> $wan_ip"
|
|
dnsctl add A "*" "$wan_ip" 300 2>/dev/null || log_warn "Failed to create wildcard record"
|
|
fi
|
|
|
|
# Generate master token for slave enrollment
|
|
local master_token=$(head -c 32 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 32)
|
|
echo "$master_token" > "$STATE_DIR/master_token"
|
|
chmod 600 "$STATE_DIR/master_token"
|
|
|
|
log_info "Master initialized for *.$domain"
|
|
echo ""
|
|
echo "Slave enrollment token: $master_token"
|
|
echo "Slaves can join with: vortexctl slave join $(hostname -I | awk '{print $1}') $master_token"
|
|
}
|
|
|
|
cmd_master_delegate() {
|
|
local node="$1"
|
|
local zone="$2"
|
|
|
|
[ -z "$node" ] || [ -z "$zone" ] && {
|
|
echo "Usage: vortexctl master delegate <node_ip> <subzone>"
|
|
echo "Example: vortexctl master delegate 192.168.1.100 node1"
|
|
exit 1
|
|
}
|
|
|
|
load_config
|
|
|
|
[ "$master_enabled" != "1" ] && {
|
|
log_error "Not configured as master. Run: vortexctl master init <domain>"
|
|
exit 1
|
|
}
|
|
|
|
local fqdn="${zone}.${wildcard_domain}"
|
|
log_info "Delegating $fqdn to $node"
|
|
|
|
# Create NS record for delegation
|
|
dnsctl add NS "$zone" "ns1.${fqdn}" 2>/dev/null
|
|
dnsctl add A "ns1.${zone}" "$node" 2>/dev/null
|
|
|
|
# Store delegation in UCI
|
|
local section="delegation_$(echo "$zone" | tr '.-' '__')"
|
|
uci set "${CONFIG}.${section}=delegation"
|
|
uci set "${CONFIG}.${section}.zone=$zone"
|
|
uci set "${CONFIG}.${section}.node=$node"
|
|
uci set "${CONFIG}.${section}.fqdn=$fqdn"
|
|
uci set "${CONFIG}.${section}.created=$(date -Iseconds)"
|
|
uci commit "$CONFIG"
|
|
|
|
# Notify slave via mesh
|
|
if [ "$gossip_enabled" = "1" ]; then
|
|
secubox-p2p publish "dns-delegation" "0" "zone=$fqdn,node=$node" 2>/dev/null
|
|
fi
|
|
|
|
log_info "Delegated $fqdn to $node"
|
|
}
|
|
|
|
cmd_master_list_slaves() {
|
|
load_config
|
|
|
|
echo "=== Delegated Zones ==="
|
|
echo ""
|
|
|
|
uci show "$CONFIG" 2>/dev/null | grep "=delegation" | while read -r line; do
|
|
local section=$(echo "$line" | cut -d= -f1 | cut -d. -f2)
|
|
local zone=$(uci_get "${section}.zone")
|
|
local node=$(uci_get "${section}.node")
|
|
local fqdn=$(uci_get "${section}.fqdn")
|
|
local created=$(uci_get "${section}.created")
|
|
|
|
echo "Zone: $zone"
|
|
echo " FQDN: $fqdn"
|
|
echo " Node: $node"
|
|
echo " Created: $created"
|
|
echo ""
|
|
done
|
|
}
|
|
|
|
# =============================================================================
|
|
# SLAVE COMMANDS
|
|
# =============================================================================
|
|
|
|
cmd_slave_join() {
|
|
local master_ip="$1"
|
|
local token="$2"
|
|
|
|
[ -z "$master_ip" ] || [ -z "$token" ] && {
|
|
echo "Usage: vortexctl slave join <master_ip> <enrollment_token>"
|
|
exit 1
|
|
}
|
|
|
|
load_config
|
|
|
|
log_info "Joining master at $master_ip..."
|
|
|
|
# Verify token with master (via mesh API)
|
|
local verify=$(wget -q -O - --timeout=10 "http://${master_ip}:7331/api/vortex/verify?token=$token" 2>/dev/null)
|
|
|
|
if [ -z "$verify" ]; then
|
|
log_warn "Could not verify with master, proceeding anyway..."
|
|
fi
|
|
|
|
# Configure slave mode
|
|
uci_set slave.enabled 1
|
|
uci_set slave.parent_master "$master_ip"
|
|
uci_set slave.sync_key "$token"
|
|
uci_set main.mode "slave"
|
|
|
|
# Request zone assignment from master
|
|
local hostname=$(uci -q get system.@system[0].hostname || hostname)
|
|
local my_ip=$(hostname -I | awk '{print $1}')
|
|
|
|
# Ask master to delegate a zone for us
|
|
local zone_response=$(wget -q -O - --timeout=10 \
|
|
"http://${master_ip}:7331/api/vortex/request-zone?hostname=$hostname&ip=$my_ip&token=$token" 2>/dev/null)
|
|
|
|
if [ -n "$zone_response" ]; then
|
|
local assigned_zone=$(echo "$zone_response" | jsonfilter -e '@.zone' 2>/dev/null)
|
|
if [ -n "$assigned_zone" ]; then
|
|
uci_set slave.delegated_zone "$assigned_zone"
|
|
log_info "Assigned zone: $assigned_zone"
|
|
fi
|
|
fi
|
|
|
|
uci commit "$CONFIG"
|
|
|
|
log_info "Joined master at $master_ip"
|
|
cmd_status
|
|
}
|
|
|
|
cmd_slave_status() {
|
|
load_config
|
|
|
|
[ "$slave_enabled" != "1" ] && {
|
|
echo "Not configured as slave"
|
|
exit 0
|
|
}
|
|
|
|
echo "=== Slave Status ==="
|
|
echo "Parent Master: $parent_master"
|
|
echo "Delegated Zone: ${delegated_zone:-pending}"
|
|
|
|
# Check connectivity to master
|
|
if wget -q -O /dev/null --timeout=3 "http://${parent_master}:7331/api/status" 2>/dev/null; then
|
|
echo "Master Connectivity: OK"
|
|
else
|
|
echo "Master Connectivity: FAILED"
|
|
fi
|
|
}
|
|
|
|
# =============================================================================
|
|
# MESH COMMANDS
|
|
# =============================================================================
|
|
|
|
cmd_mesh_sync() {
|
|
load_config
|
|
|
|
log_info "Syncing with mesh peers..."
|
|
|
|
# Get all exposure entries from peers
|
|
if command -v secubox-p2p >/dev/null 2>&1; then
|
|
secubox-p2p sync 2>/dev/null
|
|
fi
|
|
|
|
# Sync DNS records based on mesh catalog
|
|
if [ -f "/tmp/secubox-p2p-services.json" ]; then
|
|
jsonfilter -i /tmp/secubox-p2p-services.json -e '@[*]' 2>/dev/null | while read -r svc; do
|
|
local name=$(echo "$svc" | jsonfilter -e '@.name' 2>/dev/null)
|
|
local domain=$(echo "$svc" | jsonfilter -e '@.domain' 2>/dev/null)
|
|
local ip=$(echo "$svc" | jsonfilter -e '@.ip' 2>/dev/null)
|
|
|
|
if [ -n "$domain" ] && [ -n "$ip" ]; then
|
|
log_info "Mesh sync: $domain -> $ip"
|
|
# Update local DNS cache/records as needed
|
|
fi
|
|
done
|
|
fi
|
|
|
|
date -Iseconds > "$STATE_DIR/last_sync"
|
|
log_info "Mesh sync complete"
|
|
}
|
|
|
|
cmd_mesh_publish() {
|
|
local service="$1"
|
|
local domain="$2"
|
|
|
|
[ -z "$service" ] || [ -z "$domain" ] && {
|
|
echo "Usage: vortexctl mesh publish <service> <domain>"
|
|
exit 1
|
|
}
|
|
|
|
load_config
|
|
|
|
log_info "Publishing $service as $domain to mesh..."
|
|
|
|
# Use secubox-p2p to publish
|
|
if command -v secubox-p2p >/dev/null 2>&1; then
|
|
secubox-p2p publish "$service" "0" "domain=$domain" 2>/dev/null
|
|
fi
|
|
|
|
# Store locally
|
|
mkdir -p "$STATE_DIR"
|
|
local entry="{\"service\":\"$service\",\"domain\":\"$domain\",\"published\":\"$(date -Iseconds)\"}"
|
|
|
|
if [ -f "$STATE_DIR/published.json" ]; then
|
|
# Append to existing
|
|
sed -i "s/\]$/,$entry]/" "$STATE_DIR/published.json"
|
|
else
|
|
echo "[$entry]" > "$STATE_DIR/published.json"
|
|
fi
|
|
|
|
log_info "Published $service as $domain"
|
|
}
|
|
|
|
cmd_mesh_status() {
|
|
load_config
|
|
|
|
echo "=== Mesh DNS Status ==="
|
|
echo ""
|
|
echo "Gossip: $([ "$gossip_enabled" = "1" ] && echo "Enabled" || echo "Disabled")"
|
|
echo "First Peek: $([ "$first_peek" = "1" ] && echo "Enabled" || echo "Disabled")"
|
|
echo "Auto Register: $([ "$auto_register" = "1" ] && echo "Enabled" || echo "Disabled")"
|
|
echo ""
|
|
|
|
if [ -f "$STATE_DIR/last_sync" ]; then
|
|
echo "Last Sync: $(cat "$STATE_DIR/last_sync")"
|
|
fi
|
|
|
|
echo ""
|
|
echo "=== Published Services ==="
|
|
if [ -f "$STATE_DIR/published.json" ]; then
|
|
jsonfilter -i "$STATE_DIR/published.json" -e '@[*]' 2>/dev/null | while read -r pub; do
|
|
local svc=$(echo "$pub" | jsonfilter -e '@.service' 2>/dev/null)
|
|
local dom=$(echo "$pub" | jsonfilter -e '@.domain' 2>/dev/null)
|
|
echo " $svc -> $dom"
|
|
done
|
|
else
|
|
echo " (none)"
|
|
fi
|
|
}
|
|
|
|
# =============================================================================
|
|
# ZONE COMMANDS
|
|
# =============================================================================
|
|
|
|
ZONE_DIR="/srv/dns/zones"
|
|
DNSMASQ_VORTEX_DIR="/etc/dnsmasq.d/vortex"
|
|
|
|
cmd_zone_list() {
|
|
echo "=== Managed Zones ==="
|
|
echo ""
|
|
|
|
mkdir -p "$ZONE_DIR"
|
|
|
|
# List zones from UCI
|
|
uci show "$CONFIG" 2>/dev/null | grep "=zone" | while read -r line; do
|
|
local section=$(echo "$line" | cut -d= -f1 | cut -d. -f2)
|
|
local domain=$(uci_get "${section}.domain")
|
|
local enabled=$(uci_get "${section}.enabled")
|
|
local file=$(uci_get "${section}.file")
|
|
|
|
[ -z "$domain" ] && continue
|
|
|
|
local status="disabled"
|
|
[ "$enabled" = "1" ] && status="enabled"
|
|
|
|
local records=0
|
|
[ -f "$file" ] && records=$(grep -c "^[^;$]" "$file" 2>/dev/null || echo 0)
|
|
|
|
printf "%-25s %3d records [%s]\n" "$domain" "$records" "$status"
|
|
done
|
|
|
|
# Also list zone files not in UCI
|
|
for zf in "$ZONE_DIR"/*.zone; do
|
|
[ -f "$zf" ] || continue
|
|
local domain=$(basename "$zf" .zone)
|
|
if ! uci show "$CONFIG" 2>/dev/null | grep -q "domain='$domain'"; then
|
|
local records=$(grep -c "^[^;$]" "$zf" 2>/dev/null || echo 0)
|
|
printf "%-25s %3d records [not configured]\n" "$domain" "$records"
|
|
fi
|
|
done
|
|
}
|
|
|
|
cmd_zone_dump() {
|
|
local domain="$1"
|
|
|
|
[ -z "$domain" ] && {
|
|
echo "Usage: vortexctl zone dump <domain>"
|
|
echo "Example: vortexctl zone dump maegia.tv"
|
|
exit 1
|
|
}
|
|
|
|
mkdir -p "$ZONE_DIR"
|
|
local output="$ZONE_DIR/${domain}.zone"
|
|
local serial=$(date +%Y%m%d)01
|
|
|
|
log_info "Dumping zone $domain..."
|
|
|
|
# Get SOA info
|
|
local soa=$(dig +short "$domain" SOA 2>/dev/null | head -1)
|
|
local primary_ns=$(echo "$soa" | awk '{print $1}')
|
|
local hostmaster=$(echo "$soa" | awk '{print $2}')
|
|
[ -z "$hostmaster" ] && hostmaster="hostmaster.${domain}."
|
|
[ -z "$primary_ns" ] && primary_ns="ns1.${domain}."
|
|
|
|
# Start zone file
|
|
cat > "$output" << EOF
|
|
\$ORIGIN ${domain}.
|
|
\$TTL 3600
|
|
|
|
; Zone file for ${domain}
|
|
; Generated by vortexctl on $(date -Iseconds)
|
|
; Source: External DNS query
|
|
|
|
@ IN SOA ${primary_ns} ${hostmaster} (
|
|
${serial} ; serial
|
|
10800 ; refresh (3 hours)
|
|
3600 ; retry (1 hour)
|
|
604800 ; expire (1 week)
|
|
10800 ) ; minimum (3 hours)
|
|
|
|
; NS records
|
|
EOF
|
|
|
|
# Get NS records
|
|
dig +short "$domain" NS 2>/dev/null | while read -r ns; do
|
|
printf "@ IN NS %s\n" "$ns" >> "$output"
|
|
done
|
|
|
|
echo "" >> "$output"
|
|
echo "; MX records" >> "$output"
|
|
|
|
# Get MX records
|
|
dig +short "$domain" MX 2>/dev/null | while read -r mx; do
|
|
local prio=$(echo "$mx" | awk '{print $1}')
|
|
local server=$(echo "$mx" | awk '{print $2}')
|
|
printf "@ IN MX %s %s\n" "$prio" "$server" >> "$output"
|
|
done
|
|
|
|
echo "" >> "$output"
|
|
echo "; TXT records" >> "$output"
|
|
|
|
# Get TXT records
|
|
dig +short "$domain" TXT 2>/dev/null | while read -r txt; do
|
|
printf "@ IN TXT %s\n" "$txt" >> "$output"
|
|
done
|
|
|
|
echo "" >> "$output"
|
|
echo "; A records" >> "$output"
|
|
|
|
# Get root A record
|
|
local root_ip=$(dig +short "$domain" A 2>/dev/null | head -1)
|
|
[ -n "$root_ip" ] && printf "@ IN A %s\n" "$root_ip" >> "$output"
|
|
|
|
# Get subdomains from HAProxy vhosts
|
|
local subs=$(uci show haproxy 2>/dev/null | grep -oE "[a-z0-9_-]+\\.${domain}" | sed "s/\\.${domain}//" | sort -u)
|
|
|
|
echo "" >> "$output"
|
|
echo "; Subdomains (from HAProxy vhosts)" >> "$output"
|
|
|
|
for sub in $subs; do
|
|
[ "$sub" = "@" ] && continue
|
|
local ip=$(dig +short "${sub}.${domain}" A 2>/dev/null | grep -E "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$" | head -1)
|
|
if [ -n "$ip" ]; then
|
|
printf "%-20s IN A %s\n" "$sub" "$ip" >> "$output"
|
|
fi
|
|
done
|
|
|
|
local count=$(grep -c "^[^;$]" "$output" 2>/dev/null || echo 0)
|
|
log_info "Zone dumped to $output ($count records)"
|
|
echo ""
|
|
echo "Zone file: $output"
|
|
}
|
|
|
|
cmd_zone_import() {
|
|
local domain="$1"
|
|
|
|
[ -z "$domain" ] && {
|
|
echo "Usage: vortexctl zone import <domain>"
|
|
exit 1
|
|
}
|
|
|
|
# First dump the zone
|
|
cmd_zone_dump "$domain"
|
|
|
|
local zone_file="$ZONE_DIR/${domain}.zone"
|
|
[ ! -f "$zone_file" ] && {
|
|
log_error "Zone file not created"
|
|
exit 1
|
|
}
|
|
|
|
log_info "Configuring $domain as authoritative zone..."
|
|
|
|
# Create UCI config
|
|
local section="zone_$(echo "$domain" | tr '.-' '__')"
|
|
uci set "${CONFIG}.${section}=zone"
|
|
uci set "${CONFIG}.${section}.domain=$domain"
|
|
uci set "${CONFIG}.${section}.file=$zone_file"
|
|
uci set "${CONFIG}.${section}.enabled=1"
|
|
uci set "${CONFIG}.${section}.authoritative=1"
|
|
uci commit "$CONFIG"
|
|
|
|
# Create dnsmasq config
|
|
mkdir -p "$DNSMASQ_VORTEX_DIR"
|
|
|
|
local wan_if=$(uci -q get network.wan.device || echo "eth0")
|
|
|
|
cat > "${DNSMASQ_VORTEX_DIR}/${domain}.conf" << EOF
|
|
# Authoritative zone: ${domain}
|
|
# Generated by vortexctl
|
|
|
|
auth-zone=${domain}
|
|
auth-server=ns1.${domain},${wan_if}
|
|
EOF
|
|
|
|
# Reload dnsmasq
|
|
/etc/init.d/dnsmasq reload 2>/dev/null
|
|
|
|
log_info "Zone $domain imported and configured"
|
|
echo ""
|
|
echo "Zone file: $zone_file"
|
|
echo "Dnsmasq config: ${DNSMASQ_VORTEX_DIR}/${domain}.conf"
|
|
echo ""
|
|
echo "To test: dig @127.0.0.1 ${domain}"
|
|
}
|
|
|
|
cmd_zone_export() {
|
|
local domain="$1"
|
|
|
|
[ -z "$domain" ] && {
|
|
echo "Usage: vortexctl zone export <domain>"
|
|
exit 1
|
|
}
|
|
|
|
local zone_file="$ZONE_DIR/${domain}.zone"
|
|
[ ! -f "$zone_file" ] && {
|
|
log_error "Zone file not found: $zone_file"
|
|
echo "Run 'vortexctl zone dump $domain' first"
|
|
exit 1
|
|
}
|
|
|
|
cat "$zone_file"
|
|
}
|
|
|
|
cmd_zone_reload() {
|
|
local domain="$1"
|
|
|
|
if [ -n "$domain" ]; then
|
|
log_info "Reloading zone $domain..."
|
|
# Bump serial
|
|
local zone_file="$ZONE_DIR/${domain}.zone"
|
|
if [ -f "$zone_file" ]; then
|
|
local new_serial=$(date +%Y%m%d%H)
|
|
sed -i "s/[0-9]\{10\} ; serial/${new_serial} ; serial/" "$zone_file"
|
|
log_info "Updated serial to $new_serial"
|
|
fi
|
|
else
|
|
log_info "Reloading all zones..."
|
|
fi
|
|
|
|
/etc/init.d/dnsmasq reload 2>/dev/null
|
|
log_info "Dnsmasq reloaded"
|
|
}
|
|
|
|
# =============================================================================
|
|
# SECONDARY DNS COMMANDS
|
|
# =============================================================================
|
|
|
|
cmd_secondary_list() {
|
|
echo "=== Secondary DNS Providers ==="
|
|
echo ""
|
|
|
|
uci show "$CONFIG" 2>/dev/null | grep "=secondary" | while read -r line; do
|
|
local section=$(echo "$line" | cut -d= -f1 | cut -d. -f2)
|
|
local provider=$(uci_get "${section}.provider")
|
|
local enabled=$(uci_get "${section}.enabled")
|
|
|
|
local status="disabled"
|
|
[ "$enabled" = "1" ] && status="enabled"
|
|
|
|
printf "%-15s [%s]\n" "$provider" "$status"
|
|
|
|
# List zones for this provider
|
|
uci show "$CONFIG" 2>/dev/null | grep "=zone" | while read -r zline; do
|
|
local zsection=$(echo "$zline" | cut -d= -f1 | cut -d. -f2)
|
|
local domain=$(uci_get "${zsection}.domain")
|
|
local secondaries=$(uci_get "${zsection}.secondary")
|
|
|
|
if echo "$secondaries" | grep -q "$provider"; then
|
|
echo " - $domain"
|
|
fi
|
|
done
|
|
done
|
|
}
|
|
|
|
cmd_secondary_add() {
|
|
local provider="$1"
|
|
local domain="$2"
|
|
|
|
[ -z "$provider" ] || [ -z "$domain" ] && {
|
|
echo "Usage: vortexctl secondary add <provider> <domain>"
|
|
echo "Providers: ovh, gandi"
|
|
exit 1
|
|
}
|
|
|
|
log_info "Configuring $provider as secondary for $domain..."
|
|
|
|
# Check if zone exists
|
|
local zone_section="zone_$(echo "$domain" | tr '.-' '__')"
|
|
local zone_domain=$(uci_get "${zone_section}.domain")
|
|
|
|
[ -z "$zone_domain" ] && {
|
|
log_error "Zone $domain not configured. Run 'vortexctl zone import $domain' first"
|
|
exit 1
|
|
}
|
|
|
|
# Add secondary to zone
|
|
uci add_list "${CONFIG}.${zone_section}.secondary=$provider"
|
|
uci commit "$CONFIG"
|
|
|
|
# Configure secondary provider section if not exists
|
|
local sec_section="secondary_${provider}"
|
|
local sec_exists=$(uci_get "${sec_section}.provider")
|
|
if [ -z "$sec_exists" ]; then
|
|
uci set "${CONFIG}.${sec_section}=secondary"
|
|
uci set "${CONFIG}.${sec_section}.provider=$provider"
|
|
uci set "${CONFIG}.${sec_section}.enabled=1"
|
|
uci commit "$CONFIG"
|
|
fi
|
|
|
|
# Provider-specific setup
|
|
case "$provider" in
|
|
ovh)
|
|
log_info "OVH secondary DNS setup:"
|
|
echo ""
|
|
echo "1. Add OVH DNS servers to your zone's NS records"
|
|
echo "2. Configure OVH as secondary via their API or web panel"
|
|
echo "3. OVH will poll this server for zone transfers"
|
|
echo ""
|
|
echo "OVH secondary DNS servers:"
|
|
echo " dns100.ovh.net"
|
|
echo " dns101.ovh.net"
|
|
echo " dns102.ovh.net"
|
|
echo " dns103.ovh.net"
|
|
|
|
# Add auth-sec-servers to dnsmasq config
|
|
local dnsmasq_conf="${DNSMASQ_VORTEX_DIR}/${domain}.conf"
|
|
if [ -f "$dnsmasq_conf" ]; then
|
|
if ! grep -q "auth-sec-servers" "$dnsmasq_conf"; then
|
|
echo "auth-sec-servers=dns100.ovh.net,dns101.ovh.net,dns102.ovh.net,dns103.ovh.net" >> "$dnsmasq_conf"
|
|
/etc/init.d/dnsmasq reload 2>/dev/null
|
|
fi
|
|
fi
|
|
;;
|
|
gandi)
|
|
log_info "Gandi secondary DNS setup:"
|
|
echo ""
|
|
echo "Gandi uses their LiveDNS as secondary."
|
|
echo "Configure via Gandi API or web panel."
|
|
;;
|
|
*)
|
|
log_warn "Unknown provider: $provider"
|
|
;;
|
|
esac
|
|
|
|
log_info "Secondary $provider configured for $domain"
|
|
}
|
|
|
|
cmd_secondary_remove() {
|
|
local provider="$1"
|
|
local domain="$2"
|
|
|
|
[ -z "$provider" ] || [ -z "$domain" ] && {
|
|
echo "Usage: vortexctl secondary remove <provider> <domain>"
|
|
exit 1
|
|
}
|
|
|
|
local zone_section="zone_$(echo "$domain" | tr '.-' '__')"
|
|
uci del_list "${CONFIG}.${zone_section}.secondary=$provider" 2>/dev/null
|
|
uci commit "$CONFIG"
|
|
|
|
log_info "Removed $provider as secondary for $domain"
|
|
}
|
|
|
|
# =============================================================================
|
|
# DAEMON
|
|
# =============================================================================
|
|
|
|
cmd_daemon() {
|
|
load_config
|
|
|
|
[ "$enabled" = "1" ] || {
|
|
log_error "Vortex DNS is disabled"
|
|
exit 1
|
|
}
|
|
|
|
log_info "Starting Vortex DNS daemon (interval: ${sync_interval}s)..."
|
|
|
|
while true; do
|
|
cmd_mesh_sync 2>&1 | while read -r line; do
|
|
logger -t "$LOG_TAG" "$line"
|
|
done
|
|
|
|
sleep "$sync_interval"
|
|
done
|
|
}
|
|
|
|
# =============================================================================
|
|
# MAIN
|
|
# =============================================================================
|
|
|
|
case "$1" in
|
|
status)
|
|
cmd_status
|
|
;;
|
|
master)
|
|
case "$2" in
|
|
init) cmd_master_init "$3" ;;
|
|
delegate) cmd_master_delegate "$3" "$4" ;;
|
|
revoke) log_info "Not implemented yet" ;;
|
|
list-slaves) cmd_master_list_slaves ;;
|
|
*) echo "Unknown master command: $2"; usage; exit 1 ;;
|
|
esac
|
|
;;
|
|
slave)
|
|
case "$2" in
|
|
join) cmd_slave_join "$3" "$4" ;;
|
|
leave) log_info "Not implemented yet" ;;
|
|
status) cmd_slave_status ;;
|
|
*) echo "Unknown slave command: $2"; usage; exit 1 ;;
|
|
esac
|
|
;;
|
|
mesh)
|
|
case "$2" in
|
|
status) cmd_mesh_status ;;
|
|
sync) cmd_mesh_sync ;;
|
|
publish) cmd_mesh_publish "$3" "$4" ;;
|
|
unpublish) log_info "Not implemented yet" ;;
|
|
*) echo "Unknown mesh command: $2"; usage; exit 1 ;;
|
|
esac
|
|
;;
|
|
submaster)
|
|
log_info "Submaster commands not implemented yet"
|
|
;;
|
|
zone)
|
|
case "$2" in
|
|
list) cmd_zone_list ;;
|
|
dump) cmd_zone_dump "$3" ;;
|
|
import) cmd_zone_import "$3" ;;
|
|
export) cmd_zone_export "$3" ;;
|
|
reload) cmd_zone_reload "$3" ;;
|
|
*) echo "Unknown zone command: $2"; usage; exit 1 ;;
|
|
esac
|
|
;;
|
|
secondary)
|
|
case "$2" in
|
|
list) cmd_secondary_list ;;
|
|
add) cmd_secondary_add "$3" "$4" ;;
|
|
remove) cmd_secondary_remove "$3" "$4" ;;
|
|
*) echo "Unknown secondary command: $2"; usage; exit 1 ;;
|
|
esac
|
|
;;
|
|
daemon)
|
|
cmd_daemon
|
|
;;
|
|
-h|--help|help)
|
|
usage
|
|
;;
|
|
*)
|
|
usage
|
|
exit 1
|
|
;;
|
|
esac
|