Có hai cách để chúng ta có thể truy cập homelab từ bên ngoài:

  • Truy cập trực tiếp bằng Public IP của modem
  • Truy cập qua tên miền được cấu hình trỏ đến Public IP.

Đa số mọi người sẽ chọn cách thứ hai vì nó đơn giản, dễ nhớ hơn so với chuỗi dãy số IP xxx.xxx.xxx.xxx. Tuy nhiên nếu mạng ở nhà sử dụng IP động (đa số trường hợp), tên miền cần phải được tự động cập nhật mỗi khi Public IP thay đổi để tránh kết nối bị lỗi.

Hiện tại mình đang dùng 1 bash script trên Raspberry Pi để tự động cập nhật IP động cho tên miền qua dịch vụ CloudFlare. Nhờ vậy tên miền luôn được trỏ đến IP mới nhất được cấp cho modem mạng ở nhà.

Dưới đây là hướng dẫn chi tiết cách thiết lập

1. Yêu cầu

  • Đã có sẵn tên miền và đã có tài khoản Cloudflare.
  • Tên miền đã được chuyển về CloudFlare quản lý
  • Máy tính / máy ảo chạy Linux (Ubuntu / Arch / CentOS / …) hoặc Raspberry Pi (đời nào cũng được)

Lưu ý: Tên miền sử dụng phải là loại trả phí (.com, .net, .org, .me, …), không thể sử dụng các loại tên miền miễn phí (.tk, .ga, .ml, .cf, .gq, …) vì Cloudflare không hỗ trợ.

2. Cài đặt Cloudflare.sh script

Bạn cần truy cập SSH vào máy tính để thiết lập script.

Tạo file cloudflare.sh trong thư mục $HOME

cd ~
mkdir cloudflare
cd cloudflare
nano cloudflare.sh
Code language: Bash (bash)

Nhập vào nội dung sau

#!/bin/bash

# Forked from benkulbertis/cloudflare-update-record.sh
# CHANGE THESE

# API Token (Recommended)                                                                      #####
auth_token="xxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

# Domain and DNS record for synchronization
zone_identifier="f1nd7h3fuck1n6z0n31d3n71f13r4l50" # Can be found in the "Overview" tab of your domain
record_name="ipv4.example.org"                     # Which record you want to be synced

# DO NOT CHANGE LINES BELOW

# SCRIPT START
echo -e "Check Initiated"

# Check for current external network IP
ip=$(curl -s4 https://icanhazip.com/)
if [[ ! -z "${ip}" ]]; then
  echo -e "  > Fetched current external network IP: ${ip}"
else
  >&2 echo -e "Network error, cannot fetch external network IP."
fi

# The execution of update
if [[ ! -z "${auth_token}" ]]; then
  header_auth_paramheader=( -H '"Authorization: Bearer '${auth_token}'"' )
else
  header_auth_paramheader=( -H '"X-Auth-Email: '${auth_email}'"' -H '"X-Auth-Key: '${auth_key}'"' )
fi

# Seek for the record
seek_current_dns_value_cmd=( curl -s -X GET '"https://api.cloudflare.com/client/v4/zones/'${zone_identifier}'/dns_records?name='${record_name}'&type=A"' "${header_auth_paramheader[@]}" -H '"Content-Type: application/json"' )
record=`eval ${seek_current_dns_value_cmd[@]}`

# Can't do anything without the record
if [[ -z "${record}" ]]; then
  >&2 echo -e "Network error, cannot fetch DNS record."
  exit 1
elif [[ "${record}" == *'"count":0'* ]]; then
  >&2 echo -e "Record does not exist, perhaps create one first?"
  exit 1
fi

# Set the record identifier from result
record_identifier=`echo "${record}" | sed 's/.*"id":"//;s/".*//'`

# Set existing IP address from the fetched record
old_ip=`echo "${record}" | sed 's/.*"content":"//;s/".*//'`
echo -e "  > Fetched current DNS record value   : ${old_ip}"

# Compare if they're the same
if [ "${ip}" == "${old_ip}" ]; then
  echo -e "Update for A record '${record_name} (${record_identifier})' cancelled.\\n  Reason: IP has not changed."
  exit 0
else
  echo -e "  > Different IP addresses detected, synchronizing..."
fi

# The secret sause for executing the update
json_data_v4="'"'{"id":"'${zone_identifier}'","type":"A","proxied":true,"name":"'${record_name}'","content":"'${ip}'","ttl":120}'"'"
update_cmd=( curl -s -X PUT '"https://api.cloudflare.com/client/v4/zones/'${zone_identifier}'/dns_records/'${record_identifier}'"' "${header_auth_paramheader[@]}" -H '"Content-Type: application/json"' )

# Execution result
update=`eval ${update_cmd[@]} --data $json_data_v4`

# The moment of truth
case "$update" in
*'"success":true'*)
  echo -e "Update for A record '${record_name} (${record_identifier})' succeeded.\\n  - Old value: ${old_ip}\\n  + New value: ${ip}";;
