FluxIP/README.md

21 KiB
Raw Blame History

FluxIP

FluxIP 是一个使用 Python、FastAPI 和 SQLite 开发的 AWS Lightsail Static IP 轮换控制器。它可以管理多台 Lightsail 实例,把每台实例与一个 Cloudflare A 记录 组成一个托管目标再通过实例组统一设置轮换周期。A 记录默认使用 DNS only也可按 实例选择橙云模式。

轮换过程中不会停止或重启 Lightsail。FluxIP 会分配新的 Static IP解绑旧地址 把新地址附加到仍在运行的实例,验证 SOCKS5 或 HTTP 代理与 Cloudflare DNS等待释放宽限期 最后从 AWS 账号释放旧 Static IP。

配置、账号资料、任务状态和历史记录保存在 SQLite。可以分别维护多套 AWS 与 Cloudflare 账号每个托管实例独立选择要使用的两套账号。AWS 密钥与 Cloudflare API Token 以及代理用户名、密码使用本机主密钥加密后存储API 和 WebUI 不会返回密钥明文。

重要边界

  • 每台 Lightsail 实例只能附加一个 Static IP。地址切换不是双 IP 并行切流。
  • 解绑旧地址和附加新地址之间会有短暂切换窗口。实例进程不会退出,但已有 SOCKS5 TCP 连接会断开,需要客户端重连。
  • DNS only 模式下 Cloudflare 只负责 DNS 解析。DNS 缓存仍可能在 TTL 内返回旧地址, FluxIP 不能提供零中断或连接迁移。
  • 普通 Cloudflare 橙云不能转发 SOCKS5、通用 HTTP CONNECT 或 10808 等任意 TCP 端口。只有已经单独配置 Cloudflare Spectrum 等 TCP 产品时才应开启橙云FluxIP 只修改 DNS 记录的 proxied 状态,不会创建 Spectrum 应用。
  • 橙云记录的 API 回读只能证明配置已经写入,不能证明所有 Cloudflare 边缘节点已经 切换源站;默认释放宽限期也不是 Spectrum 端到端可用性的保证。
  • 旧 Static IP 一旦执行 ReleaseStaticIp 就不能找回。FluxIP 只会在新地址、DNS 和 可选代理认证与 CONNECT 检查通过后释放它。
  • AWS 默认每个区域最多 5 个 Static IP配额可以申请提高。安全轮换采用 “先分配新地址、后释放旧地址”,因此同一区域需要至少一个空余配额。
  • 调度器运行在 Web 进程内,只支持一个 Uvicorn worker 和一个应用副本。
  • 生产环境建议把控制器部署到独立的长期在线主机。虽然 Static IP 切换不会关闭 目标实例,但同机部署会使 WebUI 管理连接也受地址切换影响。

资源模型

一个托管实例包含:

  • 显示名称;
  • AWS 账号、从内置中文名称与图标目录选择的 Lightsail 区域,以及 Lightsail 实例名称;
  • Cloudflare 账号、Zone ID、唯一的完整 A 记录名和橙云开关;
  • SOCKS5/HTTP 代理链接、端口、健康检查开关和超时;
  • 旧地址释放宽限期;
  • 是否允许参与手动或分组轮换。

一个实例组包含若干托管实例、启用状态和轮换间隔。到期时,组内已启用实例按照 成员顺序逐台轮换。串行执行只额外占用一个 Static IP 配额,也避免同时切断多台 代理。下次执行时间从整个批次结束后重新计算,两个批次之间至少间隔一个完整周期。 当前系统全局只允许一个轮换批次或 DNS 同步任务执行。

AWS 与 Cloudflare 账号分别建档,同一账号可以由多个实例复用,不同实例也可以选择 完全独立的账号组合。数据库会阻止以下冲突:

  • 同一个 AWS 账号下的同一区域和 Lightsail 实例被重复管理;
  • 同一个 Cloudflare 账号下的同一记录被多个托管实例使用;
  • 同一个实例同时加入多个实例组。

账号的新增、修改和删除与实例配置共用轮换写锁。轮换批次或 DNS 同步任务执行期间 通常不能写入账号;任务进入待处理或待清理后,只允许替换当前运行项所绑定账号的 恢复密钥,不能改名、切换认证模式或操作其他账号。仍被活动实例引用的账号不能删除, 必须先把实例改绑到其他账号或删除相关实例。

轮换流程

