feat(vortex-dns): Add zone management and secondary DNS features

Add comprehensive zone management for DNS master functionality:
- vortexctl zone list/dump/import/export/reload commands
- Secondary DNS provider configuration (OVH support)
- RPCD methods for LuCI integration
- ACL permissions for new methods

This enables importing zones from external providers (Gandi) and
configuring OVH as secondary DNS with SecuBox as authoritative master.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-03-08 08:42:29 +01:00
parent 6e848c5f4a
commit 34dab42a42
3 changed files with 538 additions and 3 deletions

View File

@ -10,7 +10,7 @@ uci_get() { uci -q get "${CONFIG}.$1"; }
case "$1" in
list)
echo '{"status":{},"get_slaves":{},"get_peers":{},"get_published":{},"master_init":{"domain":"str"},"delegate":{"node":"str","zone":"str"},"revoke":{"zone":"str"},"slave_join":{"master":"str","token":"str"},"mesh_sync":{},"mesh_publish":{"service":"str","domain":"str"}}'
echo '{"status":{},"get_slaves":{},"get_peers":{},"get_published":{},"master_init":{"domain":"str"},"delegate":{"node":"str","zone":"str"},"revoke":{"zone":"str"},"slave_join":{"master":"str","token":"str"},"mesh_sync":{},"mesh_publish":{"service":"str","domain":"str"},"zone_list":{},"zone_dump":{"domain":"str"},"zone_import":{"domain":"str"},"zone_export":{"domain":"str"},"zone_reload":{"domain":"str"},"secondary_list":{},"secondary_add":{"provider":"str","domain":"str"},"secondary_remove":{"provider":"str","domain":"str"}}'
;;
call)
case "$2" in
@ -207,6 +207,168 @@ case "$1" in
json_dump
;;
zone_list)
ZONE_DIR="/srv/dns/zones"
json_init
json_add_array "zones"
for section in $(uci show "$CONFIG" 2>/dev/null | grep "=zone" | cut -d= -f1 | cut -d. -f2); do
domain=$(uci_get "${section}.domain")
[ -z "$domain" ] && continue
enabled=$(uci_get "${section}.enabled")
file=$(uci_get "${section}.file")
authoritative=$(uci_get "${section}.authoritative")
records=0
[ -f "$file" ] && records=$(grep -c "^[^;$]" "$file" 2>/dev/null || echo 0)
json_add_object
json_add_string "domain" "$domain"
json_add_string "file" "$file"
json_add_boolean "enabled" "${enabled:-0}"
json_add_boolean "authoritative" "${authoritative:-0}"
json_add_int "records" "$records"
json_close_object
done
json_close_array
json_dump
;;
zone_dump)
read -r input
domain=$(echo "$input" | jsonfilter -e '@.domain')
if [ -z "$domain" ]; then
echo '{"error":"Domain required"}'
exit 1
fi
vortexctl zone dump "$domain" >/dev/null 2>&1
zone_file="/srv/dns/zones/${domain}.zone"
json_init
if [ -f "$zone_file" ]; then
records=$(grep -c "^[^;$]" "$zone_file" 2>/dev/null || echo 0)
json_add_boolean "success" 1
json_add_string "domain" "$domain"
json_add_string "file" "$zone_file"
json_add_int "records" "$records"
else
json_add_boolean "success" 0
json_add_string "error" "Zone dump failed"
fi
json_dump
;;
zone_import)
read -r input
domain=$(echo "$input" | jsonfilter -e '@.domain')
if [ -z "$domain" ]; then
echo '{"error":"Domain required"}'
exit 1
fi
vortexctl zone import "$domain" >/dev/null 2>&1
json_init
json_add_boolean "success" 1
json_add_string "domain" "$domain"
json_add_string "message" "Zone imported and configured"
json_dump
;;
zone_export)
read -r input
domain=$(echo "$input" | jsonfilter -e '@.domain')
if [ -z "$domain" ]; then
echo '{"error":"Domain required"}'
exit 1
fi
zone_file="/srv/dns/zones/${domain}.zone"
if [ -f "$zone_file" ]; then
json_init
json_add_boolean "success" 1
json_add_string "domain" "$domain"
json_add_string "content" "$(cat "$zone_file")"
json_dump
else
echo '{"success":false,"error":"Zone file not found"}'
fi
;;
zone_reload)
read -r input
domain=$(echo "$input" | jsonfilter -e '@.domain')
vortexctl zone reload "$domain" >/dev/null 2>&1
json_init
json_add_boolean "success" 1
json_add_string "message" "Zone reloaded"
json_dump
;;
secondary_list)
json_init
json_add_array "secondaries"
for section in $(uci show "$CONFIG" 2>/dev/null | grep "=secondary" | cut -d= -f1 | cut -d. -f2); do
provider=$(uci_get "${section}.provider")
[ -z "$provider" ] && continue
enabled=$(uci_get "${section}.enabled")
json_add_object
json_add_string "provider" "$provider"
json_add_boolean "enabled" "${enabled:-0}"
json_close_object
done
json_close_array
json_dump
;;
secondary_add)
read -r input
provider=$(echo "$input" | jsonfilter -e '@.provider')
domain=$(echo "$input" | jsonfilter -e '@.domain')
if [ -z "$provider" ] || [ -z "$domain" ]; then
echo '{"error":"Provider and domain required"}'
exit 1
fi
vortexctl secondary add "$provider" "$domain" >/dev/null 2>&1
json_init
json_add_boolean "success" 1
json_add_string "provider" "$provider"
json_add_string "domain" "$domain"
json_dump
;;
secondary_remove)
read -r input
provider=$(echo "$input" | jsonfilter -e '@.provider')
domain=$(echo "$input" | jsonfilter -e '@.domain')
if [ -z "$provider" ] || [ -z "$domain" ]; then
echo '{"error":"Provider and domain required"}'
exit 1
fi
vortexctl secondary remove "$provider" "$domain" >/dev/null 2>&1
json_init
json_add_boolean "success" 1
json_dump
;;
*)
echo '{"error":"Unknown method"}'
;;

View File

@ -4,13 +4,13 @@
"read": {
"uci": ["vortex-dns"],
"ubus": {
"luci.vortex-dns": ["status", "get_slaves", "get_peers", "get_published"]
"luci.vortex-dns": ["status", "get_slaves", "get_peers", "get_published", "zone_list", "zone_export", "secondary_list"]
}
},
"write": {
"uci": ["vortex-dns"],
"ubus": {
"luci.vortex-dns": ["master_init", "delegate", "revoke", "slave_join", "mesh_sync", "mesh_publish"]
"luci.vortex-dns": ["master_init", "delegate", "revoke", "slave_join", "mesh_sync", "mesh_publish", "zone_dump", "zone_import", "zone_reload", "secondary_add", "secondary_remove"]
}
}
}

View File

@ -39,6 +39,18 @@ 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
@ -400,6 +412,349 @@ cmd_mesh_status() {
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 2>/dev/null; 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
# =============================================================================
@ -460,6 +815,24 @@ case "$1" in
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
;;