如何使用Cloudflare实现DDNS服务

使用 Cloudflare Workers 和 API 实现高性能、安全的 DDNS 服务

动态 DNS(DDNS)服务允许你将一个域名指向一个动态 IP 地址。这对于家庭网络、小型企业或任何需要从外部访问但没有静态 IP 地址的设备非常有用。传统的 DDNS 服务提供商可能存在速度、可靠性或安全性方面的问题。Cloudflare 作为全球领先的 CDN 和 DNS 提供商,提供了一个强大且免费的平台,可以用来构建我们自己的高性能、安全的 DDNS 服务。

本文将详细介绍如何利用 Cloudflare Workers 和 Cloudflare API 来实现一个自定义的 DDNS 解决方案。我们将一步步地构建一个脚本,它可以自动检测你的公网 IP 地址变化,并更新 Cloudflare 上的 DNS 记录。

为什么选择 Cloudflare 实现 DDNS?

  • 性能与可靠性: Cloudflare 拥有全球分布的边缘服务器网络,这意味着你的 DNS 解析将非常快速和可靠,无论你的用户位于何处。
  • 安全性: Cloudflare 提供了强大的 DDoS 防护和其他安全功能,可以保护你的域名免受攻击。
  • 免费套餐: Cloudflare 的免费套餐已经足够满足大多数个人和小型项目的 DDNS 需求。
  • 灵活性: 使用 Cloudflare Workers,你可以完全控制你的 DDNS 逻辑,并根据需要进行自定义。
  • API支持: Cloudflare提供了完善的API,可以对DNS记录进行增删改查。

前期准备

在开始之前,你需要准备以下事项:

  1. 一个 Cloudflare 帐户: 如果你还没有,请前往 Cloudflare 官网 注册一个免费帐户。
  2. 一个域名: 你需要拥有一个域名,并将其 DNS 解析托管到 Cloudflare。如果你还没有域名,可以从任何域名注册商处购买。
  3. Cloudflare API Token:
    • 登录 Cloudflare 仪表板。
    • 点击右上角的用户头像,选择 "My Profile"。
    • 在左侧菜单中选择 "API Tokens"。
    • 点击 "Create Token" 按钮。
    • 选择 "Edit zone DNS" 模板。
    • 在 "Zone Resources" 部分,选择 "Include" -> "Specific zone",然后选择你的域名。
    • 点击 "Continue to summary",然后点击 "Create Token"。
    • 重要提示: 复制并安全地保存你的 API Token,因为它只显示一次。
  4. Node.js 和 npm(可选,但强烈推荐): 这将使你能够更轻松地开发和部署 Cloudflare Workers。如果你没有安装,请前往 Node.js 官网 下载并安装。
  5. 文本编辑器或 IDE: 用于编写和编辑代码。推荐使用 VS Code。
  6. wrangler CLI: Cloudflare提供的命令行工具,用于部署和管理workers。

步骤 1:创建 Cloudflare Worker

Cloudflare Workers 是运行在 Cloudflare 边缘网络上的无服务器函数。我们将使用 Worker 来处理 IP 地址检测和 DNS 更新逻辑。

  1. 安装 Wrangler CLI(如果尚未安装):

    bash
    npm install -g wrangler

  2. 使用 Wrangler 登录 Cloudflare:

    bash
    wrangler login

    按照提示在浏览器中授权 Wrangler 访问你的 Cloudflare 帐户。

  3. 创建 Worker 项目:

    bash
    wrangler generate ddns-worker

    这将在当前目录下创建一个名为 ddns-worker 的新文件夹,其中包含一个基本的 Worker 项目结构。

  4. 编辑 index.js 文件:

    打开 ddns-worker/index.js 文件,并将其内容替换为以下代码(稍后我们将详细解释每个部分):