一次实例轮换按以下顺序执行:

  1. 预检:确认 Lightsail 正在运行且有公网 IPv4读取当前附加的 Static IP 确认 Cloudflare A 记录的 IP、代理模式和 TTL 与实例配置一致。
  2. 分配:使用确定性任务资源名调用 AllocateStaticIp,轮询 AWS operation 再读取资源确认新 Static IP 已存在且属于正确区域。
  3. 解绑:如果实例原本已有 Static IP调用 DetachStaticIp 并回读确认旧地址 已解绑。实例原本使用动态 IP 时,这一步用于首次 Static IP 纳管,不存在旧静态 资源需要删除。
  4. 附加:调用 AttachStaticIp 把新地址附加到目标实例,并同时核对 Static IP 的 attachedTo、实例 publicIpAddressisStaticIp
  5. 健康检查:直连新的 Static IP。SOCKS5 会完成方法协商、RFC 1929 用户密码认证 和外部 CONNECTHTTP 代理会发送带 Basic 认证的 CONNECT。粘贴链接中的域名不会 用于 DNS 切换前的源站检查,避免误测到旧 IP。
  6. 更新 DNS:把 Cloudflare A 记录切换到新地址;灰云写入 proxied=false、TTL 60 橙云写入 proxied=true、TTL Automatic然后回读验证。
  7. 释放宽限:至少等待 60 秒,默认 75 秒。旧地址此时已解绑,宽限期不会让旧 客户端继续访问,但可以避免旧地址在 DNS 缓存期立即被 AWS 分配给其他用户。
  8. 删除旧地址再次验证新地址仍附加、DNS 仍指向新地址,并按配置复查代理; 此时同时探测新 Static IP 和实际 A 记录域名,确保 DNS only 或 Spectrum 入口真正 可用后再调用 ReleaseStaticIp,以 GetStaticIp 返回不存在作为删除成功依据。

AWS 写操作是异步的。FluxIP 会保存任务阶段并轮询 GetOperation;进程意外退出后, 会根据 SQLite 状态和 AWS/Cloudflare 实际状态续跑,而不是仅根据上次 HTTP 响应猜测。 预检失败会直接结束任务;无法安全自动恢复的切换异常进入“待处理”;旧地址或回滚 新地址释放失败进入“待清理”。管理员应先核对云端状态,再更新凭据、继续或放弃任务。

如果旧 Static IP 仍存在、DNS 仍为旧地址且没有被外部修改FluxIP 会在新地址附加、 健康检查或 DNS 更新失败后自动执行持久化回滚:解绑新地址、重新附加旧地址、验证旧 代理与 DNS 路由,再删除本次分配的新 Static IP。回滚过程中每一步都会写入 SQLite 进程重启后可以幂等续跑。DNS 已经确认切到新地址时会继续向前验证,不会制造新旧 DNS 缓存分裂DNS 指向第三方地址、记录缺失或代理模式被外部改变时则停止自动操作。首次从动态 IP 纳管没有可重新附加的旧静态资源,因此不能自动回滚,只能保留现场并进入待处理。

运行要求

  • Python 3.11 或更高版本;
  • 可访问 AWS Lightsail API 和 Cloudflare API
  • 目标 Lightsail 的 SOCKS5 或 HTTP 代理服务已经运行,端口和防火墙规则正确;
  • Cloudflare 中使用独占的 A 记录;普通代理服务使用 DNS only橙云需要已配置 Spectrum
  • AWS Static IP 配额具有轮换余量。

不允许使用多 worker

--workers 1

不要运行多个 Compose 副本、多个 systemd 实例或多个指向同一数据库的控制器。

直接运行源码

Windows PowerShell

cd "D:\project\S5-IP自动轮换"
py -3.11 -m venv .venv
.\.venv\Scripts\python.exe -m pip install --upgrade pip setuptools
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
if (-not (Test-Path .env)) { Copy-Item .env.example .env }
.\.venv\Scripts\python.exe -m app

打开 http://127.0.0.1:8787。关闭服务时在运行窗口按 Ctrl+C

Linux 或 macOS

cd /path/to/fluxip
python3 -m venv .venv
.venv/bin/python -m pip install --upgrade pip setuptools
.venv/bin/python -m pip install -r requirements.txt
[ -f .env ] || cp .env.example .env
.venv/bin/python -m app

python -m app 会读取 .env 中的 ROTATOR_HOSTROTATOR_PORT 和日志配置,并固定 使用一个 worker。需要临时覆盖时也可以继续使用显式的 Uvicorn 命令。

默认数据文件为:

data/fluxip.db
data/master.key

首次启动会创建数据库和主密钥。数据库存在而主密钥丢失时,程序会拒绝启动,避免 用新密钥覆盖后无法解密原凭据。

