diff --git a/package/secubox/luci-app-vortex-dns/root/usr/libexec/rpcd/luci.vortex-dns b/package/secubox/luci-app-vortex-dns/root/usr/libexec/rpcd/luci.vortex-dns index 20470e2e..23f68135 100644 --- a/package/secubox/luci-app-vortex-dns/root/usr/libexec/rpcd/luci.vortex-dns +++ b/package/secubox/luci-app-vortex-dns/root/usr/libexec/rpcd/luci.vortex-dns @@ -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"}' ;; diff --git a/package/secubox/luci-app-vortex-dns/root/usr/share/rpcd/acl.d/luci-app-vortex-dns.json b/package/secubox/luci-app-vortex-dns/root/usr/share/rpcd/acl.d/luci-app-vortex-dns.json index 669470d9..9d4fe92c 100644 --- a/package/secubox/luci-app-vortex-dns/root/usr/share/rpcd/acl.d/luci-app-vortex-dns.json +++ b/package/secubox/luci-app-vortex-dns/root/usr/share/rpcd/acl.d/luci-app-vortex-dns.json @@ -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"] } } } diff --git a/package/secubox/secubox-vortex-dns/files/usr/sbin/vortexctl b/package/secubox/secubox-vortex-dns/files/usr/sbin/vortexctl index d5387a36..9ca87dab 100644 --- a/package/secubox/secubox-vortex-dns/files/usr/sbin/vortexctl +++ b/package/secubox/secubox-vortex-dns/files/usr/sbin/vortexctl @@ -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 Dump zone from external DNS + zone import Import zone and become authoritative + zone export Export zone file to stdout + zone reload [domain] Reload zone(s) in dnsmasq + +Secondary DNS: + secondary list List secondary DNS providers + secondary add Configure provider as secondary + secondary remove 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 " + 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 " + 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 " + 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 " + 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 " + 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 ;;