# 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`、实例 `publicIpAddress` 和 `isStaticIp`。 5. **健康检查**:直连新的 Static IP。SOCKS5 会完成方法协商、RFC 1929 用户密码认证 和外部 CONNECT;HTTP 代理会发送带 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: ```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 ``` 首次启动会创建数据库和主密钥。数据库存在而主密钥丢失时,程序会拒绝启动,避免 用新密钥覆盖后无法解密原凭据。 从只支持一套全局凭据的旧版本升级时,已配置的 AWS 和 Cloudflare 凭据会在启动迁移 后分别导入为 legacy 账号,旧实例会自动绑定对应账号。导入过程使用现有主密钥解密并 重新加密,不会把密钥明文写入数据库或日志。 ## 管理员账号 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。 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:*`: ```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 与实例里的橙云开关一致; - 同名没有 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`,并使用项目目录下三个独立的 宿主机目录保存数据库、主密钥和备份: ```text data/fluxip-data/ data/fluxip-key/ data/fluxip-backups/ ``` 部署需要 Docker Compose v2(使用 `docker compose` 命令)。启动时,一次性的 `fluxip-init` 服务会拒绝挂载目录中的符号链接或特殊文件,把目录权限收紧并交给 UID/GID `10001` 后退出;长期运行的 `fluxip` 服务仍是非 root 用户。已有数据库、 主密钥和备份不会被初始化服务删除或改写内容。 ```bash cp deploy/fluxip.env.example .env # 编辑 .env,至少设置高熵 ROTATOR_BOOTSTRAP_TOKEN docker compose up -d --build docker compose ps -a 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` 不删除上述宿主机目录。不要手工删除 `data/fluxip-key/master.key`; 数据库存在但主密钥丢失时,已保存的云端凭据将无法解密。 ## 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`:进程存活状态,返回 `status` 和应用版本;不探测 SQLite 或云端账号。 - `GET /readyz`:检查 SQLite 和已启用调度器的运行状态;就绪时返回 HTTP 200,数据库 异常或已启用调度器意外退出时返回 HTTP 503 和 `degraded`。响应中的 `scheduler` 会显示开关、运行状态、最近扫描/派发/异常时间;该接口不会调用 AWS 或 Cloudflare。 - **到期后没有自动轮换**:先检查 `/readyz` 的 `scheduler.enabled` 和 `running`。系统环境 变量优先于 `.env`;请删除意外继承的 `ROTATOR_SCHEDULER_ENABLED=false`,或明确设为 `true` 后重启。已逾期的实例组会在调度器启动后的首轮扫描立即执行。 - **没有默认管理员**:首次打开页面自行创建;远程初始化需 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 连接,这是当前架构的预期边界。 - **主密钥缺失**:恢复与数据库匹配的主密钥,不要删除数据库让程序生成新密钥。 - **Docker 启动提示 `/app/secrets/master.key` 无权限**:拉取最新代码后执行 `docker compose down`,再执行 `docker compose up -d --build --force-recreate`,确认 `docker compose ps -a` 中 `fluxip-init` 为 `Exited (0)`。这两个命令不会删除持久化 目录。旧版本可先停止服务,再将 `data/fluxip-data`、`data/fluxip-key`、 `data/fluxip-backups` 的所有者递归改为 `10001:10001`;不要删除或单独替换已有主密钥。 ## 安全清单 - Uvicorn 始终使用单 worker、单副本。 - WebUI 只开放给受信网络,公网入口使用 HTTPS。 - AWS 使用专用最小权限身份,Cloudflare Token 只授权必要 Zone。 - 每个 Cloudflare A 记录由一个托管实例独占;普通 SOCKS5/HTTP CONNECT 使用 DNS only。 - 代理服务启用身份认证并限制入站来源。 - 为 Static IP 配额预留至少一个安全轮换余量。 - 定期检查待处理、待清理任务和未附加 Static IP 费用。 - SQLite 与主密钥成对备份,备份异机存放并加密。 - 不在日志、截图、工单、Git 或最终交付文档中发送真实密钥。