使用 Durable Objects 实现使用特定地区的CF出口节点

CF的机制

正常情况下

正常情况下, CF会根据访问者所在位置来处理用户的请求, 同时, 访问目标服务器也在该节点发起.

比如当你在CN发起访问, CF的入口节点和出口节点都会在CN. 而如果使用了一个TW的代理, 那么CF的入口节点和出口节点都会在TW.

即使使用了优选IP, 出口节点依然不会变, 即出口节点只和访问者的位置有关.

例子

比如我有一台US的服务器, 由于线路不咋样延迟太高, 访问速度和下载速度很慢. 于是我套了CF, 但是由于CF没有国内的节点, 所以速度依然不理想. 这时可以考虑使用优选IP的方式, 但是这种方式需要在客户端都进行优选的相关操作, 比如设置hosts, 很明显, 这种方式不太可行, 当换了个网络环境后, 临时需要用一下的时候就尴尬了. 总不能写个脚本自动设置啥的.

这时候聪明的我想到了在CF的基础上再套一层CDN, 正好AliYun的ESA有免费套餐. 套上了ESA之后, 我发现下载速度还不错, 甚至可以跑到20MB/s+, 但是一开始的速度比较慢, 也就是一次下载前期的速度增长很慢. 如果是静态文件, 那么当被缓存后, 速度还可以接受.

在页面上加了获取加载速度的相关代码, 发现光一个静态页面的加载速度都基本需要几秒的水平.

于是去搜索找到了CF的机制.

最终确定是CF的出口节点问题, 它是不可控的.

两套方案

  • 将域名解析到8.0.0.0/8, 然后在一个worker中加入自定义路由.
  • 使用CF新出的Durable Objects

最终方案

使用方案二,Durable Objects可以直接指定出口的节点. 就是需要在本地敲代码, 然后用wrangler上传, 麻烦了点.

代码基于Global Ping进行修改.

旧写法

compatibility_date = "2025-07-10"

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { DurableObject } from 'cloudflare:workers'

export interface Env {
  FETCHER: DurableObjectNamespace<Fetcher>
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const id = env.FETCHER.idFromName(`fetcher-wnam`)
    const fetcher = env.FETCHER.get(id, { locationHint: 'wnam' })
    const newUrl = request.url.replace('worker绑定域名', '源服务域名')
    console.info(`Forwarding request to URL: ${newUrl}`)
    const result = await fetcher.request(newUrl, request)
    return result
  }
}

export class Fetcher extends DurableObject {
  constructor(ctx: DurableObjectState, env: Env) {
    super(ctx, env)
  }

  async request(url: string,request: Request): Promise<Response> {
    return await fetch(url, new Request(request))
  }
}

其中 wnam 是CF的节点hint, 可以在Supported locations查看, 选一个离服务器近的即可.

新写法

"compatibility_date": "2025-09-27",

  1. 创建项目
1
npm create cloudflare@latest -- fetcher

然后选择Typescript

  1. 修改配置和代码 wrangler.jsonc 中修改name new_sqlite_classes durable_objects
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { DurableObject } from "cloudflare:workers";

export interface Env {
  FETCHER: DurableObjectNamespace<Fetcher>
}

export class Fetcher extends DurableObject<Env> {
	constructor(ctx: DurableObjectState, env: Env) {
		super(ctx, env);
	}
	async request(url: string,request: Request): Promise<Response> {
        return await fetch(url, new Request(request))
    }
}

export default {
	async fetch(request, env, ctx): Promise<Response> {
		const id = env.FETCHER.idFromName(`fetcher-wnam`)
		const fetcher = env.FETCHER.get(id, { locationHint: 'wnam' })
		const newUrl = request.url.replace('<worker绑定域名>', '<源服务域名>')
		console.info(`Forwarding request to URL: ${newUrl}`)
		const result = await fetcher.request(newUrl, request)
		return result
	},
} satisfies ExportedHandler<Env>;

记得替换 <worker绑定域名><源服务域名>

  1. npm run deploy部署上去, 然后给新的worker绑定域名, 和<worker绑定域名>一致即可
Licensed under CC BY-NC-SA 4.0
记录平时瞎折腾遇到的各种问题, 方便查找
使用 Hugo 构建
主题 StackJimmy 设计