侧边栏壁纸
博主头像
いちごみるく

行动起来,活在当下

  • 累计撰写 70 篇文章
  • 累计创建 48 个标签
  • 累计收到 55 条评论

目 录CONTENT

文章目录

cloudflare批量自动删除所有DNS记录

本文提供 Cloudflare DNS 记录批量清理脚本:先获取有限分页快照与 JSON/BIND 备份,默认仅预览,确认目标后逐条删除,遇到失败立即停止。执行前需核对目标 Zone 和备份。

清空 DNS 会同时影响网站、邮件、验证记录等。这里的“全部”指执行时在指定 Zone 中列出的记录快照;运行期间应暂停其他 DNS 修改。不要把删除当作关闭代理或暂停站点的替代操作。

一、准备权限和目标

使用仅有目标 Zone 的 DNS Write 权限、且能读取该 Zone 信息和 DNS 列表的 API Token。凭据通过 CF_API_TOKEN 环境变量提供,不写入脚本、命令历史或备份。zone_id 与账户 ID 不是同一个值,脚本会再读取 Zone 名称并和你输入的完整域名比较。

安装依赖:

python -m pip install requests

脚本使用 per_page=100 作为便于检查的小批次,这不是 API 允许的最大值;参数范围以当前列表文档为准。Cloudflare DNS 列表 API

二、保存脚本

保存为 clear_dns.py。默认只读取和备份;加 --apply 后还要输入域名及记录数,才会删除。写操作失败立即停止,不自动重试,也不会继续打印“全部成功”。

import argparse
import json
import os
from pathlib import Path
from datetime import datetime, timezone
import uuid

import requests

API = 'https://api.cloudflare.com/client/v4'
PER_PAGE = 100


def api(session, method, path, **kwargs):
    response = session.request(method, API + path, timeout=(10, 30), **kwargs)
    response.raise_for_status()
    data = response.json()
    if data.get('success') is not True:
        raise RuntimeError(f'Cloudflare API 失败:{data.get("errors", [])}')
    return data


def snapshot(session, zone_id):
    path = f'/zones/{zone_id}/dns_records'
    first = api(session, 'GET', path, params={'page': 1, 'per_page': PER_PAGE})
    info = first['result_info']
    pages = int(info['total_pages'])
    expected = int(info['total_count'])
    records = list(first['result'])
    for page in range(2, pages + 1):
        data = api(session, 'GET', path, params={'page': page, 'per_page': PER_PAGE})
        if int(data['result_info']['total_count']) != expected:
            raise RuntimeError('DNS 数量在分页中改变,请暂停其他修改后重试')
        records.extend(data['result'])
    ids = [record['id'] for record in records]
    if len(ids) != len(set(ids)) or len(ids) != expected:
        raise RuntimeError('分页记录数量或唯一性检查失败,未执行删除')
    return records


def save_private(path, text):
    # 备份包含域名和源站信息;Unix 下限制权限,Windows 需配合目录 ACL。
    fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
    with os.fdopen(fd, 'w', encoding='utf-8') as handle:
        handle.write(text)


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--zone-id', required=True)
    parser.add_argument('--zone-name', required=True)
    parser.add_argument('--backup', default='dns-backups')
    parser.add_argument('--apply', action='store_true')
    args = parser.parse_args()
    token = os.environ.get('CF_API_TOKEN')
    if not token:
        raise RuntimeError('请先在当前进程设置 CF_API_TOKEN')

    with requests.Session() as session:
        session.headers['Authorization'] = f'Bearer {token}'
        zone = api(session, 'GET', f'/zones/{args.zone_id}')['result']
        name = zone['name']
        if name.lower() != args.zone_name.rstrip('.').lower():
            raise RuntimeError(f'Zone 名称不匹配:实际为 {name}')
        records = snapshot(session, args.zone_id)
        export = session.request('GET', API + f'/zones/{args.zone_id}/dns_records/export',
                                 timeout=(10, 30))
        export.raise_for_status()
        # API 错误不是 BIND 文件,不能把 JSON 错误响应当作可恢复备份。
        if 'json' in export.headers.get('Content-Type', '').lower() or not export.text.strip():
            raise RuntimeError('BIND 导出未返回有效文本,未执行删除')
        stamp = datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')
        folder = Path(args.backup) / f'{stamp}-{uuid.uuid4().hex[:8]}'
        folder.mkdir(parents=True, mode=0o700, exist_ok=False)
        save_private(folder / 'records.json', json.dumps(
            {'zone_id': args.zone_id, 'zone_name': name, 'records': records},
            ensure_ascii=False, indent=2))
        save_private(folder / 'records.bind', export.text)
        print(f'目标:{name},快照:{len(records)} 条,备份:{folder}')
        for record in records[:10]:
            print(record['type'], record['name'], record['content'])
        if not args.apply or not records:
            print('预览结束,未删除记录。')
            return
        confirmation = f'{name} {len(records)}'
        if input(f'如确认删除,输入 {confirmation!r}:').strip() != confirmation:
            raise RuntimeError('确认不匹配,未执行删除')
        # 确认等待期间也可能有人修改;快照变动则要求重新预览。
        current = snapshot(session, args.zone_id)
        if current != records:
            raise RuntimeError('记录已变动,请重新预览和备份')
        log = folder / 'deleted-ids.txt'
        for index, record in enumerate(records, start=1):
            result = api(session, 'DELETE',
                         f'/zones/{args.zone_id}/dns_records/{record["id"]}')['result']
            if result.get('id') != record['id']:
                raise RuntimeError('删除响应 ID 不匹配,已停止;请检查线上状态')
            # 只记录已经被 API 确認删除的 ID,便于中断后核对。
            fd = os.open(log, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600)
            with os.fdopen(fd, 'a', encoding='utf-8') as handle:
                handle.write(record['id'] + '\n')
            print(f'已删除 {index}/{len(records)}:{record["type"]} {record["name"]}')
        remaining = snapshot(session, args.zone_id)
        target_ids = {record['id'] for record in records}
        if target_ids & {record['id'] for record in remaining}:
            raise RuntimeError('复核仍有目标记录,请检查备份和删除日志')
        print(f'快照中的 {len(records)} 条已删除;当前 Zone 还有 {len(remaining)} 条记录。')


if __name__ == '__main__':
    main()

三、先预览,再执行

在当前终端交互式设置环境变量后,先运行:

python clear_dns.py --zone-id YOUR_ZONE_ID --zone-name example.com

核对备份中的 A、AAAA、CNAME、MX、TXT、SRV 等记录,再重新运行:

python clear_dns.py --zone-id YOUR_ZONE_ID --zone-name example.com --apply

第二次运行会重新备份和确认。脚本不会删除快照之后新增的记录;分页不是事务,也无法阻止其他人在删除期间修改,实际操作必须安排维护窗口。遇到 429、超时或 API 错误,依据备份、已删 ID 日志和线上列表判断完成到哪一步,再决定是否继续。不要假定超时代表删除没有发生。删除 API、Requests 超时说明

BIND 文件用于恢复 DNS 内容,JSON 用于核对 Cloudflare 字段。导入 BIND 不应被描述为逐字段恢复代理状态、备注和所有账户设置;导入后按 JSON 逐项核查,并测试网站与邮件。Cloudflare 导入与导出说明

批量处理思路参考《使用 SHELL 批量删除 Cloudflare 解析记录》。本文 Python 脚本的字段和调用方式以 Cloudflare 官方 API 文档为依据。

0

评论区