```javascript
addEventListener('scheduled', event => {
event.waitUntil(handleScheduled(event));
});

async function handleScheduled(event) {
const CF_API_TOKEN = 'YOUR_CLOUDFLARE_API_TOKEN'; // 替换为你的 API Token
const ZONE_ID = 'YOUR_ZONE_ID'; // 替换为你的 Zone ID
const DNS_RECORD_NAME = 'yoursubdomain.yourdomain.com'; // 替换为你的 DDNS 记录名称,例如 ddns.example.com
const DNS_RECORD_TYPE = 'A';

try {
const currentIp = await getCurrentIp();
const cloudflareIp = await getCloudflareIp(CF_API_TOKEN, ZONE_ID, DNS_RECORD_NAME,DNS_RECORD_TYPE);

if (currentIp !== cloudflareIp) {
  await updateCloudflareIp(CF_API_TOKEN, ZONE_ID, DNS_RECORD_NAME, currentIp,DNS_RECORD_TYPE);
  console.log(`IP address updated: ${cloudflareIp} -> ${currentIp}`);
} else {
  console.log('IP address is up to date.');
}

} catch (error) {
console.error('Error:', error);
}
}

async function getCurrentIp() {
const response = await fetch('https://api.ipify.org');
return response.text();
}

async function getCloudflareIp(apiToken, zoneId, recordName,recordType) {
const response = await fetch(
https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records?name=${recordName}&type=${recordType},
{
headers: {
'Authorization': Bearer ${apiToken},
'Content-Type': 'application/json',
},
}
);

const data = await response.json();
if (data.success && data.result.length > 0) {
return data.result[0].content;
} else {
//如果DNS记录不存在,则创建
const createResponse = await fetch(
https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records,
{
method: 'POST',
headers: {
'Authorization': Bearer ${apiToken},
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: recordType,
name: recordName,
content: await getCurrentIp(),
ttl: 1, // 设置TTL为1,表示自动
proxied: true, // 启用 Cloudflare 代理 (可选)
}),
}
);
const createData = await createResponse.json();
if(createData.success)
{
return createData.result.content;
}else{
throw new Error(Failed to create DNS record: ${createData.errors[0]?.message});
}
}
}

async function updateCloudflareIp(apiToken, zoneId, recordName, newIp,recordType) {
//先获取记录ID
const recordInfo = await fetch(
https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records?name=${recordName}&type=${recordType},
{
headers: {
'Authorization': Bearer ${apiToken},
'Content-Type': 'application/json',
},
}
);
const recordData = await recordInfo.json();
const recordId = recordData.result[0].id;

const response = await fetch(
https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records/${recordId},
{
method: 'PUT',
headers: {
'Authorization': Bearer ${apiToken},
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: recordType,
name: recordName,
content: newIp,
ttl: 1, // 设置TTL为1,表示自动
proxied: true, // 启用 Cloudflare 代理 (可选)
}),
}
);

const data = await response.json();
if (!data.success) {
throw new Error(Failed to update DNS record: ${data.errors[0]?.message});
}
}
``
5. **配置
wrangler.toml` 文件**

打开 `wrangler.toml` 文件,进行如下修改:
*   `name`:设置 Worker 的名称(例如 `ddns-worker`)。
*   `zone_id`:替换为你的域名的 Zone ID。你可以在 Cloudflare 仪表板的域名概览页面找到 Zone ID。
*   `route`:  如果要将 Worker 部署到特定路由(例如 `example.com/ddns/*`),请在此处配置。如果希望 Worker 在定时任务触发时运行,可以删除或注释掉 `route` 这一行。
*   `triggers`: 添加定时触发器配置。如下示例表示每5分钟触发一次。
```toml
name = "ddns-worker"
type = "javascript"
account_id = "" #可以不填
zone_id = "YOUR_ZONE_ID" #替换
workers_dev = true
compatibility_date = "2023-11-21"

[triggers]
crons = ["*/5 * * * *"]
```

步骤 2:代码详解