从只支持一套全局凭据的旧版本升级时,已配置的 AWS 和 Cloudflare 凭据会在启动迁移 后分别导入为 legacy 账号,旧实例会自动绑定对应账号。导入过程使用现有主密钥解密并 重新加密,不会把密钥明文写入数据库或日志。

管理员账号

FluxIP 没有默认管理员账号或默认密码

第一次打开 WebUI 时自行创建管理员,密码至少 10 位。未配置 ROTATOR_BOOTSTRAP_TOKEN 时,首次创建管理员只允许来自控制器回环地址;容器、远程 主机或公网反向代理部署应提前配置至少 32 字节高熵 Bootstrap Token。

生成示例:

python -c "import secrets; print(secrets.token_urlsafe(32))"

管理员创建完成后,可以从环境配置中删除 Bootstrap Token 并重启服务。不要把 Bootstrap Token、云端密钥或数据库主密钥提交到 Git。

WebUI 首次配置

  1. 创建管理员并登录。
  2. 打开“账号”,新增 AWS 账号:选择默认凭据链,或填写专用 Access Key ID、Secret Access Key临时凭据还需要 Session Token。
  3. 继续新增 Cloudflare 账号并填写 API Token。可以为不同 AWS 或 Cloudflare 租户创建 多个独立账号资料。
  4. 新建托管实例,选择已保存的 AWS 账号和 Cloudflare 账号;从带图标和中文名称的 区域目录选择 AWS Region再填写 Lightsail 实例名称、Zone ID 和完整 A 记录名。 代理测活链接可直接粘贴以下格式,协议、端口和认证会自动解析: socks5://用户:密码@域名:端口http://用户:密码@域名:端口socks5://域名:端口@用户:密码。用户名或密码包含 @: 时应使用 URL 编码。 链接里的域名必须与该实例的 A 记录完整域名一致。
  5. 执行实例连接测试,确认所选 AWS 账号可以读取实例公网地址,所选 Cloudflare 账号 可以读取目标记录。
  6. 如 DNS 尚未指向实例当前地址,先执行 DNS 同步。
  7. 对单个实例执行一次手动轮换,核对新 Static IP、代理 CONNECT 和 DNS。
  8. 创建实例组、添加成员、设置轮换间隔并启用计划。

配置更新使用版本号进行并发检查。如果页面数据已被另一会话修改,刷新后再提交。 账号、实例和实例组的删除是软删除,不会自动删除 Lightsail 实例或 Cloudflare Zone。

轮换批次或 DNS 同步执行中禁止新增、删除账号,也禁止修改实例或实例组。任务因凭据 失效进入待处理或待清理后,可以只替换当前运行项所绑定账号的恢复密钥;账号名称和 认证模式仍被锁定。此时也可以为当前实例重新粘贴相同域名、协议和端口的代理链接, 只替换加密的代理用户名与密码;其他实例配置仍被锁定。审计日志不记录密钥内容。

AWS 最小权限

建议使用专用 IAM 身份或角色,不要使用 AWS 根账号密钥。FluxIP 当前需要以下 Lightsail API

  • GetInstance
  • GetStaticIp
  • GetStaticIps
  • GetOperation
  • AllocateStaticIp
  • AttachStaticIp
  • DetachStaticIp
  • ReleaseStaticIp

这些调用包含新资源分配、全区域静态地址查询和异步 operation 查询,不能全部稳定地 约束到一个预先存在的资源 ARN。下面策略使用精确 Action 列表和区域条件,避免授予 lightsail:*

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "FluxIPStaticIpRotation",
      "Effect": "Allow",
      "Action": [
        "lightsail:GetInstance",
        "lightsail:GetStaticIp",
        "lightsail:GetStaticIps",
        "lightsail:GetOperation",
        "lightsail:AllocateStaticIp",
        "lightsail:AttachStaticIp",
        "lightsail:DetachStaticIp",
        "lightsail:ReleaseStaticIp"
      ],
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "aws:RequestedRegion": [
            "us-east-1",
            "ap-southeast-1"
          ]
        }
      }
    }
  ]
}

把区域列表改成实际使用的区域。单区域部署只保留一个值。组织若使用 SCP 或权限边界, 还需在相应层级允许这些 Action。权限不足时不要临时改成管理员权限应根据错误信息 补充缺失 Action 或区域。

Static IP 配额

AWS 默认每个账号、每个 Lightsail 区域最多 5 个 Static IP配额可申请提高。假设同一 区域已有 N 台托管实例各占一个 Static IP安全轮换至少需要 N + 1 的配额。实例组 逐台执行,因此通常只需要一个临时余量。

