收起左侧

使用 Cloudflare Worker 实现 IPv4/IPv6 智能访问 NAS

30
回复
1715
查看
[ 复制链接 ]

3

主题

15

回帖

0

牛值

江湖小虾

2025-7-27 01:47:42 显示全部楼层 阅读模式

背景

很多 NAS 玩家都遇到了一个问题:家庭宽带只有公网 IPv6,没有公网 IPv4。这导致在外网通过 IPv4 访问家中的 NAS 会很麻烦。

之前受到论坛帖子的启发,我尝试了 Cloudflare Tunnel,但免费版的线路速度不理想。于是,我找到了一个替代方案:结合使用 Cloudflare Worker 和 FNOS 自带的 FN Connect 中继服务。

这个方案可以实现以下目标:

  • 统一访问入口:所有用户都通过同一个域名访问 NAS。
  • IPv6 访客:直接连接到家庭宽带的 IPv6 地址,实现全速访问。(重定向到 NAS 的公网 IPv6 地址(DDNS域名)。)
  • IPv4 访客:自动通过 FNOS 官方中继服务器访问,保证兼容性。(重定向到 FNOS 的官方中继服务器地址。)
  • 成本:免费。
  • 从而实现 IPv6 直连、IPv4 中继的智能分流。

准备工作 (Prerequisites)

在开始之前,请确保你已经完成以下设置:

  1. Cloudflare 域名:拥有一个托管在 Cloudflare 的域名。
  2. IPv6 配置
    • FNOS 已开启 IPv6。
    • 路由器已设置好端口转发(公网 80/443 -> NAS 80/443)。
  3. DDNS
    • FNOS 已开启 DDNS (例如使用 DDNS-GO)。
    • 你的域名 (yourdomain.com) 和一个子域名 (www.yourdomain.com) 都已通过 DDNS 解析到你的公网 IPv6 地址。
  4. 反向代理
    • 已在 NAS 上部署 Nginx Proxy Manager (NPM) 或其他反代工具。
    • 已成功配置 SSL 证书,可以通过 https://www.yourdomain.com 访问 FNOS。

核心要求:在进行下一步之前,请确保你的 IPv6 直连已经是完整、可用且带有 SSL 加密的状态。
拥有一个由Cloudflare托管的域名 (例如 yourdomain.com)。
FNOS已开启IPv6,并且您已经在路由器上设置好了端口转发 (公网的443端口转发到NAS的443端口,80端口转发到NAS的80端口)。
FNOS已开启DDNS,例如使用DDNS-GO,将您的根域名(yourdomain.com)和www子域名(www.yourdomain.com)都动态解析到您家宽带的公网IPv6地址(www子域名做CNAME解析到根域名也是可以的)。
(最关键!) 您已经在NAS上,通过Docker或其他方式,部署好了Nginx Proxy Manager (NPM) 或其他反向代理工具。 并且,您已经成功配置了它,实现了可以通过 https://www.yourdomain.com 这样带SSL安全证书的域名,来访问您FNOS的Web服务。
一句话总结:请确保您的IPv6直连访问已经是完美的、带SSL加密的状态。 如果这一步还没完成,请先在论坛里搜索相关教程。

操作步骤

1. 配置 Cloudflare DNS

我们需要在 Cloudflare DNS 中添加一条“诱饵”记录来触发 Worker 脚本。

  1. 登录 Cloudflare,进入你的域名 DNS 设置。
  2. 添加一条新的 AAAA 记录:
    • 类型 (Type): AAAA
    • 名称 (Name): nas (或其他你喜欢的名字,这将是你的统一访问入口)
    • IPv6 地址 (Content): 100:: (这是一个特殊的保留地址,照填即可)
    • 代理状态 (Proxy status): 已代理 (橙色云朵)
    • TTL: 自动
      屏幕截图2025-07-27013436.png

这条记录本身不会指向任何地方,但它能确保所有访问 nas.yourdomain.com 的流量都经过 Cloudflare,从而让我们的 Worker 脚本有机会执行。

2. 创建并配置 Cloudflare Worker

这是整个方案的核心。

  1. 在 Cloudflare 主菜单,进入 Workers路由

    屏幕截图2025-07-27013503.png

  2. 点击 创建应用程序 -> 创建 Worker

    屏幕截图2025-07-27013550.png

  3. 直接点击 部署,然后 编辑代码

    屏幕截图2025-07-27013618.png

  4. 删除编辑器里的所有默认代码,粘贴以下脚本(选择从hello word开始,点击开始使用后直接创建work,,随后在右上角就能看到编辑代码这个内容):

export default {
  async fetch(request, env, ctx) {
    // --- 用户配置 ---
    // 1. IPv6 目标地址 (你配置好 SSL 的反代域名)
    const IPV6_TARGET = "https://www.yourdomain.com";

    // 2. IPv4 目标地址 (FNOS 官方中继)
    const IPV4_LOGIN_TARGET = "https://你的FNConnectID.fnos.net"; // 用于 App 登录和管理
    const IPV4_SHARE_TARGET = "https://s.fnnas.net"; // 用于文件分享

    // --- 核心逻辑 (无需修改) ---
    const url = new URL(request.url);
    const clientIP = request.headers.get("CF-Connecting-IP");
    const originalPathAndQuery = url.pathname + url.search;

    // 判断是否为 IPv6 访客
    if (clientIP && clientIP.includes(":")) {
      return Response.redirect(IPV6_TARGET + originalPathAndQuery, 302);
    }

    // IPv4 访客:判断是否为分享链接
    if (url.pathname.startsWith('/s/')) {
      return Response.redirect(IPV4_SHARE_TARGET + originalPathAndQuery, 302);
    } else {
      return Response.redirect(IPV4_LOGIN_TARGET + originalPathAndQuery, 302);
    }
  },
};