*)
  >&2 echo -e "Update for A record '${record_name} (${record_identifier})' failed.\\nDUMPING RESULTS:\\n${update}"
  exit 1;;
esac
Code language: Bash (bash)

Bạn cần phải điền thông tin vào các dòng sau:

  • auth_token: Cloudflare API Token
  • zone_identifier: Zone ID
  • record_name: Tên miền

3. Tạo Cloudflare API Token

Truy cập vào trang Cloudflare API Token để tạo Token mới

Bấm vào Create Token
Chọn Use Template ở mục Edit Zone DNS
Kéo xuống mục Zone Resources, chọn tên miền bạn muốn sử dụng để cập nhật IP sau đó bấm Continue to Summary
Bấm Create Token
Token đã tạo xong

Bạn cần lưu lại Token vừa mới tạo để lưu vào script đã tạo. Token này sẽ không hiện ra lại trên Cloudflare vì lý do bảo mật. Nếu bạn quên lưu, cần phải xoá Token và tạo lại cái mới.

4. Tìm thông tin Zone ID

Từ trang chủ Cloudflare, bạn bấm vào tên miền muốn sử dụng để cập nhật IP. Sau đó kéo xuống dưới sẽ thấy mục Zone ID. Lưu lại thông số này để điền vào script

Lưu lại thông tin Zone ID

5. Tạo A Record cho tên miền

Ví dụ mình muốn sử dụng tên miền alibaba.thuanbui.me để truy cập đến homelab ở nhà. Bấm vào Add record và tạo một A record mới.

  • Name: điền vào alibaba
  • IPv4: điền tạm 1.1.1.1, thông số này sẽ được tự động cập nhật về IP của modem sau khi chạy script.
Tạo A Record cho subdomain alibaba.thuanbui.me

6. Cập nhật script

Thông tin cần thiết đã có đầy đủ, mình cập nhật lại vào file cloudflare.sh và lưu lại

#!/bin/bash

# Forked from benkulbertis/cloudflare-update-record.sh
# CHANGE THESE

# API Token (Recommended)                                                                  #####
auth_token="LrAB--xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

# Domain and DNS record for synchronization
zone_identifier="48bxxxxxxxxxxxxxxxxxxxx60" # Can be found in the "Overview" tab of your domain
record_name="alibaba.thuanbui.me"                     # Which record you want to be synced

# DO NOT CHANGE LINES BELOW

# SCRIPT START
echo -e "Check Initiated"

