├── .github └── workflows │ ├── dns_cf.yml │ ├── dns_pod.yml │ └── sync.yml ├── CNAME ├── README.md ├── dnscf.py ├── dnspod.py ├── index.html ├── ipTop.html ├── ipTop10.html ├── qCloud.py └── requirements.txt /.github/workflows/dns_cf.yml: -------------------------------------------------------------------------------- 1 | name: 'dns_cf_push' 2 | 3 | on: 4 | schedule: 5 | - cron: '0 */6 * * *' 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: 'Checkout' 12 | uses: actions/checkout@v3 13 | - name: 'Set up Python' 14 | uses: actions/setup-python@v4 15 | with: 16 | python-version: 3.12 17 | - name: 'Install dependencies' 18 | run: if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 19 | - name: 'run dnscf' 20 | env: 21 | CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }} 22 | CF_ZONE_ID: ${{ secrets.CF_ZONE_ID }} 23 | CF_DNS_NAME: ${{ secrets.CF_DNS_NAME }} 24 | PUSHPLUS_TOKEN: ${{ secrets.PUSHPLUS_TOKEN }} 25 | run: python dnscf.py 26 | -------------------------------------------------------------------------------- /.github/workflows/dns_pod.yml: -------------------------------------------------------------------------------- 1 | name: 'dns_pod_push' 2 | 3 | on: 4 | schedule: 5 | - cron: '0 */6 * * *' 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: 'Checkout' 12 | uses: actions/checkout@v3 13 | - name: 'Set up Python' 14 | uses: actions/setup-python@v4 15 | with: 16 | python-version: 3.12 17 | - name: 'Install dependencies' 18 | run: if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 19 | - name: 'run dnspod' 20 | env: 21 | DOMAIN: ${{ secrets.DOMAIN }} 22 | SUB_DOMAIN: ${{ secrets.SUB_DOMAIN }} 23 | SECRETID: ${{ secrets.SECRETID }} 24 | SECRETKEY: ${{ secrets.SECRETKEY }} 25 | PUSHPLUS_TOKEN: ${{ secrets.PUSHPLUS_TOKEN }} 26 | run: python dnspod.py 27 | -------------------------------------------------------------------------------- /.github/workflows/sync.yml: -------------------------------------------------------------------------------- 1 | name: Upstream Sync 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | schedule: 8 | - cron: "0 0 * * *" # every day 9 | workflow_dispatch: 10 | 11 | jobs: 12 | sync_latest_from_upstream: 13 | name: Sync latest commits from upstream repo 14 | runs-on: ubuntu-latest 15 | if: ${{ github.event.repository.fork }} 16 | 17 | steps: 18 | # Step 1: run a standard checkout action 19 | - name: Checkout target repo 20 | uses: actions/checkout@v3 21 | 22 | # Step 2: run the sync action 23 | - name: Sync upstream changes 24 | id: sync 25 | uses: aormsby/Fork-Sync-With-Upstream-action@v3.4 26 | with: 27 | upstream_sync_repo: ZhiXuanWang/cf-speed-dns 28 | upstream_sync_branch: main 29 | target_sync_branch: main 30 | target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set 31 | 32 | # Set test_mode true to run tests instead of the true action!! 33 | test_mode: false 34 | 35 | - name: Sync check 36 | if: failure() 37 | run: | 38 | echo "[Error] 由于上游仓库的 workflow 文件变更,导致 GitHub 自动暂停了本次自动更新,你需要手动 Sync Fork 一次。" 39 | exit 1 40 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | ip.164746.xyz -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## cf-speed-dns是什么? 2 | CloudflareSpeedTest 推送「每5分钟自选优选 IP」获取Cloudflare CDN 延迟和速度最快 IP ! 3 | 4 | ## cf-speed-dns有哪些功能? 5 | * CloudflareSpeedTest优选IP,实时更新列表页面。[https://ip.164746.xyz](https://ip.164746.xyz) 6 | * CloudflareSpeedTest优选IP,Top接口(默认)[https://ip.164746.xyz/ipTop.html](https://ip.164746.xyz/ipTop.html);Top10接口[https://ip.164746.xyz/ipTop10.html](https://ip.164746.xyz/ipTop10.html)。 7 | * DNSPOD实时域名解析推送,fork 本项目。 8 | * Action配置,Actions secrets and variables 添加 DOMAIN(例如:164746.xyz),SUB_DOMAIN(例如:dns),SECRETID(xxxxx),SECRETKEY(xxxxx),PUSHPLUS_TOKEN(xxxxx)。 9 | * DNSCF实时域名解析推送,fork 本项目。 10 | * Action配置,Actions secrets and variables 添加 CF_API_TOKEN(例如:xxxxx),CF_ZONE_ID(例如:xxxxx),CF_DNS_NAME(dns.164746.xyz),PUSHPLUS_TOKEN(xxxxx)。 11 | * 接入PUSHPLUS消息通知。[https://www.pushplus.plus/push1.html](https://www.pushplus.plus/push1.html) 12 | 13 | ## 接口请求 14 | ```javascript 15 | curl 'https://ip.164746.xyz/ipTop.html' 16 | ``` 17 | ## 接口返回 18 | ```javascript 19 | 104.16.204.6,104.18.103.125 20 | ``` 21 | 22 | ## 感谢 23 | [XIU2](https://github.com/XIU2/CloudflareSpeedTest)、[ddgth](https://github.com/ddgth/cf2dns) 24 | -------------------------------------------------------------------------------- /dnscf.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import traceback 3 | import time 4 | import os 5 | import json 6 | 7 | # API 密钥 8 | CF_API_TOKEN = os.environ["CF_API_TOKEN"] 9 | CF_ZONE_ID = os.environ["CF_ZONE_ID"] 10 | CF_DNS_NAME = os.environ["CF_DNS_NAME"] 11 | 12 | # pushplus_token 13 | PUSHPLUS_TOKEN = os.environ["PUSHPLUS_TOKEN"] 14 | 15 | 16 | 17 | headers = { 18 | 'Authorization': f'Bearer {CF_API_TOKEN}', 19 | 'Content-Type': 'application/json' 20 | } 21 | 22 | def get_cf_speed_test_ip(timeout=10, max_retries=5): 23 | for attempt in range(max_retries): 24 | try: 25 | # 发送 GET 请求,设置超时 26 | response = requests.get('https://ip.164746.xyz/ipTop.html', timeout=timeout) 27 | # 检查响应状态码 28 | if response.status_code == 200: 29 | return response.text 30 | except Exception as e: 31 | traceback.print_exc() 32 | print(f"get_cf_speed_test_ip Request failed (attempt {attempt + 1}/{max_retries}): {e}") 33 | # 如果所有尝试都失败,返回 None 或者抛出异常,根据需要进行处理 34 | return None 35 | 36 | # 获取 DNS 记录 37 | def get_dns_records(name): 38 | def_info = [] 39 | url = f'https://api.cloudflare.com/client/v4/zones/{CF_ZONE_ID}/dns_records' 40 | response = requests.get(url, headers=headers) 41 | if response.status_code == 200: 42 | records = response.json()['result'] 43 | for record in records: 44 | if record['name'] == name: 45 | def_info.append(record['id']) 46 | return def_info 47 | else: 48 | print('Error fetching DNS records:', response.text) 49 | 50 | # 更新 DNS 记录 51 | def update_dns_record(record_id, name, cf_ip): 52 | url = f'https://api.cloudflare.com/client/v4/zones/{CF_ZONE_ID}/dns_records/{record_id}' 53 | data = { 54 | 'type': 'A', 55 | 'name': name, 56 | 'content': cf_ip 57 | } 58 | 59 | response = requests.put(url, headers=headers, json=data) 60 | 61 | if response.status_code == 200: 62 | print(f"cf_dns_change success: ---- Time: " + str( 63 | time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())) + " ---- ip:" + str(cf_ip)) 64 | return "ip:" + str(cf_ip) + "解析" + str(name) + "成功" 65 | else: 66 | traceback.print_exc() 67 | print(f"cf_dns_change ERROR: ---- Time: " + str( 68 | time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())) + " ---- MESSAGE: " + str(e)) 69 | return "ip:" + str(cf_ip) + "解析" + str(name) + "失败" 70 | 71 | # 消息推送 72 | def push_plus(content): 73 | url = 'http://www.pushplus.plus/send' 74 | data = { 75 | "token": PUSHPLUS_TOKEN, 76 | "title": "IP优选DNSCF推送", 77 | "content": content, 78 | "template": "markdown", 79 | "channel": "wechat" 80 | } 81 | body = json.dumps(data).encode(encoding='utf-8') 82 | headers = {'Content-Type': 'application/json'} 83 | requests.post(url, data=body, headers=headers) 84 | 85 | # 主函数 86 | def main(): 87 | # 获取最新优选IP 88 | ip_addresses_str = get_cf_speed_test_ip() 89 | ip_addresses = ip_addresses_str.split(',') 90 | dns_records = get_dns_records(CF_DNS_NAME) 91 | push_plus_content = [] 92 | # 遍历 IP 地址列表 93 | for index, ip_address in enumerate(ip_addresses): 94 | # 执行 DNS 变更 95 | dns = update_dns_record(dns_records[index], CF_DNS_NAME, ip_address) 96 | push_plus_content.append(dns) 97 | 98 | push_plus('\n'.join(push_plus_content)) 99 | 100 | if __name__ == '__main__': 101 | main() 102 | -------------------------------------------------------------------------------- /dnspod.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import time 4 | import requests 5 | from qCloud import QcloudApiv3 6 | import traceback 7 | import os 8 | import json 9 | 10 | # 域名和子域名 11 | DOMAIN = os.environ['DOMAIN'] 12 | SUB_DOMAIN = os.environ['SUB_DOMAIN'] 13 | 14 | # API 密钥 15 | SECRETID = os.environ["SECRETID"] 16 | SECRETKEY = os.environ["SECRETKEY"] 17 | 18 | # pushplus_token 19 | PUSHPLUS_TOKEN = os.environ["PUSHPLUS_TOKEN"] 20 | 21 | 22 | def get_cf_speed_test_ip(timeout=10, max_retries=5): 23 | for attempt in range(max_retries): 24 | try: 25 | # 发送 GET 请求,设置超时 26 | response = requests.get('https://ip.164746.xyz/ipTop.html', timeout=timeout) 27 | 28 | # 检查响应状态码 29 | if response.status_code == 200: 30 | return response.text 31 | except Exception as e: 32 | traceback.print_exc() 33 | print(f"get_cf_speed_test_ip Request failed (attempt {attempt + 1}/{max_retries}): {e}") 34 | # 如果所有尝试都失败,返回 None 或者抛出异常,根据需要进行处理 35 | return None 36 | 37 | 38 | def build_info(cloud): 39 | try: 40 | ret = cloud.get_record(DOMAIN, 100, SUB_DOMAIN, 'A') 41 | def_info = [] 42 | for record in ret["data"]["records"]: 43 | info = {"recordId": record["id"], "value": record["value"]} 44 | if record["line"] == "默认": 45 | def_info.append(info) 46 | print(f"build_info success: ---- Time: " + str( 47 | time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())) + " ---- ip:" + str(def_info)) 48 | return def_info 49 | except Exception as e: 50 | traceback.print_exc() 51 | print(f"build_info ERROR: ---- Time: " + str( 52 | time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())) + " ---- MESSAGE: " + str(e)) 53 | 54 | 55 | def change_dns(cloud, record_id, cf_ip): 56 | try: 57 | cloud.change_record(DOMAIN, record_id, SUB_DOMAIN, cf_ip, "A", "默认", 600) 58 | print(f"change_dns success: ---- Time: " + str( 59 | time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())) + " ---- ip:" + str(cf_ip)) 60 | return "ip:" + str(cf_ip) + "解析" + str(SUB_DOMAIN) + "." + str(DOMAIN) + "成功" 61 | 62 | except Exception as e: 63 | traceback.print_exc() 64 | print(f"change_dns ERROR: ---- Time: " + str( 65 | time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())) + " ---- MESSAGE: " + str(e)) 66 | return "ip:" + str(cf_ip) + "解析" + str(SUB_DOMAIN) + "." + str(DOMAIN) + "失败" 67 | 68 | 69 | def pushplus(content): 70 | url = 'http://www.pushplus.plus/send' 71 | data = { 72 | "token": PUSHPLUS_TOKEN, 73 | "title": "IP优选DNSPOD推送", 74 | "content": content, 75 | "template": "markdown", 76 | "channel": "wechat" 77 | } 78 | body = json.dumps(data).encode(encoding='utf-8') 79 | headers = {'Content-Type': 'application/json'} 80 | requests.post(url, data=body, headers=headers) 81 | 82 | 83 | if __name__ == '__main__': 84 | # 构造环境 85 | cloud = QcloudApiv3(SECRETID, SECRETKEY) 86 | 87 | # 获取DNS记录 88 | info = build_info(cloud) 89 | 90 | # 获取最新优选IP 91 | ip_addresses_str = get_cf_speed_test_ip() 92 | ip_addresses = ip_addresses_str.split(',') 93 | 94 | pushplus_content = [] 95 | # 遍历 IP 地址列表 96 | for index, ip_address in enumerate(ip_addresses): 97 | # 执行 DNS 变更 98 | dns = change_dns(cloud, info[index]["recordId"], ip_address) 99 | pushplus_content.append(dns) 100 | 101 | pushplus('\n'.join(pushplus_content)) 102 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | CfSpeedDns优选IP 7 | 55 | 56 | 57 |