重要: 请务必将脚本中的 yourdomain.com你的FNConnectID 替换为你自己的真实信息。

  1. 点击 保存并部署

3. 绑定 Worker 路由

让 Worker 在我们设定的 nas 子域名上生效。

  1. 在 Worker 的管理页面,点击 触发器 (Triggers) 标签页。
  2. 路由 (Routes) 部分,点击 添加路由 (Add route)
    • 路由 (Route): nas.yourdomain.com/* (确保 nas 和你之前 DNS 设置的名称一致,/* 通配符必须有)

    • 区域 (Zone): 选择 yourdomain.com

      屏幕截图2025-07-27014208.png

  3. 点击 添加路由

4. (可选) 统一 FNOS 分享链接

为了让体验更统一,可以修改 FNOS 的默认分享链接。

  1. 登录 FNOS 后台。
  2. 进入 远程访问 -> 外链分享 -> 设置
  3. 选择 自定义链接,填入你的统一入口地址:https://nas.yourdomain.com
  4. 点击 确认

完成

现在,你可以将 https://nas.yourdomain.com 作为你的唯一入口地址了。

  • IPv6 用户: 享受直连速度,共享的文件也会通过ipv6直接访问你的域名。
  • IPv4 用户: 通过官方中继稳定访问,共享文件通过飞牛的中间访问(可购买飞牛服务提高中继速度)。

希望这个教程对你有帮助。如果有任何问题,欢迎在下面留言讨论。


收藏
送赞 1
分享

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

6

主题

80

回帖

0

牛值

fnOS系统内测组

2025-7-29 11:32:08 显示全部楼层

码了,有空试试

我修改了一下代码逻辑 现在做了个ipv6和v4的联通性check,并且根据浏览器和app的agent的不同,做了不同的分流  详情 回复
2025-7-29 15:49

3

主题

15

回帖

0

牛值

江湖小虾

2025-7-29 15:49:00 楼主 显示全部楼层

我修改了一下代码逻辑
现在做了个ipv6和v4的联通性check,并且根据浏览器和app的agent的不同,做了不同的分流

3

主题

15

回帖

0

牛值

江湖小虾

2025-7-29 16:41:56 楼主 显示全部楼层
// === 目标地址配置区 ===
// 建议将这些值配置到Cloudflare Worker的环境变量中,也可以直接复制到worker代码中 (Settings > Variables)
const IPV6_TARGET = "https://www.yourdomain.com";
const IPV4_LOGIN_TARGET = "https://你的飞牛ID.fnos.net";
const IPV4_SHARE_TARGET = "https://s.fnnas.net";

// === 探测配置 ===
const PROBE_FILE = "/favicon.ico";
const TIMEOUT = 2500;

// === 浏览器识别关键词 ===
const BROWSER_UA_KEYWORDS = ['Mozilla', 'Chrome', 'Firefox', 'Safari', 'Edge', 'Opera'];

// 辅助函数:根据路径获取IPv4目标地址
function getIPv4TargetByPath(pathname) {
  if (pathname.startsWith('/s/')) {
    return IPV4_SHARE_TARGET;
  }
  return IPV4_LOGIN_TARGET;
}

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    const clientIP = request.headers.get("CF-Connecting-IP");
    const userAgent = request.headers.get("User-Agent") || "";
    const originalPathAndQuery = url.pathname + url.search;

    // 【关键修复】定义一个禁止缓存的响应头对象,用于所有动态响应
    const noCacheHeaders = new Headers({
      'Cache-Control': 'private, no-store, no-cache, must-revalidate',
    });

    const isBrowser = BROWSER_UA_KEYWORDS.some(keyword => userAgent.includes(keyword));

    if (isBrowser) {
      // 对于浏览器,返回带有JS探测的HTML页面
      const html = `
      <!DOCTYPE html>
      <html>
      <head>
        <title>智能路径分析中...</title>
        <meta charset="utf-8">
        <noscript>
          <meta http-equiv="refresh" content="0;url=${IPV4_LOGIN_TARGET}${originalPathAndQuery}">
        </noscript>
        <style>
          body { font-family: sans-serif; display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100vh; margin: 0; background-color: #f0f2f5; color: #555; }
          .loader { border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin-bottom: 20px; }
          @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
        </style>
      </head>
      <body>
        <div class="loader"></div>
        <p>正在为您选择最佳访问路径...</p>
        <script>
          (async function() {
            const ipv6Url = "${IPV6_TARGET}";
            const ipv4LoginUrl = "${IPV4_LOGIN_TARGET}";
            const ipv4ShareUrl = "${IPV4_SHARE_TARGET}";
            const originalPath = "${originalPathAndQuery}";
  
            function checkIPv6() {
              return new Promise(resolve => {
                const controller = new AbortController();
                const timeoutId = setTimeout(() => { controller.abort(); resolve(false); }, ${TIMEOUT});
                const probeUrl = ipv6Url + "${PROBE_FILE}" + "?_t=" + Date.now();
                fetch(probeUrl, { method: 'HEAD', signal: controller.signal, mode: 'no-cors' })
                  .then(() => { clearTimeout(timeoutId); resolve(true); })
                  .catch(() => { clearTimeout(timeoutId); resolve(false); });
              });
            }

            const isIPv6Available = await checkIPv6();
            let finalUrl = "";

            if (isIPv6Available) {
              finalUrl = ipv6Url + originalPath;
            } else {
              if (originalPath.startsWith('/s/')) {
                finalUrl = ipv4ShareUrl + originalPath;
              } else {
                finalUrl = ipv4LoginUrl + originalPath;
              }
            }
            window.location.replace(finalUrl);
          })();
        </script>
      </body>
      </html>
      `;
  
      // 在返回HTML时,加入禁止缓存的头
      noCacheHeaders.set('Content-Type', 'text/html;charset=UTF-8');
      return new Response(html, { headers: noCacheHeaders });

    } else {
      // 对于非浏览器(App等),直接进行服务器端重定向
      let targetUrl;
      if (clientIP && clientIP.includes(":")) {
        targetUrl = IPV6_TARGET + originalPathAndQuery;
      } else {
        const ipv4Target = getIPv4TargetByPath(url.pathname);
        targetUrl = ipv4Target + originalPathAndQuery;
      }
  
      // 【关键修复】手动创建302响应,并附上禁止缓存的头
      noCacheHeaders.set('Location', targetUrl);
      return new Response(null, {
        status: 302,
        headers: noCacheHeaders,
      });
    }
  },
};

不懂worker,有个问题咨询一下,worker里能否获取 dns的配置 比如说 我通过ddns给 某个 text记录 传了当前nas的ipv4地址和内网穿透端口,那么在worker里能否读取到这条解析记录,如果能的话是不是可以在v4情况下不走  详情 回复
2025-7-31 10:36
按新的这个修改之后会一直卡在 正在为你选择最佳路径 一直在重新请求。  详情 回复
2025-7-30 20:42

6

主题

80

回帖

0

牛值

fnOS系统内测组

2025-7-30 09:11:23 显示全部楼层

ipv6的80,443端口如何开启

运营商把你的给屏蔽了吗?  详情 回复
2025-7-30 14:02

3

主题

15

回帖

0

牛值

江湖小虾

2025-7-30 14:02:14 楼主 显示全部楼层
Jimboo7339 发表于 2025-7-30 09:11
ipv6的80,443端口如何开启

运营商把你的给屏蔽了吗?
不知道是否屏蔽,部署lucky和nginx指定80和443会报错,可能是系统限制的  详情 回复
2025-7-31 09:22

0

主题

4

回帖

0

牛值

江湖小虾

2025-7-30 18:25:42 显示全部楼层

对于每个服务一个子域名的情况好像也不适用,路由器的端口转发有上限

11

主题

22

回帖

0

牛值

初出茅庐

2025-7-30 20:42:05 显示全部楼层
一蓑烟雨任 发表于 2025-7-29 16:41
// === 目标地址配置区 ===
// 建议将这些值配置到Cloudflare Worker的环境变量中,也可以直接复制到worker ...

按新的这个修改之后会一直卡在 正在为你选择最佳路径 一直在重新请求。
可以看我在下面的回复,原因应该都差不多  详情 回复
2025-8-22 21:24
试一下无痕模式?估计是浏览器cookies缓存导致的 但是,我这个脚本已经设置每次删除缓存了,强制刷新,在选择线路的时候避免了cookies导致的bug  详情 回复
2025-7-31 02:31

3

主题

15

回帖

0

牛值

江湖小虾

2025-7-31 02:31:04 楼主 显示全部楼层
鸽子王奈奈 发表于 2025-7-30 20:42
按新的这个修改之后会一直卡在 正在为你选择最佳路径 一直在重新请求。

试一下无痕模式?估计是浏览器cookies缓存导致的
但是,我这个脚本已经设置每次删除缓存了,强制刷新,在选择线路的时候避免了cookies导致的bug

6

主题

80

回帖

0

牛值

fnOS系统内测组

2025-7-31 09:22:02 显示全部楼层
一蓑烟雨任 发表于 2025-7-30 14:02
运营商把你的给屏蔽了吗?

不知道是否屏蔽,部署lucky和nginx指定80和443会报错,可能是系统限制的

6

主题

80

回帖

0

牛值

fnOS系统内测组

2025-7-31 10:34:53 显示全部楼层

部署好了可用,给楼主点个赞

6

主题

80

回帖

0

牛值

fnOS系统内测组

2025-7-31 10:36:54 显示全部楼层
一蓑烟雨任 发表于 2025-7-29 16:41
// === 目标地址配置区 ===
// 建议将这些值配置到Cloudflare Worker的环境变量中,也可以直接复制到worker ...

不懂worker,有个问题咨询一下,worker里能否获取 dns的配置
比如说 我通过ddns给 某个 text记录 传了当前nas的ipv4地址和内网穿透端口,那么在worker里能否读取到这条解析记录,如果能的话是不是可以在v4情况下不走FNID,走ipv4的内网穿透
我这个worker脚本没有考虑到内网穿透的情况,我修改一下!尝试走一下p2p打洞! 周末我多测试一下,然后把稳定的脚本放出来  详情 回复
2025-7-31 15:01

3

主题

15

回帖

0

牛值

江湖小虾

2025-7-31 15:01:41 楼主 显示全部楼层
Jimboo7339 发表于 2025-7-31 10:36
不懂worker,有个问题咨询一下,worker里能否获取 dns的配置
比如说 我通过ddns给 某个 text记录 传了当 ...

我这个worker脚本没有考虑到内网穿透的情况,我修改一下!尝试走一下p2p打洞!
周末我多测试一下,然后把稳定的脚本放出来
大佬,有啥进展吗  详情 回复
2025-8-15 15:56

8

主题

22

回帖

0

牛值

江湖小虾

2025-7-31 15:26:45 显示全部楼层
我是电信千兆上行50,有公网IP,IPV4和6都有,现在用的免费的DDNS,XXX.f3322.org免费动态域名,感觉速度不行,能怎么提升速度么,公司的联通宽带不不支持IPV6,有文件共享访问需求
和DDNS没有任何关系,DDNS就是一个动态解析的电话本而已 你的NAS在家里放着吗?有公网ipv4,上传慢,应该是运营商限速了,建议/path用speedtest之类的,可以破解限速 但是,运营商绝大多数时候是不对ipv6上传限速的  详情 回复
2025-7-31 21:26

3

主题

15

回帖

0

牛值

江湖小虾

2025-7-31 21:26:45 楼主 显示全部楼层
niqiu8 发表于 2025-7-31 15:26
我是电信千兆上行50,有公网IP,IPV4和6都有,现在用的免费的DDNS,XXX.f3322.org免费动态域名,感觉速度不 ...

和DDNS没有任何关系,DDNS就是一个动态解析的电话本而已
你的NAS在家里放着吗?有公网ipv4,上传慢,应该是运营商限速了,建议/path用speedtest之类的,可以破解限速
但是,运营商绝大多数时候是不对ipv6上传限速的,可以使用WireGuard搭一个隧道,也就是IPv6 traffic in IPv4 tunnel
你可以查一下相关的技术攻略,如果有不懂的,我们及时交流

2

主题

14

回帖

0

牛值

江湖小虾

2025-8-9 09:20:28 显示全部楼层

我想问下,cf中的证书是怎么配置正确的?我使用https的时候会提示证书不正确

对了,记得把nginx proxy manager申请的证书导入到飞牛系统里,不然用app访问可能会提示证书报错  详情 回复
2025-8-18 13:52
证书是你自己在反向代理的时候在本地部署的,推荐使用nginx proxy manager,docker 装一下就行,使用免费证书,自动续签 CF的证书不用管,会自动部署  详情 回复
2025-8-10 20:17

3

主题

15

回帖

0

牛值

江湖小虾

2025-8-10 20:17:11 楼主 显示全部楼层
这个抄手不 发表于 2025-8-9 09:20
我想问下,cf中的证书是怎么配置正确的?我使用https的时候会提示证书不正确
...

证书是你自己在反向代理的时候在本地部署的,推荐使用nginx proxy manager,docker 装一下就行,使用免费证书,自动续签
CF的证书不用管,会自动部署

6

主题

80

回帖

0

牛值

fnOS系统内测组

2025-8-15 15:56:33 显示全部楼层
一蓑烟雨任 发表于 2025-7-31 15:01
我这个worker脚本没有考虑到内网穿透的情况,我修改一下!尝试走一下p2p打洞!
周末我多测试一下,然后把 ...

大佬,有啥进展吗
有,改了脚本逻辑,app客户端用CF做反代,全部重构了逻辑。 现在速度嘎嘎快,就是桌面的飞牛同步软件有点bug,不知道怎么回事,我还在排查  详情 回复
2025-8-18 12:07

3

主题

15

回帖

0

牛值

江湖小虾

2025-8-18 12:07:07 楼主 显示全部楼层
Jimboo7339 发表于 2025-8-15 15:56
大佬,有啥进展吗

有,改了脚本逻辑,app客户端用CF做反代,全部重构了逻辑。
现在速度嘎嘎快,就是桌面的飞牛同步软件有点bug,不知道怎么回事,我还在排查
试了一下,确实很快  详情 回复
2025-8-19 08:37

3

主题

15

回帖

0

牛值

江湖小虾

2025-8-18 12:15:30 楼主 显示全部楼层
//ai生成的代码总结:
主要特性:
- 强大安全防护:有效拦截机器人和可疑扫描请求。
- 智能客户端区分:为App和浏览器提供截然不同的优化路径。
- App请求代理:为App客户端在后端代理请求,确保功能完整可用。
- 浏览器智能探测:仅对页面导航请求返回美观、无错的动态探测页面。
- 健壮的默认代理:所有其他合法请求(如favicon, API调用等)都将被正确代理到后端。
//
//请找到:// const targetHost = isIPv6(clientIP) ? IPV6_TARGET : getIPv4TargetByPath(url.pathname); // 稳妥方案
//        const targetHost = IPV6_TARGET; // 我自己在用的方案
//        这两个意思是,对于手机飞牛app,如果你手机app没有ipv6地址,那么就通过ipv4连接到CF的服务器,CF服务器
//通过通过飞牛中继ipv4连接到你的NAS;我自己在用的方案是:无论我是的手机否拥有ipv6地址,cf服务器都通过ipv6连接
//到我的nas
//根据你自己的情况修改,选择哪种回程类型

// === 目标地址配置区 ===
const IPV6_TARGET = "https://www.yourdomain.com";// 替换为你的域名
const IPV4_LOGIN_TARGET = "https://你的飞牛ID.fnos.net";// 替换为你的飞牛ID
const IPV4_SHARE_TARGET = "https://s.fnnas.net";

// === 探测配置 ===
const PROBE_FILE = "/favicon.ico";
const TIMEOUT = 2500;

// === User-Agent 识别 ===
const BROWSER_UA_KEYWORDS = ['Mozilla/5.0', 'Chrome/', 'Firefox/', 'Safari/', 'Edge/'];
const APP_UA_KEYWORDS = ['Dart/', 'dart:io', 'FeiNiuNAS', 'Dalvik', 'CFNetwork', 'okhttp'];
const BOT_UA_KEYWORDS = [
  'bot', 'spider', 'crawler', 'scraper', 'curl', 'wget', 'python-requests', 'java', 'go-http-client',
  'Googlebot', 'bingbot', 'Baiduspider', 'YandexBot', 'AhrefsBot', 'Nmap', 'sqlmap',
  'HeadlessChrome', 'Puppeteer', 'Playwright'
];

// === 安全防护配置 ===
const SECURITY_CONFIG = {
  CHECK_SUSPICIOUS_HEADERS: true,
  SUSPICIOUS_HEADERS: [
    { header: 'X-Forwarded-Host' }, { header: 'X-Original-URL' },
    { header: 'X-Rewrite-URL' }, { header: 'X-Scanner' },
    { header: 'Acunetix-Product' }, { header: 'X-WIPP' },
  ]
};


// === 辅助函数 ===
function getUserAgentType(userAgent) {
  if (!userAgent) return 'unknown';
  const uaLower = userAgent.toLowerCase();
  if (BOT_UA_KEYWORDS.some(keyword => uaLower.includes(keyword.toLowerCase()))) return 'bot';
  if (APP_UA_KEYWORDS.some(keyword => userAgent.includes(keyword))) return 'app';
  if (BROWSER_UA_KEYWORDS.some(keyword => userAgent.includes(keyword))) return 'browser';
  return 'unknown';
}

function hasSuspiciousHeaders(headers) {
  if (!SECURITY_CONFIG.CHECK_SUSPICIOUS_HEADERS) return false;
  for (const suspicious of SECURITY_CONFIG.SUSPICIOUS_HEADERS) {
    if (headers.has(suspicious.header)) return true;
  }
  return false;
}

function generateBotResponse(request) {
    const html = `<!DOCTYPE html><html><head><title>403 Forbidden</title></head><body><h1>Access Forbidden</h1></body></html>`;
    return new Response(html, {
        status: 403,
        headers: { 'Content-Type': 'text/html;charset=UTF-8', 'X-Robots-Tag': 'noindex, nofollow' }
    });
}

function isIPv6(ip) {
  return ip ? ip.includes(':') : false;
}

function getIPv4TargetByPath(pathname) {
  return (pathname && pathname.startsWith('/s/')) ? IPV4_SHARE_TARGET : IPV4_LOGIN_TARGET;
}


// === 主处理函数 ===
export default {
  async fetch(request, env, ctx) {
    try {
      const url = new URL(request.url);
      const clientIP = request.headers.get("CF-Connecting-IP") || "";
      const userAgent = request.headers.get("User-Agent") || "";
      const originalPathAndQuery = url.pathname + url.search;

      // --- 1. 安全检查先行 ---
      const uaType = getUserAgentType(userAgent);
      const isSuspicious = hasSuspiciousHeaders(request.headers);

      if (uaType === 'bot' || isSuspicious) {
        return generateBotResponse(request);
      }
  
      // --- 2. App客户端处理:直接代理 ---
      if (uaType === 'app') {
        // const targetHost = isIPv6(clientIP) ? IPV6_TARGET : getIPv4TargetByPath(url.pathname); // 稳妥方案
        const targetHost = IPV6_TARGET; // 我自己在用的方案
        //这两个意思是,对于手机飞牛app,如果你手机app没有ipv6地址,那么就通过ipv4连接到CF的服务器,CF服务器通过通过飞牛中继ipv4连接到你的NAS;我自己在用的方案是:无论我是的手机否拥有ipv6地址,cf服务器都通过ipv6连接到我的nas//
        const targetUrl = new URL(originalPathAndQuery, targetHost);
        const proxyRequest = new Request(targetUrl, request);
        return fetch(proxyRequest);
      }
  
      // --- 3. 浏览器文档请求处理 ---
      const dest = request.headers.get('Sec-Fetch-Dest');
      if (uaType === 'browser' && (dest === 'document' || !dest)) {
        const ipv4FallbackUrl = getIPv4TargetByPath(url.pathname) + originalPathAndQuery;
  
        const html = `<!DOCTYPE html><html lang="zh-CN"><head><title>飞牛NAS - 智能路由网关</title><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate"><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap" rel="stylesheet"><noscript><meta http-equiv="refresh" content="0;url=${ipv4FallbackUrl}"></noscript><style>:root{--bg-gradient-start:#1d2b4e;--bg-gradient-end:#0c0c1e;--card-bg:rgba(255,255,255,.08);--text-primary:#fff;--text-secondary:#adb5bd;--accent-color:#3498db;--success-color:#2ecc71}*{margin:0;padding:0;box-sizing:border-box}body{font-family:'Poppins',sans-serif;display:flex;justify-content:center;align-items:center;height:100vh;background:linear-gradient(135deg,var(--bg-gradient-start) 0%,var(--bg-gradient-end) 100%);color:var(--text-primary);overflow:hidden}.container{width:90%;max-width:400px;padding:40px 30px;background:var(--card-bg);border-radius:20px;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,.1);box-shadow:0 8px 32px 0 rgba(0,0,0,.3);text-align:center;animation:fadeIn 1s ease-out}@keyframes fadeIn{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.icon-container{width:80px;height:80px;margin:0 auto 25px}.icon-container svg{width:100%;height:100%}.icon-container .radar-circle{fill:none;stroke:var(--accent-color);stroke-width:3;transform-origin:center;animation:radar-spin 2s linear infinite}.icon-container .check-path{fill:none;stroke:var(--success-color);stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:48;stroke-dashoffset:48;opacity:0}.icon-container.success .radar-circle{animation:none;opacity:0;transition:opacity .3s}.icon-container.success .check-path{opacity:1;animation:draw-check .8s ease-out forwards}@keyframes radar-spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes draw-check{to{stroke-dashoffset:0}}h1{font-size:24px;font-weight:600;margin-bottom:10px}.status-text{font-size:16px;color:var(--text-secondary);height:24px;transition:opacity .5s ease}.fallback{display:none;margin-top:25px;font-size:14px;color:var(--text-secondary);animation:fadeIn .5s 1s ease-out forwards;opacity:0}.fallback a{color:var(--accent-color);text-decoration:none;font-weight:600}.fallback a:hover{text-decoration:underline}</style></head><body><div class="container"><div class="icon-container" id="icon-container"><svg viewBox="0 0 52 52"><circle class="radar-circle" cx="26" cy="26" r="24"/><path class="check-path" d="M14 27l7.5 7.5L38 22"/></svg></div><h1>飞牛NAS 智能网关</h1><p class="status-text" id="status">正在分析您的网络环境...</p><div class="fallback" id="fallback">如果长时间未跳转,请 <a href="${ipv4FallbackUrl}">点击这里</a> 手动访问。</div></div>
<script>
(async function() {
  const ipv6Url = "${IPV6_TARGET}";
  const ipv4LoginUrl = "${IPV4_LOGIN_TARGET}";
  const ipv4ShareUrl = "${IPV4_SHARE_TARGET}";
  const originalPath = ${JSON.stringify(originalPathAndQuery)};
  const statusEl = document.getElementById('status');
  const fallbackEl = document.getElementById('fallback');
  const iconContainer = document.getElementById('icon-container');

  setTimeout(() => { if (fallbackEl) fallbackEl.style.display = 'block'; }, 5000);

  function updateStatus(message) {
    if (!statusEl) return;
    statusEl.style.opacity = '0';
    setTimeout(() => { statusEl.textContent = message; statusEl.style.opacity = '1'; }, 300);
  }

  async function checkIPv6() {
    updateStatus('正在探测 IPv6 连接...');
    try {
      const controller = new AbortController();
      const timer = setTimeout(() => controller.abort(), ${TIMEOUT});
      // *** 关键修复点: 确保 ipv6Url 是正确的字符串 ***
      await fetch(ipv6Url + "${PROBE_FILE}" + "?_t=" + Date.now(), { 
          method: 'HEAD', 
          mode: 'no-cors', 
          signal: controller.signal 
      });
      clearTimeout(timer);
      return true;
    } catch (e) { 
      return false; 
    }
  }

  const isIPv6Available = await checkIPv6();
  let finalUrl;

  if (isIPv6Available) {
    updateStatus('✓ IPv6 直连可用,即将跳转');
    iconContainer.classList.add('success');
    finalUrl = ipv6Url + originalPath;
  } else {
    updateStatus('→ 切换至 IPv4 备用链路');
    finalUrl = originalPath.startsWith('/s/') ? (ipv4ShareUrl + originalPath) : (ipv4LoginUrl + originalPath);
  }
  
  setTimeout(() => window.location.replace(finalUrl), 1500);
})();
</script>
</body></html>`;
        return new Response(html, { status: 200, headers: { 'Content-Type': 'text/html;charset=UTF-8', 'Cache-Control': 'no-store, no-cache, must-revalidate' }});
      }

      // --- 4. 最终回退:代理所有其他请求 ---
      const targetHost = isIPv6(clientIP) ? IPV6_TARGET : getIPv4TargetByPath(url.pathname);
      const targetUrl = new URL(originalPathAndQuery, targetHost);
      const proxyRequest = new Request(targetUrl, request);
      return fetch(proxyRequest);

    } catch (error) {
      console.error(`Worker fetch error: ${error.stack}`);
      return new Response(`服务暂时不可用: ${error.message}`, { status: 500 });
    }
  },
};

3

主题

15

回帖

0

牛值

江湖小虾

2025-8-18 13:52:18 楼主 显示全部楼层
这个抄手不 发表于 2025-8-9 09:20
我想问下,cf中的证书是怎么配置正确的?我使用https的时候会提示证书不正确
...

对了,记得把nginx proxy manager申请的证书导入到飞牛系统里,不然用app访问可能会提示证书报错

6

主题

80

回帖

0

牛值

fnOS系统内测组

2025-8-19 08:37:30 显示全部楼层
一蓑烟雨任 发表于 2025-8-18 12:07
有,改了脚本逻辑,app客户端用CF做反代,全部重构了逻辑。
现在速度嘎嘎快,就是桌面的飞牛同步软件有点 ...

试了一下,确实很快

6

主题

16

回帖

0

牛值

江湖小虾

2025-8-19 16:36:45 显示全部楼层

请问IPv4 访客通过 FNOS 官方中继服务器访问时,能访问其他端口的第3方app吗?如81端口的ngnix网页、5678端口的小雅等。

你试试吧,对于浏览器,我这个就是个分流,自动选择网址而已 如果是app,全部通过cf的反代实现,理论上和你直接通过域名访问没区别  详情 回复
2025-8-21 20:07

3

主题

15

回帖

0

牛值

江湖小虾

2025-8-21 20:07:27 楼主 显示全部楼层
dvdlab 发表于 2025-8-19 16:36
请问IPv4 访客通过 FNOS 官方中继服务器访问时,能访问其他端口的第3方app吗?如81端口的ngnix网页、5678端 ...

你试试吧,对于浏览器,我这个就是个分流,自动选择网址而已
如果是app,全部通过cf的反代实现,理论上和你直接通过域名访问没区别

0

主题

6

回帖

0

牛值

江湖小虾

2025-8-22 03:30:13 显示全部楼层

一直在等待自动跳转,但就是不跳

0

主题

6

回帖

0

牛值

江湖小虾

2025-8-22 21:23:31 显示全部楼层

感谢大佬的耐心指导,找到无限跳转的原因了,后续丝滑部署。其实就是解决两个核心问题,第一飞牛系统本身那个端口重定向功能要关掉,二就是cf中除了那个nas 100::需要云朵,其他与该代理相关的相关aaaa、cname等全都需要关掉。保证你的二级域名、根域名在不被代理以及输入端口情况下,可以直连到网站。如果不知道如何反代解决那个域名输入端口问题,可以直接把飞牛端口写成80 443,这样就可以了。

额外提示一下,最后那个域路由是指二级域名,和nas 100::没有任何关系。

0

主题

6

回帖

0

牛值

江湖小虾

2025-8-22 21:24:17 显示全部楼层
鸽子王奈奈 发表于 2025-7-30 20:42
按新的这个修改之后会一直卡在 正在为你选择最佳路径 一直在重新请求。

可以看我在下面的回复,原因应该都差不多

23

主题

61

回帖

0

牛值

初出茅庐

2025-9-3 16:47:14 显示全部楼层

请问一我的IPV4地址是一个frp服务器的地址 只能http访问 有什么办法吗 或者我能不能ipv6和ipv4都只用http?

0

主题

3

回帖

0

牛值

江湖小虾

2025-9-4 15:29:51 显示全部楼层

试了两次,代码可以用,最主要是利用了飞牛的网关优选。

0

主题

3

回帖

0

牛值

江湖小虾

2025-9-5 10:05:39 显示全部楼层

昨晚发现,使用飞牛app的域名无法访问,问了一下DS,代码修改如下

0

主题

3

回帖

0

牛值

江湖小虾

2025-9-5 10:06:18 显示全部楼层

//
//请找到:// const targetHost = isIPv6(clientIP) ? IPV6_TARGET : getIPv4TargetByPath(url.pathname); // 稳妥方案
// const targetHost = IPV6_TARGET; // 我自己在用的方案
// 这两个意思是,对于手机飞牛app,如果你手机app没有ipv6地址,那么就通过ipv4连接到CF的服务器,CF服务器
//通过通过飞牛中继ipv4连接到你的NAS;我自己在用的方案是:无论我是的手机否拥有ipv6地址,cf服务器都通过ipv6连接
//到我的nas
//根据你自己的情况修改,选择哪种回程类型

// === 目标地址配置区 ===
const IPV6_TARGET = "https://xxxxx";// xxxxx替换为你的域名
const IPV4_LOGIN_TARGET = "https://xxxxx.5ddd.com";// xxxxx替换为你的飞牛ID的域名
const IPV4_SHARE_TARGET = "https://s.fnnas.net";

// === 探测配置 ===
const PROBE_FILE = "/favicon.ico";
const TIMEOUT = 2500;

// === User-Agent 识别 ===
const BROWSER_UA_KEYWORDS = ['Mozilla/5.0', 'Chrome/', 'Firefox/', 'Safari/', 'Edge/'];
const APP_UA_KEYWORDS = ['Dart/', 'dart:io', 'FeiNiuNAS', 'Dalvik', 'CFNetwork', 'okhttp'];
const BOT_UA_KEYWORDS = [
'bot', 'spider', 'crawler', 'scraper', 'curl', 'wget', 'python-requests', 'java', 'go-http-client',
'Googlebot', 'bingbot', 'Baiduspider', 'YandexBot', 'AhrefsBot', 'Nmap', 'sqlmap',
'HeadlessChrome', 'Puppeteer', 'Playwright'
];

// === 飞牛App特定域名识别 ===
const FEINIU_APP_DOMAINS = [
'https://xxxxx',// 替换为你的域名
'https://xxxxx.5ddd.com' // xxxxx替换为你的飞牛ID的域名

];

// === 安全防护配置 ===
const SECURITY_CONFIG = {
CHECK_SUSPICIOUS_HEADERS: true,
SUSPICIOUS_HEADERS: [
{ header: 'X-Forwarded-Host' }, { header: 'X-Original-URL' },
{ header: 'X-Rewrite-URL' }, { header: 'X-Scanner' },
{ header: 'Acunetix-Product' }, { header: 'X-WIPP' },
]
};

// === 辅助函数 ===
function getUserAgentType(userAgent) {
if (!userAgent) return 'unknown';
const uaLower = userAgent.toLowerCase();
if (BOT_UA_KEYWORDS.some(keyword => uaLower.includes(keyword.toLowerCase()))) return 'bot';
if (APP_UA_KEYWORDS.some(keyword => userAgent.includes(keyword))) return 'app';
if (BROWSER_UA_KEYWORDS.some(keyword => userAgent.includes(keyword))) return 'browser';
return 'unknown';
}

function isFeiNiuAppDomain(hostname) {
return FEINIU_APP_DOMAINS.some(domain => hostname.includes(domain));
}

function hasSuspiciousHeaders(headers) {
if (!SECURITY_CONFIG.CHECK_SUSPICIOUS_HEADERS) return false;
for (const suspicious of SECURITY_CONFIG.SUSPICIOUS_HEADERS) {
if (headers.has(suspicious.header)) return true;
}
return false;
}

function generateBotResponse(request) {
const html = <!DOCTYPE html><html><head><title>403 Forbidden</title></head><body><h1>Access Forbidden</h1></body></html>;
return new Response(html, {
status: 403,
headers: { 'Content-Type': 'text/html;charset=UTF-8', 'X-Robots-Tag': 'noindex, nofollow' }
});
}

function isIPv6(ip) {
return ip ? ip.includes(':') : false;
}

function getIPv4TargetByPath(pathname) {
return (pathname && pathname.startsWith('/s/')) ? IPV4_SHARE_TARGET : IPV4_LOGIN_TARGET;
}

// === 主处理函数 ===
export default {
async fetch(request, env, ctx) {
try {
const url = new URL(request.url);
const clientIP = request.headers.get("CF-Connecting-IP") || "";
const userAgent = request.headers.get("User-Agent") || "";
const hostname = url.hostname;
const originalPathAndQuery = url.pathname + url.search;

  // --- 1. 安全检查先行 ---
  const uaType = getUserAgentType(userAgent);
  const isSuspicious = hasSuspiciousHeaders(request.headers);
  const isFeiNiuDomain = isFeiNiuAppDomain(hostname);

  if (uaType === 'bot' || isSuspicious) {
    return generateBotResponse(request);
  }

  // --- 2. App客户端处理:直接代理 ---
  // 识别飞牛App的两种方式:User-Agent或特定域名
  if (uaType === 'app' || isFeiNiuDomain) {
    console.log(`Processing FeiNiu App request from: ${clientIP}, UA: ${userAgent}, Domain: ${hostname}`);
  
    // const targetHost = isIPv6(clientIP) ? IPV6_TARGET : getIPv4TargetByPath(url.pathname); // 稳妥方案
    const targetHost = IPV6_TARGET; // 我自己在用的方案
    //这两个意思是,对于手机飞牛app,如果你手机app没有ipv6地址,那么就通过ipv4连接到CF的服务器,CF服务器通过通过飞牛中继ipv4连接到你的NAS;我自己在用的方案是:无论我是的手机否拥有ipv6地址,cf服务器都通过ipv6连接到我的nas//
  
    const targetUrl = new URL(originalPathAndQuery, targetHost);
  
    // 复制原始请求的所有头信息,但移除可能引起问题的头
    const newHeaders = new Headers(request.headers);
    newHeaders.set('Host', targetUrl.hostname);
  
    const proxyRequest = new Request(targetUrl, {
      method: request.method,
      headers: newHeaders,
      body: request.body,
      redirect: 'follow'
    });
  
    return fetch(proxyRequest);
  }

  // --- 3. 浏览器文档请求处理 ---
  const dest = request.headers.get('Sec-Fetch-Dest');
  if (uaType === 'browser' && (dest === 'document' || !dest)) {
    const ipv4FallbackUrl = getIPv4TargetByPath(url.pathname) + originalPathAndQuery;

    const html = `<!DOCTYPE html><html lang="zh-CN"><head><title>飞牛NAS - 智能路由网关</title><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate"><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap" rel="stylesheet"><noscript><meta http-equiv="refresh" content="0;url=${ipv4FallbackUrl}"></noscript><style>:root{--bg-gradient-start:#1d2b4e;--bg-gradient-end:#0c0c1e;--card-bg:rgba(255,255,255,.08);--text-primary:#fff;--text-secondary:#adb5bd;--accent-color:#3498db;--success-color:#2ecc71}*{margin:0;padding:0;box-sizing:border-box}body{font-family:'Poppins',sans-serif;display:flex;justify-content:center;align-items:center;height:100vh;background:linear-gradient(135deg,var(--bg-gradient-start) 0%,var(--bg-gradient-end) 100%);color:var(--text-primary);overflow:hidden}.container{width:90%;max-width:400px;padding:40px 30px;background:var(--card-bg);border-radius:20px;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,.1);box-shadow:0 8px 32px 0 rgba(0,0,0,.3);text-align:center;animation:fadeIn 1s ease-out}@keyframes fadeIn{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.icon-container{width:80px;height:80px;margin:0 auto 25px}.icon-container svg{width:100%;height:100%}.icon-container .radar-circle{fill:none;stroke:var(--accent-color);stroke-width:3;transform-origin:center;animation:radar-spin 2s linear infinite}.icon-container .check-path{fill:none;stroke:var(--success-color);stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:48;stroke-dashoffset:48;opacity:0}.icon-container.success .radar-circle{animation:none;opacity:0;transition:opacity .3s}.icon-container.success .check-path{opacity:1;animation:draw-check .8s ease-out forwards}@keyframes radar-spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes draw-check{to{stroke-dashoffset:0}}h1{font-size:24px;font-weight:600;margin-bottom:10px}.status-text{font-size:16px;color:var(--text-secondary);height:24px;transition:opacity .5s ease}.fallback{display:none;margin-top:25px;font-size:14px;color:var(--text-secondary);animation:fadeIn .5s 1s ease-out forwards;opacity:0}.fallback a{color:var(--accent-color);text-decoration:none;font-weight:600}.fallback a:hover{text-decoration:underline}</style></head><body><div class="container"><div class="icon-container" id="icon-container"><svg viewBox="0 0 52 52"><circle class="radar-circle" cx="26" cy="26" r="24"/><path class="check-path" d="M14 27l7.5 7.5L38 22"/></svg></div><h1>飞牛NAS 智能网关</h1><p class="status-text" id="status">正在分析您的网络环境...</p><div class="fallback" id="fallback">如果长时间未跳转,请 <a href="${ipv4FallbackUrl}">点击这里</a> 手动访问。</div></div>
`; return new Response(html, { status: 200, headers: { 'Content-Type': 'text/html;charset=UTF-8', 'Cache-Control': 'no-store, no-cache, must-revalidate' }}); }
  // --- 4. 最终回退:代理所有其他请求 ---
  const targetHost = isIPv6(clientIP) ? IPV6_TARGET : getIPv4TargetByPath(url.pathname);
  const targetUrl = new URL(originalPathAndQuery, targetHost);
  
  // 复制原始请求的所有头信息
  const newHeaders = new Headers(request.headers);
  newHeaders.set('Host', targetUrl.hostname);
  
  const proxyRequest = new Request(targetUrl, {
    method: request.method,
    headers: newHeaders,
    body: request.body,
    redirect: 'follow'
  });
  
  return fetch(proxyRequest);

} catch (error) {
  console.error(`Worker fetch error: ${error.stack}`);
  return new Response(`服务暂时不可用: ${error.message}`, { status: 500 });
}

},
};

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则