#!/bin/sh # OpenVPN learn-address hook: registers VPN clients in Technitium DNS. # add/update -> A .$VPN_DNS_ZONE + PTR, TTL 60 # delete -> removes both records # # CRITICAL: this script must ALWAYS exit 0 — a non-zero exit makes # OpenVPN reject the client's address. DNS registration is best-effort; # it must never take the VPN down with it. OP="$1"; ADDR="$2"; CN="${3:-}" API="http://127.0.0.1:5380/api" [ -n "${TECHNITIUM_ADMIN_PASSWORD:-}" ] || exit 0 [ -n "${VPN_DNS_ZONE:-}" ] || exit 0 log() { echo "learn-address: $*"; } TOKEN=$(curl -sf -m 5 -G "$API/user/login" \ --data-urlencode "user=admin" \ --data-urlencode "pass=$TECHNITIUM_ADMIN_PASSWORD" 2>/dev/null \ | jq -r '.token // empty') [ -n "$TOKEN" ] || { log "Technitium API login failed; skipping $OP $ADDR"; exit 0; } AUTH="Authorization: Bearer $TOKEN" # 172.31.8.5 -> 5.8.31.172.in-addr.arpa PTR_HOST=$(printf '%s' "$ADDR" | awk -F. '{print $4"."$3"."$2"."$1".in-addr.arpa"}') deregister() { # PTR tells us which A record this IP belonged to TARGET=$(curl -sf -m 5 -H "$AUTH" -G "$API/zones/records/get" \ --data-urlencode "domain=$PTR_HOST" 2>/dev/null \ | jq -r '.response.records[]? | select(.type=="PTR") | .rData.ptrName' | head -n1) [ -n "$TARGET" ] || return 0 curl -sf -m 5 -H "$AUTH" -G "$API/zones/records/delete" \ --data-urlencode "domain=$PTR_HOST" \ --data-urlencode "type=PTR" \ --data-urlencode "ptrName=$TARGET" >/dev/null 2>&1 || log "PTR delete failed for $ADDR" curl -sf -m 5 -H "$AUTH" -G "$API/zones/records/delete" \ --data-urlencode "domain=$TARGET" \ --data-urlencode "zone=$VPN_DNS_ZONE" \ --data-urlencode "type=A" \ --data-urlencode "ipAddress=$ADDR" >/dev/null 2>&1 || log "A delete failed for $TARGET/$ADDR" } register() { # sanitize CN (= AD username) into a DNS label NAME=$(printf '%s' "$CN" | tr '[:upper:]' '[:lower:]' \ | tr -c 'a-z0-9-' '-' | sed 's/^-*//;s/-*$//' | cut -c1-63) [ -n "$NAME" ] || return 0 curl -sf -m 5 -H "$AUTH" -G "$API/zones/records/add" \ --data-urlencode "domain=$NAME.$VPN_DNS_ZONE" \ --data-urlencode "zone=$VPN_DNS_ZONE" \ --data-urlencode "type=A" \ --data-urlencode "ttl=60" \ --data-urlencode "ipAddress=$ADDR" \ --data-urlencode "ptr=true" \ --data-urlencode "createPtrZone=true" >/dev/null 2>&1 \ || log "A/PTR add failed for $NAME/$ADDR" } case "$OP" in add) register ;; update) deregister; register ;; # IP reassigned to a different client delete) deregister ;; esac exit 0