分类: network

  • How to simulate hard disconnection for websocket

    虽然 Chrome/Firefox 在开发者工具中提供了离线模式,但是这个功能无法中断已经跟服务端建立起来的 WebSocket 连接。

    如果你跟我一样使用 Linux 系统,那么就可以借助 iptables 命令来模拟连接中断的情况:

    # 中断所有访问 localhost:3001 的连接
    sudo iptables -I INPUT 1 -p tcp -i lo --dport 3001 -j DROP
    
    # 恢复连接
    sudo iptables -D INPUT -p tcp -i lo --dport 3001 -j DROP

    如果你发现上面的命令不生效,那么可能是因为你的浏览器在通过 IPv6 地址访问本地端口,你需要将 iptables 替换成 ip6tables

    如果你是基于 iptables 实现的 ufw 用户,你会发现 ufw 中建立的本地端口规则完全不生效,这是因为 ufw 不会处理任何从本机访问本机的连接,我们只能手动调用更底层的 iptables

  • OpenWRT 上的 zerotier 突然无法启动了

    This entry is part 5 of 5 in the series homelab 历险记

    在我之前的文章中,介绍了我从公网远程访问家庭内网的方式,但在上个周末,这套配置出现了故障。故障的表现是 OpenWRT 上的 zerotier 客户端无法启动:

    zerotier-one[21492]: terminate called after throwing an instance of 'std::bad_cast'

    通过搜索得知这个故障通常伴随着硬盘问题,比如磁盘空间不足。通过 df -h 指令可以看到 /tmp 分区满了,而在 OpenWRT 上 /var 实际上会被链接到 /tmp 分区,这就导致 zerotier 客户端在启动时无法将配置写入 /var/lib/zerotier-one/networks.d 文件夹中。

    使用 du 命令,可以看到是 singbox 的日志文件充满了 /tmp 分区,强行删除日志文件后,虽然 df 命令可以看到空间被释放了,但实际上尝试写入 /var 仍然会报空间不足的错误。

    在 Gemini 的指点下,得知这很可能是因为仍然存在进程在使用这个文件,导致磁盘空间并没有被释放。果然,重启 singbox 进程后, zerotier 客户端终于能正常启动了。

    为了避免以后再有类似的情况出现,我干脆关闭了 singbox 的日志,等需要 debug 的时候再开启。

  • 给 QNAP 文件分享外链套个壳

    This entry is part 4 of 5 in the series homelab 历险记

    QNAP 提供的在线服务 MyQNAPCloud 的下载速度有时真的非常令人捉急。为了方便他人下载我分享的文件,得提供一个允许公网访问 NAS 共享链接的方式。

    基础设施改动

    考虑到我现在 Homelab 的基础设施中已经有了 cloudflared,所以可以用它设置一个 tunnel,但是为了避免将 NAS 其他功能暴露到公网,还需要再用 Nginx 设置一个代理,拒绝所有访问非共享链接页面的请求:

    location ^~ /share.cgi {
      proxy_pass https://qnap.lan:5001;
    }
    
    location ~* \.(js|svg|gif|png)$ {
      proxy_pass https://qnap.lan:5001;
    }
    
    
    location ^~ /shareLink/ {
      proxy_pass https://qnap.lan:5001;
    }
    
    location / {
      return 302 "https://zeeko.dev";
    }

    接下来,设置 Cloudflared 的 tunnel,将 Local Service 的地址改成这台 nginx 服务器。

    上面之所以还配置了 Host Header 是因为我的 nginx 上配置的域名是给局域网专用的,跟公网域名不同,为了让 Nginx 能够正常识别请求,就需要让 Cloudflared 设置转发 Host Header。

    UI 体验优化

    设置好网络功能后,还需要优化一下 QNAP 管理页面的 UI,方便我复制公网链接。这里我选择使用油猴脚本来实现我的目的。

    // ==UserScript==
    // @name         My QNAP
    // @namespace    https://qnap.lan/
    // @version      2025-05-31
    // @description  try to take over the QNAP!
    // @author       You
    // @match        https://qnap.lan/*
    // @icon         https://www.google.com/s2/favicons?sz=64&domain=tampermonkey.net
    // @grant        none
    // ==/UserScript==
    
    (function() {
        'use strict';
    
        // Configuration for the observer
        const observerConfig = {
            childList: true,
            subtree: true,
            attributes: false,
            characterData: false
        };
    
        // Create an observer instance
        const observer = new MutationObserver(mutations => {
            requestIdleCallback(() => {
                // DEBUG: Get all label elements
                const allLabels = Array.from(document.querySelectorAll('label.x-form-item-label'));
                // DEBUG: Find Local IP label
                const localIPLabel = allLabels
                .find(it => it.innerText === 'Local IP');
                console.debug('[DEBUG] localIPLabel:', localIPLabel);
                if (localIPLabel == null) {
                    console.warn('[DEBUG] Local IP label not found');
                    return;
                }
                const localIP = localIPLabel.nextElementSibling.innerText.trim();
                console.debug('[DEBUG] localIP:', localIP);
    
                if(localIP == null || localIP === '') {
                    console.warn('[DEBUG] localIP is null or empty');
                    return;
                }
    
                const query = new URL(localIP).search;
                console.debug('[DEBUG] query:', query);
    
                // DEBUG: Find SmartShare label
                const smartShareLabel = allLabels
                .find(it => it.innerText === 'SmartShare');
                if (smartShareLabel == null) {
                    console.warn('[DEBUG] SmartShare label not found');
                    return;
                }
                const copyButton = smartShareLabel.nextElementSibling.querySelector('button');
                if (copyButton == null) {
                    console.warn('[DEBUG] Copy button not found');
                    return;
                }
    
                copyButton.addEventListener('click', () => {
                    const shareUrl = 'https://smartshare.zeeko.dev/share.cgi' + query;
                    navigator.clipboard.writeText(shareUrl);
                });
    
            });
        });
    
        // Start observing the document body
        observer.observe(document.body, observerConfig);
    
        // Cleanup observer when page unloads
        window.addEventListener('unload', () => {
            observer.disconnect();
        });
    })();

    启用上面的脚本后,就可以在 UI 上直接复制文件共享的公网链接:

    最终效果

    相比通过 QNAP 的云下载,速度要快了 10 倍。

  • Zerotier: No route to host

    This entry is part 3 of 5 in the series homelab 历险记

    一觉醒来,突然发现家里的 OpenWRT 软路由无法通过 Zerotier 网络访问了,虽然面板上显示设备在线,但不管是 Ping 还是 curl ,都会报错:No route to host。

    09:54:21.055775 [0-0] * [HTTPS-CONNECT] adjust_pollset -> 1 socks
    09:54:22.048545 [0-0] * [HTTPS-CONNECT] connect, check h21
    09:54:22.049187 [0-0] * connect to 192.168.100.22 port 443 from 10.1.1.111 port 59496 failed: No route to host

    首先,我们需要判断这个问题是否能在 Zerotier 网络中其他节点复现,通过登录不同节点执行 ping 跟 curl,发现只有 OpenWRT 在 Zerotier 网络中不可达。

    接下来登录 OpenWRT 上查看 Zerotier 客户端的状态:

    $ zerotier-cli info -j
    {
     "address": "....",
     "clock": 1748052290108,
     "config": {
      "settings": {
       "allowTcpFallbackRelay": true,
       "forceTcpRelay": false,
       "homeDir": "/var/lib/zerotier-one",
       "listeningOn": [
        "192.168.100.1/9993",
        "192.168.1.2/9993"
       ],
       "portMappingEnabled": true,
       "primaryPort": 9993,
       "secondaryPort": 44537,
       "softwareUpdate": "disable",
       "softwareUpdateChannel": "release",
       "surfaceAddresses": [
        "家宽公网 IP"
       ],
       "tertiaryPort": 44486
      }
     },
     "online": true,
     "planetWorldId": 149604618,
     "planetWorldTimestamp": 1738848951118,
     "publicIdentity": "....",
     "tcpFallbackActive": false,
     "version": "1.14.1",
     "versionBuild": 0,
     "versionMajor": 1,
     "versionMinor": 14,
     "versionRev": 1
    }
    
    $ zerotier-cli peers
    200 peers
    <ztaddr>   <ver>  <role> <lat> <link>   <lastTX> <lastRX> <path>
    778cde7190 -      PLANET   225 DIRECT   44578    134480   103.195.103.66/9993
    cafe04eba9 -      PLANET   318 DIRECT   44578    134387   84.17.53.155/9993
    cafe80ed74 -      PLANET   165 DIRECT   4519     129532   185.152.67.145/9993
    cafefd6717 -      PLANET   263 DIRECT   44578    129434   79.127.159.187/9993

    可以看到 OpenWRT 上的客户端并没有显示任何异常信息,这也对应了控制面板上看到的设备在线状态。但值得注意的是,在 surfaceAddresses 字段上显示的是我的家庭宽带公网出口地址,这也对应了控制面板上显示的该设备的物理地址。考虑到 Zerotier 可能已经被中国联通给 Ban 了,所以接下来可以尝试让 Zerotier 客户端通过代理连接 Planet。

    这时,有意思的事情出现了,尽管我在代理软件中正确地设置了 Zerotier 的协议转发规则(转发目标端口为 9993 的 TCP/UDP 流量),但我在代理软件的日志中却无法查看到 Zerotier 相关的流量信息。

    我的规则设置有误还是代理软件在这个场景下存在 Bug ?这可以通过一个实验来验证,使用 ncat -vuz 84.17.53.155 9993 就可以向 Zerotier 的 Planet 发送一个用来测试连接性的 UDP 包。

    Ncat: Version 7.93 ( https://nmap.org/ncat )
    Ncat: Connected to 84.17.53.155:9993.
    Ncat: UDP packet sent successfully
    Ncat: 1 bytes sent, 0 bytes received in 2.00 seconds.

    可以看到这个 UDP 包被成功发送了,而且在代理软件的日志中也能找到相关的转发记录。这说明我的代理软件规则配置是正确的,而且代理软件也没有任何问题。那为什么在代理软件的日志中看不到 Zerotier 的流量呢,难道 Zerotier 客户端压根没有去连接 Planet ?这就需要使用 Wireshark 来分析 OpenWRT 的流量了。

    # on my laptop
    $ ssh openwrt 'tcpdump  -s 0 -U -n -w - -i eth0' | sudo wireshark -k -i -<br>

    通过分析 OpenWRT 各个网络设备上的流量,可以发现 Zerotier 的 UDP 包确实被发送了,但它是通过 OpenWRT 的 WAN 地址发送的,而我的代理软件只会通过 nft 拦截来自 localhost 跟 LAN 的流量,这就解释了为什么代理软件无法转发 Zerotier 的流量。

    确实,在网络编程中,当机器被绑定了多个 IP 地址时,我们可以指定某个特定的 IP 作为源 IP,对于 VPN 软件 Zerotier 来说,这样做更是基操。所以,在 Zerotier 的配置文件中肯定存在某个配置项,用来设置 Zerotier 流量的出口网络。

    通过翻阅它的文档,可以看到有两个选项会影响 Zerotier 流量的发送地址:

    {
      "settings": {
        "interfacePrefixBlacklist": [ "XXX",... ], /* Array of interface name prefixes (e.g. eth for eth#) to blacklist for ZT traffic */
        "bind": [ "ip",... ], /* If present and non-null, bind to these IPs instead of to each interface (wildcard IP allowed) */
      }
    }

    在我的测试下,需要指定 bind 为 LAN 地址,才能真正解决这个问题。这样设置后,Zerotier 就会通过代理服务跟 Planet 沟通,在管理面板上,OpenWRT 的物理地址也会被展示成代理服务的 IP。

    实测下来,从外网访问 OpenWRT 的速度还是能达到家庭宽带的上限,网络延迟也还不错,问题终于解决了~

  • 解决 Cloudflare Tunnel 无法通过 Passwall2 连接的问题

    This entry is part 3 of 3 in the series 网上冲浪指南

    最近发现家里 NAS 上部署的 cloudflared 很容易掉线,切换节点也不好使。

    找了一圈互联网资料后,最后根据这篇文章的描述,成功修复了 passwall2 的配置。

    简单来说, Cloudflare Tunnel 客户端(Cloudflared)会先解析 region1.v2.argotunnel.com/region2.v2.argotunnel.com 的地址,再用 quic.cftunnel.com/h2.cftunnel.com 作为 SNI 去请求。

    当开启 Passwall2 上的覆盖连接目标地址功能时,Xray 就会在转发流量时用 *.cftunnel.com 替换掉 Cloudflared 客户端获取的真实地址,当流量达到远程 Xray 时,由于 *.cftunnel.com 并没有 A/AAAA DNS 记录,就会导致流量转发失败,从而出现这样的错误:

    2025-05-09T14:07:05Z ERR Serve tunnel error error="TLS handshake with edge error: EOF" connIndex=0 event=0 ip=198.41.192.57

    要解决这个问题,需要禁用掉 Passwall2 上对 Cloudflare tunnel 相关域名的嗅探功能。

    或者,如果你能保证客户端 DNS 解析没有问题的话,可以干脆禁用掉「覆盖连接目标地址」功能,这也是 Xray 文档中推荐的做法:

    在能保证 被代理连接能得到正确的 DNS 解析 时,使用 routeOnly 且开启 destOverride 的同时,将路由匹配策略 domainStrategy 设置为 AsIs 即可实现全程无 DNS 解析进行域名及 IP 分流。此时遇到 IP 规则匹配时使用的 IP 为域名原始 IP。

    https://xtls.github.io/config/inbound.html#sniffingobject
  • 如何让 MACvLan 网络下的容器与宿主机互联互通

    我在 NAS 上部署了一个 NginxProxyManager 方便管理其他 Self Host 服务的 SSL 证书,为了避免占用宿主机的 80/443 端口,我用了 macvlan 为 NPM 容器分配了一个独立的局域网 IP。

    networks:
      macvlan_network:
        driver: macvlan
        driver_opts:
          parent: <your-network-interface>
        ipam:
          config:
            - subnet: 192.168.100.0/24
              gateway: 192.168.100.1
    
    services:
      npm:
        image: 'jc21/nginx-proxy-manager:latest'
        restart: unless-stopped
        networks:
          macvlan_network:
            ipv4_address: 192.168.100.22
        environment:
          DISABLE_IPV6: 'true'
        volumes:
          - ./data:/data
          - ./letsencrypt:/etc/letsencrypt

    这时,局域网其他的设备就可以通过 192.168.100.22 访问 NPM,但是对于宿主机以及宿主机上其他的容器来说,这个地址是无法访问的

    在咨询了 GPT 之后,它建议了下面的方案:

    [Unit]
    Description=Set up specific macvlan route for a Docker container
    After=network.target
    
    [Service]
    Type=oneshot
    RemainAfterExit=yes
    # Replace <your-network-interface> with the actual interface name
    ExecStart=/usr/bin/ip link add mvlan0 link <your-network-interface> type macvlan mode bridge
    ExecStart=/usr/bin/ip addr add <unused-ip>/24 dev mvlan0
    ExecStart=/usr/bin/ip link set mvlan0 up
    ExecStart=/usr/bin/ip route del 192.168.100.0/24 dev mvlan0
    ExecStart=/usr/bin/ip route add 192.168.100.22 dev mvlan0
    ExecStop=/usr/bin/ip link delete mvlan0
    
    [Install]
    WantedBy=multi-user.target

    默认情况下的问题:

      • 当使用 macvlan/ipvlan 时,Docker 容器会直接连接到物理网络接口
      • 但是宿主机无法通过物理接口直接与这些容器通信,因为物理接口不会处理宿主机到 macvlan 接口的流量

      创建 mvlan0 的作用:

      sudo ip link add mvlan0 link <your-network-interface> type macvlan mode bridge
      sudo ip addr add <unused-ip>/24 dev mvlan0
      sudo ip link set mvlan0 up
      • 这实际上是在宿主机创建了一个新的网络接口 mvlan0
      • 这个接口也是一个 macvlan 接口,与 Docker 容器使用相同的技术
      • mvlan0 接口可以看作是宿主机在 192.168.100.0/24 网段的一个”入口”

      通信流程:

      宿主机 -> mvlan0 (unused-ip) -> 物理网卡 -> Docker 容器 (192.168.100.22)
      • 当宿主机要访问 192.168.100.22 时
      • 数据包会通过 mvlan0 接口发出
      • 由于 mvlan0 和容器的网络接口都是 macvlan 类型,它们可以在同一层次通信

      类比解释:

        • 可以把这个设置想象成宿主机也”加入”到了容器所在的网络
        • mvlan0 就像是宿主机的一个”虚拟网卡”,使它能够与容器在同一个网络层面通信

        为什么需要这样:

          • 这种设置避免了常规的网络栈限制
          • 提供了一个直接的通信路径
          • 保持了网络隔离性的同时又允许必要的通信

          这就是为什么添加这个 macvlan 接口可以让宿主机访问容器的原理。这种方法既保持了容器网络的独立性,又提供了必要的访问路径。

        1. 2024 家庭网络设备大升级 网络篇

          This entry is part 2 of 5 in the series homelab 历险记

          网络设备硬件到齐之后,就需要重新配置网络了,在我的使用场景中,家庭网络除了基本的上网之外需要承担两个额外的功能:

          • 全局透明代理
          • 允许电脑、手机远程访问本地网络服务

          我选择的方案是使用 N100 小主机运行一个 OpenWRT 虚拟机,并将两个物理网卡分配给它,以实现更加高级的网络配置。

          怎么连网线

          因为家宽的下行带宽只有 300M,所以就用开发商自带的超五类网线连接光猫跟 N100 小主机的千兆 WAN 口。

          N100 小主机的 2.5G LAN 口跟一台 TP-Link 路由器连接,路由器设置有线 AP 模式,这样全屋所有的流量都由虚拟机软路由处理,用 openwrt-passwall2 实现全局透明代理功能。

          QNAP NAS 通过网线连接到 TP-Link 路由器,不过由于路由器只有千兆网口,所以 QNAP 的2.5G 网口完全是性能过剩的。

          N100 上再用一个有线网卡连接到 TP-Link 路由器,给宿主机联网使用,这个有线网卡配置了一个静态 IP,方便在软路由挂掉的时候访问管理面板排查故障。

          其他的设备都用无线网络连接,因为户型小,所以单个 TP-Link 路由器的 5GHz WiFi 完全可以覆盖主要的视频消费设备。至于厨房、阳台等犄角旮旯的物联网设备,2.4GHz WiFi 也能提供足够的覆盖度。

          怎么从任意的外部网络访问家里的局域网

          很多 homelab 玩家会选择公网 IP 搭配 DDNS 的方案,但对我来说,这种方案会显著提高局域网的安全风险 —— 我并不是一个 OpenWRT 的运维高手,也完全不会使用 iptables/nft ,如果有人通过公网 IP 侵入了局域网,我大概率很难及时地发现问题。

          我的方案是 zerotier + 拥有公网 IP 的 VPS + singbox。外网设备通过 singbox 客户端连接 VPS 上的 singbox 服务端,然后通过路由规则将发往局域网的流量通过 zerotier 转发到家庭网络的 OpenWRT 上,以此实现远程访问。

          首先,将局域网中的 OpenWRT 跟 VPS 都加入同一个 zerotier 网络。OpenWRT 加入 zerotier 比较复杂,建议参考 openwrt 的文档配置。

          在 OpenWRT 上成功加入 zerotier 网络后,就可以看到下面的接口以及防火墙配置:

          然后在 VPS 上添加一条路由规则:

          ip route add 192.168.100.0/24 via 10.2.2.233
          # 10.2.2.233 就是 OpenWRT 在 zerotier 网段中的 IP

          接着其他设备在外部网络下通过 singbox 连接到 VPS,并且添加下面的 singbox 路由规则:

          "route": {
            "rules": [
              {
                // 家庭局域网网段
                "ip_cidr": [
                  "192.168.100.0/24"
                ],
                "outbound": "vps"
              },
              {
                // 局域网服务专属域名,在公网设置解析到家庭局域网的 Web 网关
                "domain_suffix": [
                  ".n.zeeko.dev"
                ],
                "outbound": "vps"
              },
            ]
          }

          这样不仅可以减少家庭局域网在公网上暴露面,还方便各种设备(笔记本电脑、手机、平板)在各种网络条件下访问家里的局域网设备 —— 只要能连接上部署了 singbox 的 VPS,就可以访问家里的局域网。至于远程访问时的网络连接速度,实测在联通 5G 网络下满足远程播放 20Mbps 码率视频的场景比哔哩哔哩大会员的 4K 视频要强

        2. 使用 WebP/AVIF 压缩博文图片

          使用 WebP/AVIF 压缩博文图片

          往文章中贴截图的时候,如果使用压缩率较低的图片格式(jpeg/png),长时间的图片下载耗时会降低用户的阅读体验。

          根据 CanIUse 的统计,目前绝大多数的网上冲浪用户使用的浏览器已经支持了 WebP/AVIF 这类高效的图片格式。

          https://caniuse.com/avif Can I use AVIF?
          https://caniuse.com/webp Can I use WebP?

          以文章开头的那张图片为例,原图大概 108KB,转换成 AVIF 格式并压缩后,体积缩小了接近 90% 。

          如果想要给 WordPress 启用自动转换的功能,我推荐使用下面的免费插件,只要你的 PHP 支持 imagick/GD 插件,就可以使用。

          安装完成后需要在管理面板中启用 Mordern ImageFormats 功能。

          如果需要把已有的图片都转换成 WebP/AVIF,可以使用下面的插件批量转换。

          安装完成后就可以在后台管理页面批量转换格式或者单独地转换格式。

          一些疑问

          如果用户的浏览器不支持 WebP/AVIF 该怎么办?

          AVIF 在比较新的浏览器中普及率很高 只有现代 IE —— Safari 不支持,WebP 的兼容性也非常好,不过万一有人用史前时代的浏览器上网呢?

          其实这个问题完全不用担心,上面介绍的插件在生成 WebP/AVIF 格式后并不会删除原图,浏览器会跟 WordPress 协商应该使用何种格式的图片向用户展示。

          怎么确认我正在使用 WebP/AVIF 版本的图片?

          仅仅依靠文件名无法区分图片格式,应该检查浏览器请求的响应部分,如果 Content-Type 的值为 image/avif 说明这张图片正在以 AVIF 格式展示,WebP 也是同理。

          为什么图片看起来有些糊?

          CompressX 插件默认使用有损压缩,建议手动调整为无损压缩选项。

          为什么转换格式这么慢?

          转换速度完全取决于你的服务器配置,简而言之,加钱世界可得~

          为什么 PNG 图片没有被自动转换成 WebP/AVIF ?

          因为 Modern Image Format 暂时还没加上这个功能,具体可以看这里的讨论:https://github.com/WordPress/performance/pull/1421。就目前而言,可以使用 CompressX 插件手动转换 PNG 图片。

        3. 不要钱的博客网站性能优化

          赛博菩萨 Cloudflare 给免费帐户提供了 Cache Rules 功能,除了可以点亮域名后的云朵图标之外,还可以利用 Cache Rules 缓存博客网站的页面,提高访问者的使用体验。

          要给 WordPress 网站启用 Cache Rules 推荐使用下面这个插件:

          Super Page Cache for Cloudflare

          不过在使用这个插件的时候需要注意,这个插件自带的 Cloudflare API Token 权限说明有误,请参考下面的权限列表。

          如果你想我一样还使用了 LiteSpeed Cache 等缓存插件,建议参考 Super Page Cache 的 FAQ 来调整其他关联插件的配置。

          完成插件的安装跟配置后,可以在 Cloudflare 控制台看到由插件创建的 Cache Rule:

          至此,你的网站就可以享受到免费的全球 CDN 加成了。

        4. 使用 cloudflare tunnel 在线上预览本地的开发服务器

          Cloudflare Tunnel 是一项服务,允许开发者将其本地开发服务器通过安全的隧道连接到 Cloudflare 的服务器,并发布到互联网上。这意味着:

          1. 其他人可以直接通过互联网访问你本机的开发服务器
          2. 本地的 HTTP 服务可以自动获得由 Cloudflare 提供的 https 证书

          Cloudflare 在这篇文章中介绍了如何将本地项目发布到互联网,但是对于前端开发服务器的场景来说,缺少一些必要的配置说明,例如,稳定的域名、网页缓存策略等。如果你遇到了开发服务器通过 Cloudflare tunnel 访问后出现了一些诡异的问题,不妨参考下面的步骤来检查一下自己的配置。

          创建本地管理的 tunnel

          在开始之前,请确保你准备好了这些东西:

          • 在本地安装 cloudflared
          • 在 cloudflare 上绑定了自己的域名

          接着,参考这里的说明,创建一个本地管理的 tunnel。完成这里的步骤后,你应该会得到一份 config.yaml 文件,默认存储在 ~/.cloudflared 目录下。

          Cloudflare 的文档中使用了 url 字段配置反向代理,但我建议使用下面的 ingress 配置,这样你可以在同一个配置文件中声明多个服务。

          tunnel: <Tunnel ID>
          credentials-file: /path/to/<Tunnel ID>.json
          
          ingress:
            - hostname: ng-serve.zeeko.dev
              path: /api/.*
              service: http://localhost:3000
            - hostname: ng-serve.zeeko.dev
              service: http://localhost:4200
            # this is a required fallback rule
            - service: http_status:503

          禁用 Cloudfare 缓存

          你的 Cloudflare 帐户很可能默认开启了请求缓存功能,在一些使用场景下,例如,webpack dev server,这个自带的缓存功能会让 dev server 变得很鬼畜,我们需要手动在 Cloudflare 控制面板禁用缓存。

          添加 webpack dev server 白名单

          如果你在使用 webpack dev server,记得把绑定的域名添加到 allowedHosts 中,避免 HMR 失败。