# Check for current external network IP
ip=$(curl -s4 https://icanhazip.com/)
if [[ ! -z "${ip}" ]]; then
  echo -e "  > Fetched current external network IP: ${ip}"
else
  >&2 echo -e "Network error, cannot fetch external network IP."
fi

# The execution of update
if [[ ! -z "${auth_token}" ]]; then
  header_auth_paramheader=( -H '"Authorization: Bearer '${auth_token}'"' )
else
  header_auth_paramheader=( -H '"X-Auth-Email: '${auth_email}'"' -H '"X-Auth-Key: '${auth_key}'"' )
fi

# Seek for the record
seek_current_dns_value_cmd=( curl -s -X GET '"https://api.cloudflare.com/client/v4/zones/'${zone_identifier}'/dns_records?name='${record_name}'&type=A"' "${header_auth_paramheader[@]}" -H '"Content-Type: application/json"' )
record=`eval ${seek_current_dns_value_cmd[@]}`

# Can't do anything without the record
if [[ -z "${record}" ]]; then
  >&2 echo -e "Network error, cannot fetch DNS record."
  exit 1
elif [[ "${record}" == *'"count":0'* ]]; then
  >&2 echo -e "Record does not exist, perhaps create one first?"
  exit 1
fi

# Set the record identifier from result
record_identifier=`echo "${record}" | sed 's/.*"id":"//;s/".*//'`

# Set existing IP address from the fetched record
old_ip=`echo "${record}" | sed 's/.*"content":"//;s/".*//'`
echo -e "  > Fetched current DNS record value   : ${old_ip}"

# Compare if they're the same
if [ "${ip}" == "${old_ip}" ]; then
  echo -e "Update for A record '${record_name} (${record_identifier})' cancelled.\\n  Reason: IP has not changed."
  exit 0
else
  echo -e "  > Different IP addresses detected, synchronizing..."
fi

# The secret sause for executing the update
json_data_v4="'"'{"id":"'${zone_identifier}'","type":"A","proxied":true,"name":"'${record_name}'","content":"'${ip}'","ttl":120}'"'"
update_cmd=( curl -s -X PUT '"https://api.cloudflare.com/client/v4/zones/'${zone_identifier}'/dns_records/'${record_identifier}'"' "${header_auth_paramheader[@]}" -H '"Content-Type: application/json"' )

# Execution result
update=`eval ${update_cmd[@]} --data $json_data_v4`

# The moment of truth
case "$update" in
*'"success":true'*)
  echo -e "Update for A record '${record_name} (${record_identifier})' succeeded.\\n  - Old value: ${old_ip}\\n  + New value: ${ip}";;
*)
  >&2 echo -e "Update for A record '${record_name} (${record_identifier})' failed.\\nDUMPING RESULTS:\\n${update}"
  exit 1;;
esac
Code language: Bash (bash)

Mặc định, script này sẽ kích hoạt chế độ proxy trên Cloudflare nhằm mục đích ẩn IP của modem, giảm nguy cơ bị tấn công vào mạng nhà. Nếu vì lý do nào đó không muốn ẩn IP của mình thì có thể thay đổi thông số "proxied":true thành "proxied":false

7. Chạy thử nghiệm

Thiết lập quyền thực thi cho file cloudflare.sh

chmod +X cloudflare.shCode language: CSS (css)

Chạy thử xem nào

./cloudflare.sh
thuanbui@raspberrypi:~/cloudflare $ ./cloudflare.sh
bash: warning: setlocale: LC_ALL: cannot change locale (en_US.UTF-8)
Check Initiated
  > Fetched current external network IP: 27.xx.xx.xx
  > Fetched current DNS record value   : 1.1.1.1
  > Different IP addresses detected, synchronizing...
Update for A record 'alibaba.thuanbui.me (48bxxxxxxxxxxxxxxxxxxxx60)' succeeded.
  - Old value: 1.1.1.1
  + New value: 27.xx.xx.xx
Code language: YAML (yaml)

Quay lại Cloudflare, tên miền alibaba.thuanbui.me, mình thấy đã được tự động cập nhật với IP mới. Ngon lành!

8. Thiết lập cron tự động

Việc cuối cùng cần làm là thiết lập cron để script tự động chạy 24/7. Sau đó, không cần bận tâm đến nó nữa.

crontab -eCode language: Nginx (nginx)

Mình chỉnh cho script tự động chạy sau mỗi 10 phút bằng cách thêm dòng này vào dưới cùng

*/10 * * * * /bin/bash /home/thuanbui/cloudflare/cloudflare.shCode language: Markdown (markdown)

Chú ý nhớ thay /home/thuanbui/cloudflare/cloudflare.sh thành đường dẫn đến nơi lưu file cloudflare.sh của bạn.

Chúc bạn thực hiện thành công!

[convertkit form=7087807]

