7월쯤에 가비아 dns 서버에서 클플로 이사를 하면서 duckdns는 더이상 ddns 역할보다 그냥 외부ip 구하는 서비스로 사용하고 있었습니다.
근데 최근에 로그를 확인하니 duckdns가 맛이 간건지 상당히 많이 에러 메세지가 남아있었네요. 그래서 클플ddns 기능 스크립트 정리하다가 클플 관련 api id 구하는 함수 코드 날려먹어서 다시 정리 하는 김에 포스팅 하기로 했습니다.
1. 준비물
클라우드플레어 계정이 필요합니다. 그리고 개인 도메인도요. 개인 도메인 없이 ddns 세팅하는 방법은 저는 모르니 뒤로가기 눌러주세요.
2. 사용자 API 토큰 생성하기

https://dash.cloudflare.com/profile/api-tokens
위 링크에 접속해서 우측에 토큰 생성을 누릅니다.

그럼 사용자 API 토큰 화면에서 영역DNS 편집의 템플릿 사용을 클릭합니다.

토큰이름은 원하는대로 수정해주고 권한에 대해 위처럼 두개를 세팅합니다.
첫번째 영역-DNS-편집 권한은 api명령을 사용해서 도메인 ip를 수정할 권한인거고
두번째 영역-영역-읽기 권한은 api 사용시 어느 영역에 항목을 수정할건지 찾아야 하는데 그 id값을 찾기 위해서 필요한 권한입니다. (제가 이거를 몰라서 한참 헤맸네요)
영역리소스는 모든 영역 다 할 수 있게해도 되지만 그냥 저는 제 도메인만 해당하도록 세팅했습니다. 그리고 계속을 눌러줍니다.

요약이 나오고 토큰 생성을 눌러줍니다.

이제 토큰이 생성되었습니다. 토큰값은 한번만 보여주고 다시는 볼 수 없기 때문에 잘 복사해 둡니다.
3. API 작업
이제 해야 하는것은 API호출에 필요한 값을 구해야 합니다. zone id와 dns record id 두개 값이 있어야 ip를 갱신할 수 있습니다.
3-1 zone id 구하기
curl https://api.cloudflare.com/client/v4/zones --header 'Authorization: Bearer 토큰값'
위에서 구한 토큰값을 넣어서 실행합니다.
결과값이 길게 한줄 텍스트로 나오는데 이걸 고대로 복사해서 vscode -> 새문서 에 붙여넣고 Shift+alt+f 를 누르면 보기 좋게 나옵니다.

이렇게 구한 아이디 값은 잘 저장해 둡니다.
3-2 dns record id 구하기
curl https://api.cloudflare.com/client/v4/zones/존아이디/dns_records --header 'Authorization: Bearer 토큰값'
역시 위와 마찬가지로 상당히 긴 한줄 텍스트가 나오는데 vscode에서 json format 기능을 써서 보기 좋게 바꿔서 봅니다.

