commit 1786ce366b49d3d4185e0ce938178d1fe503dc9e Author: jung geun Date: Sun Sep 1 03:26:24 2024 +0900 Initial commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ae20309 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +src/cloudflare_cname.py +README.md \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b4a5ac9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +src/cloudflare_cname.py \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3381580 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +FROM python:3.9-slim + +# Install required packages +RUN apt-get update && apt-get install -y \ + cron \ + make \ + && rm -rf /var/lib/apt/lists/* + +RUN mkdir -p /app/cloudflare-ddns/ +WORKDIR /app/cloudflare-ddns +RUN mkdir -p /app/cloudflare-ddns/config + +COPY . . + +RUN pip install --no-cache-dir -r requirements.txt + +RUN cp /app/cloudflare-ddns/cron/cronjob /etc/cron.d/cloudflare-ddns + +RUN touch /var/log/cron.log +RUN chmod +x /app/cloudflare-ddns/start.sh +RUN chmod +x /app/cloudflare-ddns/run_script.sh + +VOLUME [ "/app/cloudflare-ddns/config" ] + +ENTRYPOINT [ "bash", "-c" ] + +CMD [ "/app/cloudflare-ddns/start.sh && cron -f" ] \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..81050b1 --- /dev/null +++ b/Makefile @@ -0,0 +1,47 @@ +IMAGE_NAME = "cloudflare-ddns" + +default: build + +.PHONY: build +build: stop + docker build -t $(IMAGE_NAME) . + +.PHONY: run +run: + docker run --rm --privileged=true -d -v /opt/cloudflare-ddns/config:/app/cloudflare-ddns/config $(IMAGE_NAME) + +.PHONY: stop +stop: + @container_id=$$(docker ps -q -f ancestor=$(IMAGE_NAME)); \ + if [ -n "$$container_id" ]; then \ + echo "Stopping container $$container_id"; \ + docker stop $$container_id; \ + else \ + echo "No running container found for image $(IMAGE_NAME)"; \ + fi + +.PHONY: install +install: + @echo "Installing cloudflare-ddns to '/app/cloudflare-ddns'" + @mkdir -p /app/cloudflare-ddns + @cp -r ./* /app/cloudflare-ddns + @if [ ! -f /app/cloudflare-ddns/config/env.json ]; then \ + echo "Creating default env.json"; \ + mkdir -p /app/cloudflare-ddns/config; \ + cp /app/cloudflare-ddns/init/default_env.json /app/cloudflare-ddns/config/env.json; \ + echo "Please edit /app/cloudflare-ddns/config/env.json"; \ + fi + @cp /app/cloudflare-ddns/cron/cronjob /etc/cron.d/cloudflare-ddns + @touch /var/log/cloudflare_ddns.log + @chmod +x /app/cloudflare-ddns/run_script.sh + @chmod +x /app/cloudflare-ddns/start.sh + + @echo "Enter 'make configure' to configure the Cloudflare API key, domain and ZONE ID" + + @echo "Done" + +.PHONY: configure +configure: + @echo "Please edit /app/cloudflare-ddns/config/env.json" + @vi /app/cloudflare-ddns/config/env.json + @echo "Done" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..51e4755 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# Cloudflare DDNS Project + +This project aims to implement a Dynamic DNS (DDNS) solution using Cloudflare's API. + +## Overview +Dynamic DNS allows you to associate a domain name with a changing IP address, making it easier to access your network resources remotely. Cloudflare's API provides the necessary tools to automate this process. + +## Prerequisites +Before getting started, make sure you have the following: + +- A Cloudflare account +- A registered domain name +- API credentials from Cloudflare + +## Installation +To install and set up the project, follow these steps: + +1. Clone the repository: `git clone https://github.com/jung-geun/cloudflare-ddns.git` +2. Deploy the project to a server or cloud platform: + - You can deploy the project to a server or cloud platform of your choice. + - Make sure to install the necessary dependencies by running 'make install' +3. Configure the project: + - Create a configuration file named `config.json` in the initial + 'make configure' will create a configuration file with default settings. + - Add your Cloudflare API credentials and domain information to the configuration file. + + +## Usage +Once the project is up and running, it will periodically check for IP address changes and update the DNS records accordingly. You can customize the update frequency and other settings in the configuration file. + +## Contributing +Contributions are welcome! If you have any suggestions or improvements, feel free to open an issue or submit a pull request. + +## License +This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for more details. +## 기여하기 +기여는 환영합니다! 제안이나 개선 사항이 있으시면 이슈를 열거나 풀 리퀘스트를 제출해 주세요. + +## 라이선스 +이 프로젝트는 MIT 라이선스로 배포됩니다. 자세한 내용은 [LICENSE](./LICENSE) 파일을 참조하세요. \ No newline at end of file diff --git a/cron/cronjob b/cron/cronjob new file mode 100644 index 0000000..8acae72 --- /dev/null +++ b/cron/cronjob @@ -0,0 +1 @@ +*/5 * * * * root /app/cloudflare-ddns/run_script.sh diff --git a/init/default_env.json b/init/default_env.json new file mode 100644 index 0000000..ed8fac5 --- /dev/null +++ b/init/default_env.json @@ -0,0 +1,5 @@ +{ + "CLOUDFLARE_API_KEY": "", + "CLOUDFLARE_DOMAIN": "", + "CLOUDFLARE_ZONE_ID": "" +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..fd7d3e0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +requests==2.25.1 \ No newline at end of file diff --git a/run_script.sh b/run_script.sh new file mode 100644 index 0000000..4cd77a4 --- /dev/null +++ b/run_script.sh @@ -0,0 +1,7 @@ +#!/bin/bash +export CLOUDFLARE_API_KEY=$API_KEY +export CLOUDFLARE_DOMAIN=$DOMAIN_NAME +export CLOUDFLARE_ZONE_ID=$ZONE_ID + +# Run the script +python3 /app/cloudflare-ddns/src/update_dns.py $@ \ No newline at end of file diff --git a/src/update_dns.py b/src/update_dns.py new file mode 100644 index 0000000..406280a --- /dev/null +++ b/src/update_dns.py @@ -0,0 +1,168 @@ +import logging.handlers +import os +import sys +import requests +import json +import logging + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) +format = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S") +request_log = logging.getLogger("requests").setLevel(logging.WARNING) +fileHandler = logging.handlers.RotatingFileHandler( + "/var/log/cloudflare_ddns.log", maxBytes=100000, backupCount=5 +) +fileHandler.setFormatter(format) + +logger.addHandler(fileHandler) +logger.addHandler(logging.StreamHandler()) + + + +def load_config(): + config = {} + required_keys = ["CLOUDFLARE_API_KEY", "CLOUDFLARE_DOMAIN", "CLOUDFLARE_ZONE_ID"] + + # 1. env.json 파일에서 설정 로드 (있는 경우) + json_file = "/app/cloudflare-ddns/config/env.json" + if os.path.exists(json_file): + with open(json_file, "r") as file: + config = json.load(file) + + + # 2. 환경 변수에서 설정 로드 (파일에 없는 경우에만) + for key in required_keys: + if key not in config and key in os.environ: + config[key] = os.getenv(key) + + # 3. 필수 키가 모두 있는지 확인 + missing_keys = [key for key in required_keys if key not in config] + if missing_keys: + raise ValueError(f"Missing required configuration: {', '.join(missing_keys)}") + + return config + + +def get_ip(): + try: + response = requests.get("https://ifconfig.me") + return response.text + except Exception as e: + logger.error(f"Error: {e}") + # print(f"Error: {e}") + return None + + +def previous_ip(): + try: + if os.path.exists("/tmp/external_ip.txt"): + with open("/tmp/external_ip.txt", "r") as file: + return file.read() + else: + os.mknod("/tmp/external_ip.txt") + except Exception as e: + logger.error(f"Error: {e}") + # print(f"Error: {e}") + return None + + +def update_ip(ip): + try: + with open("/tmp/external_ip.txt", "w") as file: + file.write(ip) + except Exception as e: + logger.error(f"Error: {e}") + # print(f"Error: {e}") + return None + + +def get_record(zone_id, domain_name, api_key): + try: + url = f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records" + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {api_key}", + } + params = { + "type": "A", + "name": domain_name, + } + response = requests.get(url, headers=headers, params=params) + records = response.json()["result"] + return records if records else None + except Exception as e: + logger.error(f"Error: {e}") + logger.warning("recommendation: check the environment variables") + # print(f"Error: {e}") + return None + + +def update_dns_record(zone_id, record_id, record_name, ip_address, api_key): + try: + url = f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records/{record_id}" + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {api_key}", + } + data = { + "type": "A", + "name": record_name, + "content": ip_address, + "ttl": 1, + "proxied": True, + } + response = requests.put(url, headers=headers, data=json.dumps(data)) + success = response.json()["success"] + return success if success else None + except Exception as e: + logger.error(f"Error: {e}") + logger.warning("recommendation: check the environment variables") + # print(f"Error: {e}") + return None + + +if __name__ == "__main__": + config = load_config() + + external_ip = get_ip() + logger.info(f"External IP: {external_ip}") + # print(f"External IP: {external_ip}") + previous_ip_ = previous_ip() + logger.info(f"Previous IP: {previous_ip_}") + # print(f"Previous IP: {previous_ip_}") + + if external_ip != previous_ip_: + logger.info("IP has changed") + # print("IP has changed") + + records = get_record( + config["CLOUDFLARE_ZONE_ID"], + config["CLOUDFLARE_DOMAIN"], + config["CLOUDFLARE_API_KEY"], + ) + if not records: + logger.warning("No records found") + # print("No records found") + sys.exit(0) + + record_id = records[0]["id"] + + result = update_dns_record( + config["CLOUDFLARE_ZONE_ID"], + record_id, + config["CLOUDFLARE_DOMAIN"], + external_ip, + config["CLOUDFLARE_API_KEY"], + ) + if not result: + logger.error("Failed to update DNS record") + # print("Failed to update DNS record") + sys.exit(0) + + update_ip(external_ip) + logger.info("IP has been updated") + # print("IP has been updated") + else: + logger.info("IP has not changed") + # print("IP has not changed") + sys.exit(0) diff --git a/start.sh b/start.sh new file mode 100644 index 0000000..275be7c --- /dev/null +++ b/start.sh @@ -0,0 +1,8 @@ +#!/bin/bash +if [ ! -f /app/cloudflare-ddns/config/env.json ]; then + cp /app/cloudflare-ddns/init/default_env.json /app/cloudflare-ddns/config/env.json +fi + +sh /app/cloudflare-ddns/run_script.sh + +# tail -f /var/log/cloudflare_ddns.log \ No newline at end of file