58 |
59 | 60 |
61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 |
IP地址已发送已接收丢包率平均延迟下载速度测速时间
440.0%145.0159.97MB/s2025-06-02 14:37:39
440.0%141.0146.98MB/s2025-06-02 14:37:39
440.0%140.0101.01MB/s2025-06-02 12:53:27
440.0%140.093.3MB/s2025-06-02 12:53:27
440.0%142.020.36MB/s2025-06-02 13:53:52
440.0%144.018.21MB/s2025-06-02 13:53:52
440.0%142.018.14MB/s2025-06-02 13:53:52
440.0%145.018.0MB/s2025-06-02 13:08:53
440.0%144.012.2MB/s2025-06-02 13:53:52
440.0%138.011.47MB/s2025-06-02 14:08:45
196 | 197 | 227 | 230 | 240 | 241 | 242 | -------------------------------------------------------------------------------- /ipTop.html: -------------------------------------------------------------------------------- 1 | 104.18.17.254,172.64.145.248 -------------------------------------------------------------------------------- /ipTop10.html: -------------------------------------------------------------------------------- 1 | 104.18.17.254,172.64.145.248,104.17.110.108,104.16.57.94,198.41.206.218,162.159.241.149,104.18.111.18,104.18.27.10,104.21.224.101,104.19.32.193 -------------------------------------------------------------------------------- /qCloud.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Mail: tongdongdong@outlook.com 4 | # Reference: https://cloud.tencent.com/document/product/302/8517 5 | # QcloudApiv3 DNSPod 的 API 更新了 By github@z0z0r4 6 | 7 | import json 8 | from tencentcloud.common import credential 9 | from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException 10 | from tencentcloud.dnspod.v20210323 import dnspod_client, models 11 | 12 | 13 | class QcloudApiv3(): 14 | def __init__(self, SECRETID, SECRETKEY): 15 | self.SecretId = SECRETID 16 | self.secretKey = SECRETKEY 17 | self.cred = credential.Credential(SECRETID, SECRETKEY) 18 | 19 | def del_record(self, domain: str, record_id: int): 20 | client = dnspod_client.DnspodClient(self.cred, "") 21 | req_model = models.DeleteRecordRequest() 22 | params = { 23 | "Domain": domain, 24 | "RecordId": record_id 25 | } 26 | req_model.from_json_string(json.dumps(params)) 27 | 28 | resp = client.DeleteRecord(req_model) 29 | resp = json.loads(resp.to_json_string()) 30 | resp["code"] = 0 31 | resp["message"] = "None" 32 | return resp 33 | 34 | def get_record(self, domain: str, length: int, sub_domain: str, record_type: str): 35 | def format_record(record: dict): 36 | new_record = {} 37 | record["id"] = record['RecordId'] 38 | for key in record: 39 | new_record[key.lower()] = record[key] 40 | return new_record 41 | 42 | try: 43 | client = dnspod_client.DnspodClient(self.cred, "") 44 | 45 | req_model = models.DescribeRecordListRequest() 46 | params = { 47 | "Domain": domain, 48 | "Subdomain": sub_domain, 49 | "RecordType": record_type, 50 | "Limit": length 51 | } 52 | req_model.from_json_string(json.dumps(params)) 53 | 54 | resp = client.DescribeRecordList(req_model) 55 | resp = json.loads(resp.to_json_string()) 56 | temp_resp = {} 57 | temp_resp["code"] = 0 58 | temp_resp["data"] = {} 59 | temp_resp["data"]["records"] = [] 60 | for record in resp['RecordList']: 61 | temp_resp["data"]["records"].append(format_record(record)) 62 | temp_resp["data"]["domain"] = {} 63 | temp_resp["data"]["domain"]["grade"] = self.get_domain(domain)["DomainInfo"]["Grade"] # DP_Free 64 | return temp_resp 65 | except TencentCloudSDKException: 66 | # 构造空响应... 67 | temp_resp = {} 68 | temp_resp["code"] = 0 69 | temp_resp["data"] = {} 70 | temp_resp["data"]["records"] = [] 71 | temp_resp["data"]["domain"] = {} 72 | temp_resp["data"]["domain"]["grade"] = self.get_domain(domain)["DomainInfo"]["Grade"] # DP_Free 73 | return temp_resp 74 | 75 | def create_record(self, domain: str, sub_domain: str, value: int, record_type: str = "A", line: str = "默认", 76 | ttl: int = 600): 77 | client = dnspod_client.DnspodClient(self.cred, "") 78 | req = models.CreateRecordRequest() 79 | params = { 80 | "Domain": domain, 81 | "SubDomain": sub_domain, 82 | "RecordType": record_type, 83 | "RecordLine": line, 84 | "Value": value, 85 | "ttl": ttl 86 | } 87 | req.from_json_string(json.dumps(params)) 88 | 89 | # 返回的resp是一个CreateRecordResponse的实例,与请求对象对应 90 | resp = client.CreateRecord(req) 91 | resp = json.loads(resp.to_json_string()) 92 | resp["code"] = 0 93 | resp["message"] = "None" 94 | return resp 95 | 96 | def change_record(self, domain: str, record_id: int, sub_domain: str, value: str, record_type: str = "A", 97 | line: str = "默认", ttl: int = 600): 98 | client = dnspod_client.DnspodClient(self.cred, "") 99 | req = models.ModifyRecordRequest() 100 | params = { 101 | "Domain": domain, 102 | "SubDomain": sub_domain, 103 | "RecordType": record_type, 104 | "RecordLine": line, 105 | "Value": value, 106 | "TTL": ttl, 107 | "RecordId": record_id 108 | } 109 | req.from_json_string(json.dumps(params)) 110 | 111 | # 返回的resp是一个ChangeRecordResponse的实例,与请求对象对应 112 | resp = client.ModifyRecord(req) 113 | resp = json.loads(resp.to_json_string()) 114 | resp["code"] = 0 115 | resp["message"] = "None" 116 | return resp 117 | 118 | def get_domain(self, domain: str): 119 | client = dnspod_client.DnspodClient(self.cred, "") 120 | 121 | # 实例化一个请求对象,每个接口都会对应一个request对象 122 | req = models.DescribeDomainRequest() 123 | params = { 124 | "Domain": domain 125 | } 126 | req.from_json_string(json.dumps(params)) 127 | 128 | # 返回的resp是一个DescribeDomainResponse的实例,与请求对象对应 129 | resp = client.DescribeDomain(req) 130 | resp = json.loads(resp.to_json_string()) 131 | return resp 132 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.28.1 2 | tencentcloud-sdk-python==3.0.806 3 | --------------------------------------------------------------------------------