#!/usr/bin/env bash # dns-cli - CLI wrapper for Technitium DNS Server API # # Usage: # dns-cli zones List all zones # dns-cli list [zone] List records in a zone (default: $TECHNITIUM_DNS_ZONE) # dns-cli get Resolve a record (A/AAAA) # dns-cli add [ttl] Add A record (+ auto PTR) # dns-cli delete Delete A record # dns-cli flush Flush DNS cache # dns-cli search Search records by name pattern # # Env (provided via --env-file ~/.creds/technitium.env): # TECHNITIUM_URL Technitium base URL (required) # TECHNITIUM_DNS_TOKEN API token (required) # TECHNITIUM_DNS_ZONE Default zone (required) set -euo pipefail URL="${TECHNITIUM_URL:?TECHNITIUM_URL is required}" TOKEN="${TECHNITIUM_DNS_TOKEN:?TECHNITIUM_DNS_TOKEN is required}" ZONE="${TECHNITIUM_DNS_ZONE:?TECHNITIUM_DNS_ZONE is required}" # API helper — adds Bearer auth, returns JSON api() { local method="$1" endpoint="$2"; shift 2 local args=() for kv in "$@"; do args+=(-d "$kv") done curl -s -H "Authorization: Bearer $TOKEN" -X "$method" "${args[@]}" \ "$URL/api/$endpoint" 2>/dev/null } # Subcommands cmd_zones() { api GET "zones/list" | python3 -c " import json,sys d=json.load(sys.stdin) for z in d.get('response',{}).get('zones',[]): print(z['name']) " } cmd_list() { local zone="${1:-$ZONE}" api GET "zones/records/get?domain=$zone&zone=$zone&listZone=true" | python3 -c " import json,sys d=json.load(sys.stdin) recs = d.get('response',{}).get('records',[]) for r in sorted(recs, key=lambda x: (x['name'], x['type'])): rd = r.get('rData',{}) ip = rd.get('ipAddress','') val = ip or rd.get('nameServer','') or rd.get('primaryNameServer','') or str(rd) print(f\"{r['name']:50s} {r['type']:6s} {r.get('ttl',''):6} {val}\") " } cmd_get() { local name="$1" dig +short "$name" 2>/dev/null || true } cmd_add() { local name="$1" ip="$2" ttl="${3:-3600}" local fqdn="$name" [[ "$fqdn" != *.* ]] && fqdn="$name.$ZONE" api POST "zones/records/add" \ "zone=$ZONE" "domain=$fqdn" "type=A" "ttl=$ttl" "ipAddress=$ip" "ptr=true" "overwrite=true" | python3 -c " import json,sys d=json.load(sys.stdin) print(d.get('status','?'), d.get('errorMessage','')) " } cmd_delete() { local name="$1" ip="$2" local fqdn="$name" [[ "$fqdn" != *.* ]] && fqdn="$name.$ZONE" api POST "zones/records/delete" \ "zone=$ZONE" "domain=$fqdn" "type=A" "ipAddress=$ip" | python3 -c " import json,sys d=json.load(sys.stdin) print(d.get('status','?'), d.get('errorMessage','')) " } cmd_flush() { api POST "cache/flush" | python3 -c " import json,sys d=json.load(sys.stdin) print(d.get('status','?')) " } cmd_search() { local pattern="$1" api GET "zones/records/get?domain=$ZONE&zone=$ZONE&listZone=true" | python3 -c " import json,sys d=json.load(sys.stdin) recs = d.get('response',{}).get('records',[]) for r in sorted(recs, key=lambda x: x['name']): if '$pattern' in r['name'].lower(): rd = r.get('rData',{}) ip = rd.get('ipAddress','') val = ip or rd.get('nameServer','') or str(rd) print(f\"{r['name']:50s} {r['type']:6s} {val}\") " } # Main case "${1:-}" in zones) cmd_zones ;; list) shift; cmd_list "${1:-}" ;; get) shift; cmd_get "$1" ;; add) shift; cmd_add "$@" ;; delete) shift; cmd_delete "$@" ;; flush) cmd_flush ;; search) shift; cmd_search "$1" ;; ""|-h|--help|help) sed -n '2,18p' "$0" >&2 exit 0 ;; *) echo "Unknown command: $1" >&2; exit 1 ;; esac