提要

在有动态公网IPV6的情况下,使用DDNS和自定义端口对外提供WEB服务并不难,但是面临两个重要问题:

  • 如果客户端不支持IPV6,怎么办?
  • 需要打开防火墙,暴漏端口,是否不太安全?

对外提供Web服务的方案很多,比如使用端口映射方案,我也曾使用过 fatedier/frp 这样的开源项目,但是还需要另外再维护一台VPS,其带宽和成本都是个问题,另外,端口映射的方案其实也并不安全。

所以我选用了另一种更加适合我目前场景的方案:

腾讯云 Edge One

EdgeOne 的具体介绍可以查看腾讯云的产品介绍,这里就不再赘述,可以简单的把他看作一个更加安全的 CDN 。EdgeOne 的这几个特性非常好的解决了我的需求:

  • 支持IPV6的自定义端口源站:也就是说,EO可以直接回源IPV6地址的指定端口,而客户端访问时,可以直接使用域名,并支持V4和V6双栈。
  • 多个子域名可共用源站组:可以配置一个源站组,然后在各个子域中引入这个源站组。这样,在动态IP地址变更时,只需要更新源站组的IP即可,不必操作多个源站。
  • 支持API更新源站组:可以在服务器上使用自动化脚本来监测IPV6地址变更情况,并在地址变更时自动更新源站组,保障服务可用性。
  • 提供EO节点IP地址:防火墙可以仅对 EdgeOne 节点放通入站请求,规避一些安全问题。

其实 Cloudflare 也提供了类似服务,甚至免费,但是中国大陆地区的服务质量不是特别稳定,我所在的地区测试延迟很高,无法使用。

部署小记

1. 本地服务器上线 WEB 服务

我的本地服务器环境为 Windows Server 2022 ,已经使用 Docker 部署了我的博客,使用宿主机的 60023 端口。

image

2. 为 EdgeOne 节点开放防火墙

腾讯云通过地址 https://api.edgeone.ai/ips 提供EdgeOne的节点IP,并接收参数 versionarea 来指定地区和IPV4/IPV6。

为了能够动态地更新防火墙规则,我将其编写为一个Python脚本,放在计划任务中定期执行:

import requests
import subprocess
import itertools
 
def get_ips(url):
    try:
        response = requests.get(url)
        response.raise_for_status()
        ip_list = response.text.strip().split('\n')
        return ','.join(ip_list)
    except requests.RequestException:
        return None
 
 
def read_previous_ips(filename):
    try:
        with open(filename, 'r') as file:
            return file.read().strip()
    except FileNotFoundError:
        return ""  # 返回空字符串而不是 None
 
 
def write_ips(filename, ips):
    with open(filename, 'w') as file:
        file.write(ips)
 
 
def check_rule_exists(name):
    cmd = f"netsh advfirewall firewall show rule name=\"{name}\""
    result = subprocess.run(cmd, capture_output=True, text=True, shell=True)
    return name in result.stdout
 
 
def update_firewall_rule(name, ips, protocol="TCP", direction="in", action="allow"):
    delete_cmd = f"netsh advfirewall firewall delete rule name=\"{name}\""
    add_cmd = f"netsh advfirewall firewall add rule name=\"{name}\" dir={direction} action={action} protocol={protocol} remoteip={ips}"
    subprocess.run(delete_cmd, shell=True)  # 删除旧规则
    subprocess.run(add_cmd, shell=True)  # 添加新规则
 
 
def add_firewall_rule(name, ips, protocol="TCP", direction="in", action="allow"):
    cmd = f"netsh advfirewall firewall add rule name=\"{name}\" dir={direction} action={action} protocol={protocol} remoteip={ips}"
    subprocess.run(cmd, shell=True)  # 创建规则
 
 
def main():
    ip_versions = ['v4', 'v6']
    areas = ['mainland-china', 'overseas']
    changes_made = False
    for version, area in itertools.product(ip_versions, areas):
        url = f"https://api.edgeone.ai/ips?version={version}&area={area}"
        ips = get_ips(url)
        if not ips:
            continue
        filename = f"{version}_{area}_ips.txt"
        previous_ips = read_previous_ips(filename)
        if ips == previous_ips:
            continue
        rule_name = f"@EdgeOne_{version}_{area}"
        if check_rule_exists(rule_name):
            update_firewall_rule(rule_name, ips)
        else:
            add_firewall_rule(rule_name, ips)
        write_ips(filename, ips)
        changes_made = True
    return changes_made
 
 
if __name__ == "__main__":
    main()

该脚本从地址中读取节点地址,并分别添加以下四条规则:

  • @EdgeOne_v4_mainland-china
  • @EdgeOne_v4_overseas
  • @EdgeOne_v6_mainland-china
  • @EdgeOne_v6_overseas

使用 @ 前缀只是我的个人习惯,用于区别我自定义的规则和系统默认规则。

3. 创建 EdgeOne 站点

4. 配置计划任务动态更新源站组

结语

该方案和内网穿透相比,有一定局限性,比较依赖运行商是否开放IPV6,其流量也很少。但是不局限于内网穿透的vps带宽,并且仅仅用于web服务时,价格相对还算可接受范围。并且提供了一些基础防护和CDN,总的来说,我的使用体验还不错。

sequenceDiagram
    用户-->>+EdgeOne: ipv4发起请求
    EdgeOne-->>+服务器: 公网V6回源
    服务器-->>EdgeOne: 
    EdgeOne-->>+用户: 响应请求
    
    用户->>+EdgeOne: ipv6发起请求
    EdgeOne->>+用户: 返回302
    用户->>+服务器: ipv6发起请求
    服务器->>+用户: 响应请求