클플 dns 항목에 추가해놓은 항목값들이 나옵니다 한개만 넣은 사람은 한개만 나올테고 저처럼 여러개 세팅한 사람은 여러개 나올테고요. 그중에 수정하고 싶은 항목을 골라서 id값을 구합니다.
3-3 dns 갱신 스크립트
#!/bin/bash
# -------------------------------------------------------------------------------
# 2025-09-01
path_datfile="/경로/파일이름"
public_ip=""
prev_ip=""
zone_id="아까 구한 존아이디"
dns_record_id="아까 구한 dns 레코드 아이디"
token="아까 구한 토큰값"
ua="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36"
is_valid_public_ipv4() {
local ip=$1
# IPv4 형식 확인 (0~255까지 단순 형식)
if ! [[ $ip =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
return 1
fi
IFS='.' read -r o1 o2 o3 o4 <<< "$ip"
# 각 옥텟이 0~255 범위인지 확인
for octet in $o1 $o2 $o3 $o4; do
if ((octet < 0 || octet > 255)); then
return 1
fi
done
# 사설 IP 대역 제외
if ((o1 == 10)); then return 1; fi
if ((o1 == 172 && o2 >= 16 && o2 <= 31)); then return 1; fi
if ((o1 == 192 && o2 == 168)); then return 1; fi
# 루프백, 브로드캐스트, 링크로컬, 멀티캐스트, 예약 IP 제외
if ((o1 == 127)); then return 1; fi
if ((o1 == 169 && o2 == 254)); then return 1; fi
if ((o1 >= 224)); then return 1; fi
# 0.x.x.x (현재 네트워크) 및 255.255.255.255 (브로드캐스트) 제외
if ((o1 == 0)); then return 1; fi
if ((o1 == 255 && o2 == 255 && o3 == 255 && o4 == 255)); then return 1; fi
return 0
}
get_public_ip(){
local attempts=0
local max_attempts=5 #5번 시도해서 전부 망한 사이트가 나오진 않겠지
local ip=""
while (( attempts < max_attempts )); do
# 0~9 사이 랜덤 숫자 생성
local rand_num=$((RANDOM % 10))
case "$rand_num" in
0) ((attempts++)); continue ;; #ip=$(host 무언가.duckdns.org | grep "has address" | awk '{print $4}') ;;
1) ip=$(curl -s --user-agent "$ua" https://icanhazip.com/) ;;
2) ip=$(curl -s --user-agent "$ua" https://ipinfo.io/ip) ;;
3) ip=$(curl -s --user-agent "$ua" http://ident.me/) ;;
4) ip=$(curl -s --user-agent "$ua" https://wtfismyip.com/text) ;;
5) ip=$(curl -s --user-agent "$ua" https://api.ipify.org) ;;
6) ip=$(curl -s --user-agent "$ua" https://ipapi.co/ip) ;;
7) ip=$(curl -s --user-agent "$ua" https://checkip.amazonaws.com/) ;;
8) ip=$(curl -s --user-agent "$ua" http://myexternalip.com/raw) ;;
9) ip=$(curl -s https://ifconfig.me/) ;;
*) ((attempts++)); continue ;;
esac
break # 성공했으면 루프 탈출
done
echo "${rand_num}_${ip}"
}
main(){
start_time="$(date +"%Y-%m-%d %H:%M:%S")"
# 외부ip를 구함 get_site는 나중에 해당 서비스 종료되면 뺄려고 에러나는 거 기록용
# public_ip : 외부ip
# get_site : ip 구하는데 사용한 방식
IFS='_' read -r get_site public_ip <<< "$(get_public_ip)"
#앞뒤에 특수문자나 공백 삭제
public_ip=$(echo "$public_ip" | sed 's/^[[:space:][:punct:]]*//; s/[[:space:][:punct:]]*$//')
# 정상적인 외부ip를 못구하면 그냥 종료
if ! is_valid_public_ipv4 "$public_ip"; then
echo "[${start_time}] 외부ip 못구함: Site:$get_site / ${public_ip}"
exit 1
fi
# 파일에서 이전ip를 구한다
if [ -e "$path_datfile" ]; then
read -r prev_ip < $path_datfile
#앞뒤에 특수문자나 공백 삭제
prev_ip=$(echo "$prev_ip" | sed 's/^[[:space:][:punct:]]*//; s/[[:space:][:punct:]]*$//')
fi
# 이전ip랑 현재 공인 ip가 다르면..
if [ "$public_ip" != "$prev_ip" ]; then
echo $public_ip > $path_datfile # 현재 공인IP를 파일에 기록한다
chmod 600 $path_datfile # 파일은 개인 파일로 권한 변경
# 현재 시간을 구한다
update_date="갱신일: $(date '+%y년%-m월%-d일 %H:%M:%S')"
# 클플에 ip 업데이트를 한다
json_payload=$(cat <<EOF
{
"name": "@",
"ttl": 1,
"type": "A",
"content": "$public_ip",
"comment": "$update_date",
"proxied": true
}
EOF
)
response=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records/$dns_record_id" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $token" \
-d "$json_payload")
echo '-------------------------------------------------------------------------'
echo "Start: "$start_time
echo ""
echo "공인IP = "$public_ip
echo "이전IP = "$prev_ip
echo ""
# 응답 전체 출력
echo $response
echo ""
echo "End: "$(date +"%Y/%m/%d %H:%M:%S")
echo '-------------------------------------------------------------------------'
fi
}
main
원리를 간단하게 설명하자면…
외부 서비스 몇개를 이용해서 외부ip를 구합니다. (처음에는 duckdns만 사용했는데 가끔 걸리는 섭다운을 보고 다른 서비스도 추가 했습니다. 0번이 duckdns인데 요즘 정말 망한거 같아서 제외)
외부ip를 구했으면 이게 유효한지 체크를 함(사설망이라던지 루프백이라던지 체크 하는건데 이걸 하는 이유는 초기에 외부ip 구하는 방법이 외부서비스 없이 작동했는데 그때 가끔씩 사설망ip가 튀어나오는 문제 때문에 생겼음)
정상적인 외부ip를 구했다면 이전에 파일로 저장해둔걸 읽어서 이전ip와 동일한지 비교해보고 다를 경우에만 클플 갱신을 시도합니다.
저는 크론탭에 5분 간격으로 스크립트를 호출하게 등록해 놓았습니다.
4. 마무리
아직까진 잘 작동하는데 또 뭔가 터지면 업데이트 하도록 하겠습니다.
답글 남기기