# FluxIP FluxIP 是一个使用 Python、FastAPI 和 SQLite 开发的 AWS Lightsail Static IP 轮换控制器。它可以管理多台 Lightsail 实例,把每台实例与一个 Cloudflare DNS-only A 记录组成一个托管目标,再通过实例组统一设置轮换周期。 轮换过程中不会停止或重启 Lightsail。FluxIP 会分配新的 Static IP,解绑旧地址, 把新地址附加到仍在运行的实例,验证 SOCKS5 与 Cloudflare DNS,等待释放宽限期, 最后从 AWS 账号释放旧 Static IP。 配置、任务状态和历史记录保存在 SQLite。AWS 密钥与 Cloudflare API Token 使用本机 主密钥加密后存储。 ## 重要边界 - 每台 Lightsail 实例只能附加一个 Static IP。地址切换不是双 IP 并行切流。 - 解绑旧地址和附加新地址之间会有短暂切换窗口。实例进程不会退出,但已有 SOCKS5 TCP 连接会断开,需要客户端重连。 - Cloudflare 只负责 DNS 解析。DNS 缓存仍可能在 TTL 内返回旧地址,FluxIP 不能提供 零中断或连接迁移。 - 旧 Static IP 一旦执行 `ReleaseStaticIp` 就不能找回。FluxIP 只会在新地址、DNS 和 可选 SOCKS5 检查通过后释放它。 - AWS 默认每个区域最多 5 个 Static IP,配额可以申请提高。安全轮换采用 “先分配新地址、后释放旧地址”,因此同一区域需要至少一个空余配额。 - 调度器运行在 Web 进程内,只支持一个 Uvicorn worker 和一个应用副本。 - 生产环境建议把控制器部署到独立的长期在线主机。虽然 Static IP 切换不会关闭 目标实例,但同机部署会使 WebUI 管理连接也受地址切换影响。 ## 资源模型 一个托管实例包含: - 显示名称; - AWS 区域和 Lightsail 实例名称; - Cloudflare Zone、唯一的完整 A 记录名; - SOCKS5 端口、健康检查开关和超时; - 旧地址释放宽限期; - 是否允许参与手动或分组轮换。 一个实例组包含若干托管实例、启用状态和轮换间隔。到期时,组内已启用实例按照 成员顺序逐台轮换。串行执行只额外占用一个 Static IP 配额,也避免同时切断多台 代理。下次执行时间从整个批次结束后重新计算,两个批次之间至少间隔一个完整周期。 当前系统全局只允许一个轮换批次或 DNS 同步任务执行。 所有托管实例共用一套 AWS 凭据和一枚 Cloudflare API Token。数据库会阻止以下 冲突: - 同一个 AWS 区域和 Lightsail 实例被重复管理; - 同一个 Cloudflare 记录被多个托管实例使用; - 同一个实例同时加入多个实例组。 ## 轮换流程 一次实例轮换按以下顺序执行: 1. **预检**:确认 Lightsail 正在运行且有公网 IPv4;读取当前附加的 Static IP; 确认 Cloudflare A 记录是 DNS-only 并与当前公网 IP 一致。 2. **分配**:使用确定性任务资源名调用 `AllocateStaticIp`,轮询 AWS operation, 再读取资源确认新 Static IP 已存在且属于正确区域。 3. **解绑**:如果实例原本已有 Static IP,调用 `DetachStaticIp` 并回读确认旧地址 已解绑。实例原本使用动态 IP 时,这一步用于首次 Static IP 纳管,不存在旧静态 资源需要删除。 4. **附加**:调用 `AttachStaticIp` 把新地址附加到目标实例,并同时核对 Static IP 的 `attachedTo`、实例 `publicIpAddress` 和 `isStaticIp`。 5. **健康检查**:按实例配置连接新地址的 SOCKS5 端口,并执行 SOCKS5 协议握手。 6. **更新 DNS**:把 Cloudflare A 记录切换到新地址,强制 `proxied=false`、TTL 60, 然后回读验证。 7. **释放宽限**:至少等待 60 秒,默认 75 秒。旧地址此时已解绑,宽限期不会让旧 客户端继续访问,但可以避免旧地址在 DNS 缓存期立即被 AWS 分配给其他用户。 8. **删除旧地址**:再次验证新地址仍附加、DNS 仍指向新地址,并按配置复查 SOCKS5; 随后调用 `ReleaseStaticIp`,以 `GetStaticIp` 返回不存在作为删除成功依据。 AWS 写操作是异步的。FluxIP 会保存任务阶段并轮询 `GetOperation`;进程意外退出后, 会根据 SQLite 状态和 AWS/Cloudflare 实际状态续跑,而不是仅根据上次 HTTP 响应猜测。 预检失败会直接结束任务;无法安全自动恢复的切换异常进入“待处理”;旧地址或回滚 新地址释放失败进入“待清理”。管理员应先核对云端状态,再更新凭据、继续或放弃任务。 如果旧 Static IP 仍存在、DNS 仍为旧地址且没有被外部修改,FluxIP 会在新地址附加、 健康检查或 DNS 更新失败后自动执行持久化回滚:解绑新地址、重新附加旧地址、验证旧 SOCKS5 与 DNS 路由,再删除本次分配的新 Static IP。回滚过程中每一步都会写入 SQLite, 进程重启后可以幂等续跑。DNS 已经确认切到新地址时会继续向前验证,不会制造新旧 DNS 缓存分裂;DNS 指向第三方地址、记录缺失或开启代理时则停止自动操作。首次从动态 IP 纳管没有可重新附加的旧静态资源,因此不能自动回滚,只能保留现场并进入待处理。 ## 运行要求 - Python 3.11 或更高版本; - 可访问 AWS Lightsail API 和 Cloudflare API; - 目标 Lightsail 的 SOCKS5 服务已经运行,端口和防火墙规则正确; - Cloudflare 中使用独占的 DNS-only A 记录; - AWS Static IP 配额具有轮换余量。 不允许使用多 worker: ```text --workers 1 ``` 不要运行多个 Compose 副本、多个 systemd 实例或多个指向同一数据库的控制器。 ## 直接运行源码 ### Windows PowerShell ```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 ```bash 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_HOST`、`ROTATOR_PORT` 和日志配置,并固定 使用一个 worker。需要临时覆盖时也可以继续使用显式的 Uvicorn 命令。 默认数据文件为: ```text data/fluxip.db data/master.key ``` 首次启动会创建数据库和主密钥。数据库存在而主密钥丢失时,程序会拒绝启动,避免 用新密钥覆盖后无法解密原凭据。 ## 管理员账号 FluxIP **没有默认管理员账号或默认密码**。 第一次打开 WebUI 时自行创建管理员,密码至少 10 位。未配置 `ROTATOR_BOOTSTRAP_TOKEN` 时,首次创建管理员只允许来自控制器回环地址;容器、远程 主机或公网反向代理部署应提前配置至少 32 字节高熵 Bootstrap Token。 生成示例: ```bash 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。填写 Cloudflare API Token。 3. 新建托管实例,填写区域、Lightsail 实例名称、Zone 和完整 A 记录名。 4. 执行实例连接测试,确认 AWS 实例公网地址和 Cloudflare 记录状态。 5. 如 DNS 尚未指向实例当前地址,先执行 DNS 同步。 6. 对单个实例执行一次手动轮换,核对新 Static IP、SOCKS5 和 DNS。 7. 创建实例组、添加成员、设置轮换间隔并启用计划。 配置更新使用版本号进行并发检查。如果页面数据已被另一会话修改,刷新后再提交。 实例和实例组的删除是软删除,不会自动删除 Lightsail 实例或 Cloudflare Zone。 轮换执行中通常禁止修改共享凭据、实例或实例组。任务因凭据失效进入待处理或待清理 后,可以只替换恢复凭据;审计日志只记录被更新的字段名,不记录密钥内容。 ## AWS 最小权限 建议使用专用 IAM 身份或角色,不要使用 AWS 根账号密钥。FluxIP 当前需要以下 Lightsail API: - `GetInstance` - `GetStaticIp` - `GetStaticIps` - `GetOperation` - `AllocateStaticIp` - `AttachStaticIp` - `DetachStaticIp` - `ReleaseStaticIp` 这些调用包含新资源分配、全区域静态地址查询和异步 operation 查询,不能全部稳定地 约束到一个预先存在的资源 ARN。下面策略使用精确 Action 列表和区域条件,避免授予 `lightsail:*`: ```json { "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 为 **DNS only(灰云)**; - 同名没有 CNAME、AAAA 或第二条 A 记录; - 记录由 FluxIP 独占,不由其他自动化同时修改。 Cloudflare 橙云不代理任意 SOCKS5 TCP 流量。客户端应直接连接域名和实际 SOCKS5 端口,例如 `s5.example.com:1080`。 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`,并使用三个独立命名卷保存 数据库、主密钥和备份: ```bash 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: ```powershell Copy-Item deploy\fluxip.env.example .env docker compose up -d --build Invoke-RestMethod http://127.0.0.1:8787/readyz ``` 常用命令: ```bash 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`: ```bash 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 ``` 查看日志: ```bash sudo journalctl -u fluxip.service -n 200 --no-pager ``` `deploy/nginx-fluxip.conf.example` 是本机 Nginx HTTPS 反向代理示例。启用后设置: ```dotenv 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: ```bash python3 deploy/backup.py \ --database /var/lib/fluxip/fluxip.db \ --output-dir /var/backups/fluxip \ --keep-last 30 ``` Docker: ```bash 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.service` 和 `deploy/fluxip-backup.timer`。备份后应验证同名 `.sha256`,并定期在隔离环境实际恢复。 只有目标目录本身已经加密时,才使用 `--allow-plaintext-key-archive` 创建包含主密钥的 完整归档。恢复时先停止 FluxIP,同时恢复匹配的数据库和主密钥,再启动并验证 `/readyz`、管理员登录、共享凭据状态、实例组计划和云端连接。 ## 健康检查与排错 - `GET /healthz`:进程存活状态。 - `GET /readyz`:SQLite 就绪状态,正常返回 HTTP 200。 - **没有默认管理员**:首次打开页面自行创建;远程初始化需 Bootstrap Token。 - **AWS AccessDenied**:核对上述 8 个 Lightsail Action 和目标区域,不要直接授予管理员。 - **Static IP 配额不足**:提高该区域配额;不要先释放正在使用的旧地址。 - **找不到附加的旧地址**:确认实例、Static IP 和区域一致,避免同时在控制台人工修改。 - **SOCKS5 检查失败**:确认服务监听公网接口、端口开放、协议握手可用。 - **DNS 冲突**:删除同名 CNAME、AAAA 或多余 A 记录,并关闭 Cloudflare 代理。 - **任务待处理**:先核对新旧 Static IP 的真实附加状态和 DNS,再更新恢复凭据或继续。 - **任务待清理**:新地址通常已经生效,但旧地址尚未成功释放,应尽快处理以避免费用。 - **客户端短暂掉线**:Static IP 替换会终止原 TCP 连接,这是当前架构的预期边界。 - **主密钥缺失**:恢复与数据库匹配的主密钥,不要删除数据库让程序生成新密钥。 ## 安全清单 - Uvicorn 始终使用单 worker、单副本。 - WebUI 只开放给受信网络,公网入口使用 HTTPS。 - AWS 使用专用最小权限身份,Cloudflare Token 只授权必要 Zone。 - 每个 Cloudflare A 记录由一个托管实例独占并保持 DNS-only。 - SOCKS5 服务启用身份认证并限制入站来源。 - 为 Static IP 配额预留至少一个安全轮换余量。 - 定期检查待处理、待清理任务和未附加 Static IP 费用。 - SQLite 与主密钥成对备份,备份异机存放并加密。 - 不在日志、截图、工单、Git 或最终交付文档中发送真实密钥。