Lưu ý: Nếu bạn cần hỗ trợ kỹ thuật, vui lòng gửi câu hỏi trực tiếp ở phần Thảo luận bên dưới, mình sẽ trả lời sớm. Đừng vào hỏi trong fanpage Yêu Chạy Bộ, sẽ không có phản hồi đâu!

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *


55 Comments

  1. bạn cho hỏi mình bị lỗi nay khi chay ./cloudflare.sh
    ./cloudflare.sh: command substitution: line 20: syntax error near unexpected token `)’
    ./cloudflare.sh: command substitution: line 20: `curl -s4 )’
    Network error, cannot fetch external network IP.
    Network error, cannot fetch DNS record.

    1. Mình mới kiểm tra lại, dòng 20 bị dư 2 dấu < > nên đã cập nhật lại. Bạn chỉnh code lại theo bản mới ở trên nhé. Cám ơn bạn đã thông báo.

  2. em chạy lệnh thì nó không hiện lỗi dòng nào, nhưng nó báo không fetch dns.

    root@rpisrv4:/home/pi/cloudflare# bash cloudflare.sh
    Check Initiated
    > Fetched current external network IP: 113.113.173.201
    Network error, cannot fetch DNS record.

    thêm nữa, phần Auth_token nó có 2 dấu [–]
    nhưng trong phần token em lấy thì nó không có dấu gạch đó. Em phải điền sao cho đúng?

    1. Nhiều khả năng là em nhập sai API Token rồi. Do sai nên nó không truy cập Cloudflare được nên báo lỗi Cannot fetch DNS Record.
      Chú ý là cần lấy API Token, không phải Global API nhé.

      1. 1. Về API token, em làm theo hướng dẫn của anh để lấy, nhưng API token khi nhập trong code nó định dạng thêm –.
        Trong khi API Token của em lấy thì nó không có cái dấu –.
        Em phải điền sao cho đúng.
        Ví dụ:
        API token của em là ABCDEFGHIJKLM.
        Khi nhập vào đoạn code của anh sẽ cắt 4 ký tự đầu ra ABCD–EFGH… ==> như vậy có đúng không?

        2. Lỗi trả về khi em chạy file bằng 2 lệnh nó khác nhau.
        Lệnh sh:
        pi@rpisrv4:~/cloudflare $ sh cloudflare.sh
        -e Check Initiated
        cloudflare.sh: 20: cloudflare.sh: [[: not found
        -e Network error, cannot fetch external network IP.
        cloudflare.sh: 28: cloudflare.sh: Syntax error: “(” unexpected (expecting “fi”)

        Lệnh bash
        pi@rpisrv4:~/cloudflare $ bash cloudflare.sh
        Check Initiated
        > Fetched current external network IP: xxx.113.173.201
        Network error, cannot fetch DNS record.

        1. API Token nó hiện ra sao thì paste vô y hệt vậy, không có chia ra gì hết nha em. Thêm — vô là nó sai rồi.
          Bản chất cloudflare.sh nó là câu lệnh rồi nên em gõ `sh cloudflare.sh` là sai. Chỉ cần gõ `./cloudflare.sh` là được.

  3. Check Initiated
    > Fetched current external network IP: 155.64.16.13
    Network error, cannot fetch DNS record.

    Admin giúp mình với ạ. Mình đã check đi check lại API token, zone ID đúng nhưng vẫn bị lỗi trên

    1. Báo lỗi Network error, cannot fetch DNS record nghĩa là kết nối đến Cloudflare không thành công. Bạn cần kiểm tra lại các thông số: API Token, Zone ID, record name

        1. Trang API của Cloudflare không bị gì cả nhé. Bạn có thể test curl theo lệnh này: curl -X GET "https://api.cloudflare.com/client/v4/zones/Thay-ZONE-ID-vô-đây" -H "Content-Type:application/json" -H "Authorization: Bearer Thay-API-Token-vô-đây". Lưu ý là gõ lệnh vào Terminal, không phải gõ vào trình duyệt. Nếu thông tin chính xác nó sẽ trả về kết quả tương tự như sau

          {"result":{"id":"aeb40eaxxxxxxf51a0b","name":"thuanbui.me","status":"active","paused":false,"type":"full","development_mode":0,"name_servers":["ian.ns.cloudflare.com","melinda.ns.cloudflare.com"],"original_name_servers":["dns1.registrar-servers.com","dns2.registrar-servers.com","dns3.registrar-servers.com","dns4.registrar-servers.com","dns5.registrar-servers.com"],"original_registrar":"namesilo, llc r203-me","original_dnshost":null,"modified_on":"2020-04-24T22:27:18.820906Z","created_on":"2016-03-12T12:06:29.074831Z","activated_on":"2016-03-12T12:08:53.006040Z","meta":{"step":4,"custom_certificate_quota":0,"page_rule_quota":3,"phishing_detected":false,"multiple_railguns_allowed":false},"owner":{"id":"d139f5e53xxxxxxc0d8","type":"user","email":"xxxxx@gmail.com"},"account":{"id":"b9xxxxxx9d4bb","name":"xxxxxx7@gmail.com"},"permissions":["#access:edit","#access:read","#analytics:read","#app:edit","#auditlogs:read","#billing:edit","#billing:read","#cache_purge:edit","#dns_records:edit","#dns_records:read","#healthchecks:edit","#healthchecks:read","#image:edit","#image:read","#lb:edit","#lb:read","#legal:edit","#legal:read","#logs:edit","#logs:read","#member:edit","#member:read","#organization:edit","#organization:read","#ssl:edit","#ssl:read","#stream:edit","#stream:read","#subscription:edit","#subscription:read","#teams:edit","#teams:pii","#teams:read","#teams:report","#waf:edit","#waf:read","#waitingroom:edit","#waitingroom:read","#webhooks:edit","#webhooks:read","#worker:edit","#worker:read","#zaraz:edit","#zaraz:read","#zone:edit","#zone:read","#zone_settings:edit","#zone_settings:read"],"plan":{"id":"0feeeeeeeexxxxxxe","name":"Free Website","price":0,"currency":"USD","frequency":"","is_subscribed":false,"can_subscribe":false,"legacy_id":"free","legacy_discount":false,"externally_managed":false}},"success":true,"errors":[],"messages":[]}
          1. Em chạy lệnh “curl” trên thì ra đúng kết quả như anh nói rồi ạ, nhưng chạy /.cloudflare.sh thì vẫn bị lỗi “Network error, cannot fetch DNS record”

            Anh giúp em với ạ

          2. Nếu chạy curl ra đúng thì có nghĩa các thông số đều đúng. Chạy file sh báo lỗi thì nhiều khả năng là phần thiết lập thông số trong file cloudflare.sh bị sai chỗ nào rồi. Kiểm tra lại 5 thông số này:

            • auth_email: điền email tài khoản Cloudflare
            • auth_key: bỏ trống
            • auth_toke: điền vào Token Key
            • zone_identifier: mã Zone
            • record_name: tên miền sub domain
  4. Check Initiated
    > Fetched current external network IP: 115.77.50.116
    Network error, cannot fetch DNS record.

    Mình đã check đi check lại API token, zone ID đúng nhưng vẫn bị lỗi trên.
    Mình đã chạy curl ở bình luận trên, thông tin token , zone id mình củng đều đúng.
    Mình đã thêm auth_email luôn rồi.
    Mong admin giúp đỡ

    1. Bạn cập nhật lại script trong bài nhé. Mình mới cập nhật lại script mới, cái cũ bị lỗi dư kí tự < và > nên fetch DNS bị lỗi. Sorry bạn nhiều.

  5. a cho e hỏi, nếu có nhiều script để chạy cho nhiều subdomain thì setup sao vậy a? e có thử thêm nhiều dòng script vào cron như vầy:
    */10 * * * * /bin/bash /home/thuanbui/cloudflare/1.sh
    */10 * * * * /bin/bash /home/thuanbui/cloudflare/2.sh
    */10 * * * * /bin/bash /home/thuanbui/cloudflare/3.sh
    chỉ có cái 1.sh là chạy auto được. ( 1,2,3.sh khi chạy tay đều ok) cám ơn a.

    1. Bạn thử kết hợp tất cả thành 1 dòng như thế này xem có chạy ổn không nhé: */10 * * * * /bin/bash /home/thuanbui/cloudflare/1.sh ; /bin/bash /home/thuanbui/cloudflare/2.sh ; /bin/bash /home/thuanbui/cloudflare/3.sh

  6. Bác ơi xem giúp e xem tại sao sau khi chạy script nó lại báo như thế này. thanks bác, mọi thông số e đã nhập đúng.
    Check Initiated
    > Fetched current external network IP: 113.xxx.xxx.94
    > Fetched current DNS record value : 8.8.8.8
    > Different IP addresses detected, synchronizing…
    Update for A record ‘home.vinhvinhcloud.tk (f45313b2974xxxxxxx646fb3c94386a6)’ failed.
    DUMPING RESULTS:

      1. E có di chuyển bằng lệnh cd như hd bác rồi ạ. Nhưng nó vẫn báo như kia 🙁

        lethanhtung@ubuntu:~$ cd cloudflare
        lethanhtung@ubuntu:~/cloudflare$ dir
        cloudflare.sh
        lethanhtung@ubuntu:~/cloudflare$ sudo chmod +X cloudflare.sh
        [sudo] password for lethanhtung:
        lethanhtung@ubuntu:~/cloudflare$ sudo ./cloudflare.sh
        sudo: ./cloudflare.sh: command not found
        lethanhtung@ubuntu:~/cloudflare$

  7. Khi em chạy nó hiện ra lỗi này ạ:
    root@HungNAS:~# chmod +X cloudflare.sh
    root@HungNAS:~# chmod +X cloudflare.sh
    root@HungNAS:~# ./cloudflare.sh
    -bash: ./cloudflare.sh: /bin/bash^M: bad interpreter: No such file or directory

  8. Mình cũng bị lỗi Network error, cannot fetch DNS record. như mấy bạn trên, tất cả mọi thứ mình đều điền đúng và kiểm tra rất nhiều lần, không biết là bị sao ạ? Mình dùng debian 11

  9. bài viết quá hay cảm ơn bác ạ, hôm qua em nghe nói là có ông nào đó dùng duckdns xong ném cái địa chỉ của nó vào trong value cname mà k rõ lắm bác có thể giải thích cho em không ạ

    1. Cái địa chỉ của DuckDNS nó đã được trỏ về đúng IP ở nhà. Khi bạn set CNAME là địa chỉ DuckDNS thì cái tên miền sẽ được trỏ về cái IP của DuckDNS.

  10. ad cho mình hỏi, mình muốn truy cập từ cái subdomain đó trỏ về một host trong mạng của mình thì mình phải làm như nào ạ. Ví dụ mình dùng con ubuntu để cài giám sát nagios, chạy thêm cả scrip để cập nhật địa chỉ public như bài được rồi. Làm cách nào để trỏ đến web interface của con giám sát đc ?

    1. Mình không hiểu câu hỏi của bạn nhé. Nếu bạn đang hỏi về cách dùng subdomain để truy cập vào web service trên máy chủ thì cài thêm reverse proxy: nginx hoặc caddy.

  11. anh ơi. anh có thể nói rõ hơn chỗ phần chạy crontab được ko anh? thêm vào đâu ạ ? Vì em chạy lệnh crontab -e nó tự động tạo ra một cái file txt… em ko biết là add vào cuối file đó hay là file cloudflare.sh ạ?

  12. Anh Thuận ơi, em đang cần cấu hình Proxy cho TCP mà trên cloudflare cái đó mất tiền nên em phải tắt cái status Proxied (DNS only) trên record typeA. Nhưng mỗi khi chạy script , recordA vừa update IP Wan mới vừa ghi đè lại cái status Proxied. Có trường nào để em set cứng nó là DNS only trong script không ạ

      1. Em cảm ơn anh nha, thực ra em đang muốn làm proxy cho MQTT (tcp request) nhưng khi em dùng lệnh này thì Nginx local nhận địa chỉ IP đầu vào là của Cloudflare nên Mqtt Broker nó không định danh được client:
        $ dig @1.1.1.1 iot-mqtt-broker.click
        Em mò thì cần phải cài cấu hình Tcp Proxied trên CF và mất tiền phải không ạ