让我们逐块分析 index.js 中的代码:

  • 事件监听器:

    javascript
    addEventListener('scheduled', event => {
    event.waitUntil(handleScheduled(event));
    });

    这段代码监听 scheduled 事件,该事件由 wrangler.toml 中配置的 Cron 表达式触发。当事件触发时,它会调用 handleScheduled 函数,并使用 event.waitUntil 确保函数在 Worker 终止之前完成执行。

  • handleScheduled 函数:

    ```javascript
    async function handleScheduled(event) {
    const CF_API_TOKEN = 'YOUR_CLOUDFLARE_API_TOKEN'; // 替换为你的 API Token
    const ZONE_ID = 'YOUR_ZONE_ID'; // 替换为你的 Zone ID
    const DNS_RECORD_NAME = 'yoursubdomain.yourdomain.com'; // 替换为你的 DDNS 记录名称,例如 ddns.example.com
    const DNS_RECORD_TYPE = 'A';
    try {
    const currentIp = await getCurrentIp();
    const cloudflareIp = await getCloudflareIp(CF_API_TOKEN, ZONE_ID, DNS_RECORD_NAME,DNS_RECORD_TYPE);

    if (currentIp !== cloudflareIp) {
      await updateCloudflareIp(CF_API_TOKEN, ZONE_ID, DNS_RECORD_NAME, currentIp,DNS_RECORD_TYPE);
      console.log(`IP address updated: ${cloudflareIp} -> ${currentIp}`);
    } else {
      console.log('IP address is up to date.');
    }
    

    } catch (error) {
    console.error('Error:', error);
    }
    }
    ```

    这是 DDNS 逻辑的核心部分。它首先定义了几个常量:
    * CF_API_TOKEN:你的 Cloudflare API Token。
    * ZONE_ID:你的域名的 Zone ID。
    * DNS_RECORD_NAME:你想要用于 DDNS 的 DNS 记录的完整名称(例如 ddns.example.com)。
    * DNS_RECORD_TYPE: DNS记录类型,A记录.

    然后,它按顺序执行以下操作:
    1. 调用 getCurrentIp 函数获取当前的公网 IP 地址。
    2. 调用 getCloudflareIp 函数获取 Cloudflare 上该 DNS 记录的当前 IP 地址。
    3. 比较两个 IP 地址。如果它们不同,则调用 updateCloudflareIp 函数更新 Cloudflare 上的 DNS 记录,并记录一条日志消息。如果它们相同,则记录一条 IP 地址是最新的消息。
    4. 使用 try...catch 块捕获任何可能发生的错误,并将错误信息记录到控制台。

  • getCurrentIp 函数:

    javascript
    async function getCurrentIp() {
    const response = await fetch('https://api.ipify.org');
    return response.text();
    }

    这个函数使用 fetch API 向 https://api.ipify.org 发送一个请求,该服务会返回你的公网 IP 地址。然后,它从响应中提取文本内容(即 IP 地址)并返回。

  • getCloudflareIp 函数:

    ``javascript
    async function getCloudflareIp(apiToken, zoneId, recordName,recordType) {
    const response = await fetch(
    https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records?name=${recordName}&type=${recordType},
    {
    headers: {
    'Authorization':
    Bearer ${apiToken}`,
    'Content-Type': 'application/json',
    },
    }
    );

    const data = await response.json();
    if (data.success && data.result.length > 0) {
    return data.result[0].content;
    } else {
    //如果DNS记录不存在,则创建
    const createResponse = await fetch(
    https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records,
    {
    method: 'POST',
    headers: {
    'Authorization': Bearer ${apiToken},
    'Content-Type': 'application/json',
    },
    body: JSON.stringify({
    type: recordType,
    name: recordName,
    content: await getCurrentIp(),
    ttl: 1, // 设置TTL为1,表示自动
    proxied: true, // 启用 Cloudflare 代理 (可选)
    }),
    }
    );
    const createData = await createResponse.json();
    if(createData.success)
    {
    return createData.result.content;
    }else{
    throw new Error(Failed to create DNS record: ${createData.errors[0]?.message});
    }
    }
    }

    ```

    这个函数使用 Cloudflare API 获取指定 DNS 记录的当前 IP 地址。
    * 它向 Cloudflare API 发送一个 GET 请求,并附带 API Token、Zone ID 和 DNS 记录名称。
    * 它解析 API 响应的 JSON 数据。
    * 如果响应成功且存在对应的DNS记录,它会返回该记录的 content 字段(即 IP 地址)。
    * 如果响应成功,但是记录不存在,则创建一条新的DNS记录,并返回IP.
    * 如果失败,它会抛出一个错误。

  • updateCloudflareIp 函数:

    ``javascript
    async function updateCloudflareIp(apiToken, zoneId, recordName, newIp,recordType) {
    //先获取记录ID
    const recordInfo = await fetch(
    https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records?name=${recordName}&type=${recordType},
    {
    headers: {
    'Authorization':
    Bearer ${apiToken}`,
    'Content-Type': 'application/json',
    },
    }
    );
    const recordData = await recordInfo.json();
    const recordId = recordData.result[0].id;

    const response = await fetch(
    https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records/${recordId},
    {
    method: 'PUT',
    headers: {
    'Authorization': Bearer ${apiToken},
    'Content-Type': 'application/json',
    },
    body: JSON.stringify({
    type: recordType,
    name: recordName,
    content: newIp,
    ttl: 1, // 设置TTL为1,表示自动
    proxied: true, // 启用 Cloudflare 代理 (可选)
    }),
    }
    );

    const data = await response.json();
    if (!data.success) {
    throw new Error(Failed to update DNS record: ${data.errors[0]?.message});
    }
    }

    ```

    这个函数使用 Cloudflare API 更新指定 DNS 记录的 IP 地址。
    * 它向 Cloudflare API 发送一个 PUT 请求,并附带 API Token、Zone ID、DNS 记录名称和新的 IP 地址。
    * 先通过GET请求获取DNS记录的ID,然后通过ID进行PUT更新操作.
    * 它将请求体设置为一个 JSON 对象,其中包含要更新的 DNS 记录的字段(typenamecontentttlproxied)。
    * 它解析 API 响应的 JSON 数据。如果响应不成功,它会抛出一个错误。
    * ttl: 1 表示使用 Cloudflare 的自动 TTL,这将根据 Cloudflare 的建议自动设置 TTL 值。
    * proxied: true 表示启用 Cloudflare 代理,这将隐藏你的源服务器 IP 地址,并提供 DDoS 防护和其他安全功能。如果你不希望使用 Cloudflare 代理,可以将此值设置为 false

步骤 3:部署 Cloudflare Worker

完成代码编写和配置后,我们可以使用 Wrangler CLI 部署 Worker:

bash
wrangler publish

这将把你的 Worker 代码上传到 Cloudflare,并根据 wrangler.toml 中的配置进行部署。

部署完成后,你的 DDNS 服务就已经开始运行了!它将按照你设置的 Cron 表达式(例如每 5 分钟)自动检查你的公网 IP 地址,并在发生变化时更新 Cloudflare 上的 DNS 记录。

步骤 4: 测试

  1. 手动触发: 你可以使用以下命令手动触发 Worker 执行,以进行测试:

    bash
    wrangler dev

    这将模拟一个定时事件,并运行你的 Worker。你可以在控制台中查看输出,以确认 IP 地址是否正确检测和更新。

  2. 更改你的公网 IP 地址: 最直接的方法是重启你的路由器或调制解调器,这通常会分配一个新的公网 IP 地址。

  3. 检查 DNS 记录: 你可以使用 dig 命令(Linux 或 macOS)或 nslookup 命令(Windows)来查询你的 DDNS 记录,并确认它是否指向了正确的 IP 地址:

    bash
    dig yoursubdomain.yourdomain.com

    bash
    nslookup yoursubdomain.yourdomain.com

    yoursubdomain.yourdomain.com 替换为你的 DDNS 记录名称。

高级配置和优化

  • 错误处理和通知: 你可以在代码中添加更健壮的错误处理逻辑,例如在更新 DNS 记录失败时发送电子邮件或 Slack 通知。
  • 日志记录: 你可以使用 Cloudflare Workers 的日志记录功能来记录更详细的事件信息,以便进行故障排除和监控。
  • 支持多个 DNS 记录: 你可以修改代码以支持同时更新多个 DNS 记录(例如,A 记录和 AAAA 记录)。
  • 自定义 IP 检测服务: 如果你不希望使用 https://api.ipify.org,你可以替换为其他 IP 检测服务,或者编写你自己的 IP 检测逻辑。
  • IPv6 支持: 修改 getCurrentIp 使用支持 IPv6 的服务 (例如 https://api64.ipify.org),以及在创建/更新 Cloudflare 记录时,如果 DNS_RECORD_TYPE 设置为 AAAA,进行相应处理。
  • 速率限制: Cloudflare API 有速率限制。如果你的更新频率很高,或者你需要管理大量的 DNS 记录,你可能需要考虑实现请求队列或使用其他技术来避免触发速率限制。

进阶:不同场景下的应用与调整

  • 家庭网络:

    • 路由器集成: 一些路由器支持自定义脚本。你可以将上述逻辑(或其部分)移植到路由器上运行的脚本中,这样就不需要单独的设备来运行 Worker。
    • NAS 集成: 如果你有一个 NAS(网络附加存储)设备,例如群晖或 QNAP,它们通常有内置的 DDNS 客户端或允许安装第三方应用程序。你可以寻找与 Cloudflare API 兼容的 DDNS 客户端,或者编写一个在 NAS 上运行的脚本。
  • 小型企业:

    • 多个子域名: 如果你有多个需要 DDNS 的服务(例如,邮件服务器、Web 服务器、VPN 服务器),你可以为每个服务创建一个单独的子域名,并在 Worker 代码中同时更新它们。
    • 负载均衡: 如果你有多个服务器提供相同的服务,你可以使用 Cloudflare 的负载均衡功能,并将 DDNS 记录指向负载均衡器。
  • 云服务器:

    • 开机自启动: 如果你在云服务器上运行 DDNS 客户端,你可以将其配置为开机自启动,以确保即使服务器重启也能自动更新 DNS 记录。
    • 容器化: 你可以将 DDNS 客户端打包成一个 Docker 容器,这样可以更轻松地部署和管理。
  • 使用场景举例:

    1. 远程访问家庭网络: 你可以在家中的树莓派或其他设备上运行 DDNS 客户端,然后使用域名从任何地方访问你的家庭网络。
    2. 搭建个人网站: 你可以在家中的电脑上搭建一个网站,并使用 DDNS 将域名指向你的电脑,这样就可以通过域名访问你的网站。
    3. 远程桌面连接: 你可以使用 DDNS 远程连接到家中的电脑,进行文件传输或远程控制。
    4. 游戏服务器: 你可以在家中的电脑上搭建一个游戏服务器,并使用 DDNS 让你的朋友可以通过域名加入游戏。

安全卫士:加固你的 DDNS 服务

  • API Token 权限最小化: 确保你创建的 API Token 只具有编辑 DNS 记录的权限,而没有其他不必要的权限。
  • 代码审查: 定期审查你的 Worker 代码,确保没有安全漏洞。
  • 监控和告警: 监控你的 DDNS 服务的运行状态,并在出现异常情况时及时收到通知。
  • 使用 Cloudflare 的安全功能: 充分利用 Cloudflare 提供的 DDoS 防护、防火墙、WAF(Web 应用程序防火墙)等安全功能,保护你的域名和服务器免受攻击。
  • 避免在代码中硬编码敏感信息: 使用 Wrangler secrets 来存储 API Token,而不是直接写在代码中。

更上一层楼:其他 DDNS 实现方式

虽然本文重点介绍了使用 Cloudflare Workers 的方法,但还有其他一些实现 DDNS 的方式:

  • 使用现有的 DDNS 客户端: 许多操作系统和路由器都内置了 DDNS 客户端,或者你可以安装第三方 DDNS 客户端。这些客户端通常支持各种 DDNS 服务提供商,包括 Cloudflare。
  • 使用 Shell 脚本: 你可以使用 Shell 脚本(例如 Bash 或 Python)来实现 DDNS 逻辑,并通过 Cron 或其他任务调度程序定期运行它。
  • 使用第三方 DDNS 服务: 有许多免费和付费的 DDNS 服务提供商,它们提供易于使用的客户端和 Web 界面。

展望未来

Cloudflare Workers 和 API 提供了一个强大而灵活的平台,可以用来构建各种自定义的应用程序,DDNS 只是其中之一。随着无服务器计算的不断发展,我们可以期待看到更多基于 Cloudflare Workers 的创新应用。 这种方法不仅提供了高性能和安全性,还赋予了我们完全的控制权和定制能力。无论你是个人用户还是企业用户,都可以利用 Cloudflare 的强大功能来构建满足你特定需求的 DDNS 解决方案。

独辟蹊径

通过本文详尽的步骤和代码解析,你应该已经掌握了如何使用 Cloudflare Workers 和 API 构建自己的 DDNS 服务。这种方法相对于传统的 DDNS 服务提供商,具有更高的性能、可靠性、安全性和灵活性。你可以根据自己的需求定制代码,并充分利用 Cloudflare 提供的各种功能。希望这篇文章能帮助你更好地理解和应用 Cloudflare Workers,开启你的无服务器计算之旅。

THE END