如果 5 个地址已经全部占用FluxIP 不会先删除旧地址来腾配额。那样会失去恢复余地, 也可能重新获得相同地址。应先在 AWS 配额页面申请提高限制,或减少该区域的现用地址。 未附加的 Static IP 可能产生费用;待处理和待清理任务应尽快核对。

Cloudflare 最小权限

创建专用 API Token不要使用 Global API Key。Token 只包含托管目标所在 Zone并授予

  • Zone / Zone / Read
  • Zone / DNS / Edit

多个实例跨多个 Zone 时,把这些特定 Zone 都加入同一 Token或改用覆盖所需 Zone 的 受控 Token。所有目标记录必须满足

  • 类型为 A
  • Proxy status 与实例里的橙云开关一致;
  • 同名没有 CNAME、AAAA 或第二条 A 记录;
  • 记录由 FluxIP 独占,不由其他自动化同时修改。

默认应使用 DNS only灰云。Cloudflare 普通橙云不代理任意 SOCKS5、通用 HTTP CONNECT 或 10808 等任意 TCP 端口。只有先在 Cloudflare 配置并开通 Spectrum 等对应 TCP 服务后才开启 FluxIP 的橙云开关;这个开关本身不会创建 Spectrum 应用。

FluxIP 写入前会核对当前记录,写入后再回读,但 Cloudflare 常规 DNS 接口不提供跨 AWS 与 DNS 的原子事务。需要人工接管时,先禁用相关实例组计划,并等待当前任务完成 或在任务详情明确处理异常现场。

环境变量

应用读取项目根目录 .env,变量统一使用 ROTATOR_ 前缀。

变量 默认值 说明
ROTATOR_ENVIRONMENT development production 会关闭 API 文档
ROTATOR_HOST 127.0.0.1 服务监听地址
ROTATOR_PORT 8787 服务端口
ROTATOR_DATABASE_PATH data/fluxip.db SQLite 文件
ROTATOR_MASTER_KEY_FILE data/master.key 凭据加密主密钥文件
ROTATOR_MASTER_KEY 外部注入的 URL-safe Base64 主密钥,优先于文件
ROTATOR_BOOTSTRAP_TOKEN 远程首次初始化令牌
ROTATOR_COOKIE_SECURE false HTTPS 生产环境应设为 true
ROTATOR_SESSION_DAYS 7 管理员会话有效期
ROTATOR_ALLOWED_ORIGINS 本机 8787 允许的完整 Origin逗号分隔
ROTATOR_TRUSTED_HOSTS localhost,127.0.0.1 接受的 Host逗号分隔
ROTATOR_TRUSTED_PROXIES 127.0.0.1,::1 可提交转发头的代理 IP/CIDR
ROTATOR_SCHEDULER_ENABLED true 是否执行实例组到期扫描
ROTATOR_SCHEDULER_TICK_SECONDS 10 扫描间隔2 到 60 秒
ROTATOR_LOG_LEVEL INFO 日志级别
ROTATOR_EXTERNAL_TIMEOUT_SECONDS 20 外部请求基础超时5 到 60 秒

生产示例见 deploy/fluxip.env.example。不要把生产密钥直接写进仓库内的示例文件。

Docker Compose

Docker Compose 默认把 WebUI 发布到 127.0.0.1:8787,并使用三个独立命名卷保存 数据库、主密钥和备份:

cp deploy/fluxip.env.example .env
# 编辑 .env至少设置高熵 ROTATOR_BOOTSTRAP_TOKEN
docker compose up -d --build
docker compose ps
curl http://127.0.0.1:8787/readyz

Windows PowerShell

Copy-Item deploy\fluxip.env.example .env
docker compose up -d --build
Invoke-RestMethod http://127.0.0.1:8787/readyz

常用命令:

docker compose logs -f --tail=200 fluxip
docker compose restart fluxip
docker compose down

docker compose down 不删除命名卷。除非已验证备份并确定要销毁全部数据,否则不要 执行 docker compose down -v

Linux systemd 与 HTTPS

示例假定代码位于 /opt/fluxip

sudo useradd --system --home /var/lib/fluxip --shell /usr/sbin/nologin fluxip
sudo install -d -o root -g root -m 0755 /opt/fluxip /etc/fluxip
sudo cp -a app migrations deploy requirements.txt pyproject.toml README.md /opt/fluxip/
sudo python3 -m venv /opt/fluxip/.venv
sudo /opt/fluxip/.venv/bin/python -m pip install -r /opt/fluxip/requirements.txt
sudo install -o root -g root -m 0600 deploy/fluxip.env.example /etc/fluxip/fluxip.env
sudo editor /etc/fluxip/fluxip.env
sudo install -o root -g root -m 0644 deploy/fluxip.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now fluxip.service
curl http://127.0.0.1:8787/readyz

查看日志:

sudo journalctl -u fluxip.service -n 200 --no-pager

deploy/nginx-fluxip.conf.example 是本机 Nginx HTTPS 反向代理示例。启用后设置:

ROTATOR_COOKIE_SECURE=true
ROTATOR_ALLOWED_ORIGINS=https://controller.example.com
ROTATOR_TRUSTED_HOSTS=controller.example.com,127.0.0.1
ROTATOR_TRUSTED_PROXIES=127.0.0.1,::1

若代理不在本机,只填写代理的精确 IP/CIDR并同步限制 Uvicorn --forwarded-allow-ips 和防火墙。不要信任 0.0.0.0/0::/0 的转发头。

备份与恢复

数据库和主密钥必须成对恢复,但不应默认放进同一个未加密归档:

  • 数据库包含管理员、实例、实例组、任务历史和加密凭据;
  • master.key 可以解密云端凭据,必须单独保存在密码库或加密介质;
  • 丢失主密钥无法恢复凭据,泄露主密钥会使数据库密文失去保护;
  • 不要提交数据库、主密钥、.env 或备份包。

在线备份只打包 SQLite

python3 deploy/backup.py \
  --database /var/lib/fluxip/fluxip.db \
  --output-dir /var/backups/fluxip \
  --keep-last 30

Docker

docker compose exec -T fluxip python /app/deploy/backup.py \
  --database /app/data/fluxip.db \
  --output-dir /app/backups \
  --keep-last 30
docker compose cp fluxip:/app/backups/. ./fluxip-backups

systemd 定时备份示例为 deploy/fluxip-backup.servicedeploy/fluxip-backup.timer。备份后应验证同名 .sha256,并定期在隔离环境实际恢复。

只有目标目录本身已经加密时,才使用 --allow-plaintext-key-archive 创建包含主密钥的 完整归档。恢复时先停止 FluxIP同时恢复匹配的数据库和主密钥再启动并验证 /readyz、管理员登录、账号资料状态、实例组计划和云端连接。

健康检查与排错

  • GET /healthz:进程存活状态,返回 status 和应用版本;不探测 SQLite 或云端账号。
  • GET /readyz:检查 SQLite 是否可用;就绪时返回 HTTP 200数据库异常时返回 HTTP 503 和 degraded。该接口不会调用 AWS 或 Cloudflare。
  • 没有默认管理员:首次打开页面自行创建;远程初始化需 Bootstrap Token。
  • 账号连接测试失败:确认实例选中了正确类型的 AWS 与 Cloudflare 账号,账号密钥 完整,并且 Region、Zone ID 和记录名属于对应账号。
  • AWS AccessDenied:核对所选 AWS 账号的上述 8 个 Lightsail Action 和目标区域, 不要直接授予管理员。
  • Static IP 配额不足:提高该区域配额;不要先释放正在使用的旧地址。
  • 找不到附加的旧地址确认实例、Static IP 和区域一致,避免同时在控制台人工修改。
  • 代理检查失败:确认服务监听公网接口、端口开放、链接协议与服务一致,并重新粘贴 正确的用户名、密码。探测会实际建立到 1.1.1.1:443 的 CONNECT。
  • DNS 冲突:删除同名 CNAME、AAAA 或多余 A 记录,并让橙云状态与实例配置一致。
  • 任务待处理:先核对新旧 Static IP 的真实附加状态和 DNS再更新恢复凭据或继续。
  • 任务待清理:新地址通常已经生效,但旧地址尚未成功释放,应尽快处理以避免费用。
  • 客户端短暂掉线Static IP 替换会终止原 TCP 连接,这是当前架构的预期边界。
  • 主密钥缺失:恢复与数据库匹配的主密钥,不要删除数据库让程序生成新密钥。

安全清单

  • Uvicorn 始终使用单 worker、单副本。
  • WebUI 只开放给受信网络,公网入口使用 HTTPS。
  • AWS 使用专用最小权限身份Cloudflare Token 只授权必要 Zone。
  • 每个 Cloudflare A 记录由一个托管实例独占;普通 SOCKS5/HTTP CONNECT 使用 DNS only。
  • 代理服务启用身份认证并限制入站来源。
  • 为 Static IP 配额预留至少一个安全轮换余量。
  • 定期检查待处理、待清理任务和未附加 Static IP 费用。
  • SQLite 与主密钥成对备份,备份异机存放并加密。
  • 不在日志、截图、工单、Git 或最终交付文档中发送真实密钥。