Files
OpenFlare/docs/en/design/cloudflare-pointing.md
T
ryan 454542c1d0 docs(i18n): 恢复并补齐英文版 vitepress,README 默认改为英文
- README 默认英文:README.en.md → README.md(英文为默认),中文移至 README.zh-CN.md,语言切换链接同步
- 恢复被删除的 docs/en/ 英文文档(git 历史 cc5e53c5^),删除 4 篇已废弃文件
- 英文导航 config.ts 对齐中文结构(新增 Deployment/Changelog 侧栏,同步 Guide/Design 条目)
- 翻译 15 篇中文新增文档:guide 5 篇(certificates/pages-usage/proxy-config/uptime-kuma/zone-domain-migration)+ design 10 篇(zone-design/cloudflare-pointing/waf-orchestration/origin-error-page/edge-cache-design/pages-design/logstore/kuma-design/login-captcha/observability 三篇)
- en 首页更新(新增 Pages 特性、tagline 同步);changelog 英文入口指向中文版
- vitepress 构建验证:43 个英文页面全部渲染

注意:29 篇旧英文文档为恢复版,部分内容(如 deployment/server、reference/configuration)可能落后于中文,需后续逐篇同步
2026-08-16 23:18:29 +08:00

224 lines
9.9 KiB
Markdown

# Cloudflare DNS Pointing Design
## Goals
Point **ZoneDomains (explicit FQDNs)** in OpenFlare to edge node IPs quickly via the Cloudflare API, replacing manual A-record edits in the CF console. Users organize domains into **pointing groups**: each group configures a primary node, a backup node, and a default orange-cloud (proxied) policy; members can override orange-cloud individually. The system treats the database tables as the desired state and idempotently syncs remote DNS.
This module is an **optional integration capability**; it does not turn Zones into an authoritative DNS control plane. Zones still only handle root-domain boundaries, domains, certificates, and reverse-proxy associations; A record create/update/delete is driven by this module through Cloudflare.
## Scope and Phasing
### Phase 1 (this design's scope)
* Sidebar **Cloudflare** entry with a Token-ready gate
* Connection config: import from an existing DNS account **or** standalone entry within the module (mixed sources), stored encrypted
* Pointing group CRUD: primary node, backup node (reserved), group default orange-cloud
* Member management: add/remove at the granularity of `zone_domain_id`; member-level orange-cloud
* Sync: write each member as a **single A record** on Cloudflare → current active node IPv4
* Triggers: manual sync, member add, node/orange-cloud change, node IP change enqueue
* Async task batch sync; member sync status with readable errors
### Phase 2
* Agent heartbeat offline detection of primary node failure → `active_node` switches to backup → whole-group auto sync
* Optional auto failback, failure notification push
### Explicitly Out of Scope (later or permanent)
* Multiple parallel Cloudflare accounts (one global connection config)
* AAAA / multi-A load balancing / CNAME to node hostnames
* Managing MX/TXT/Page Rules and other non-module A records
* Non-Cloudflare DNS providers
* Merging DNS record management into the Zone core model
## Relationship with Existing Capabilities
| Existing Capability | Relationship |
| --- | --- |
| `of_zones` / `of_zone_domains` | Provide the pointable FQDN list; this module only references `zone_domain_id` |
| `of_nodes.ip` | Source of A record `content`; recommended to restrict to edge nodes with valid IPv4 |
| `of_dns_accounts` + `sealSensitive` | ACME DNS-01 already supports Cloudflare Token; this module can **import** the same account or store a Token standalone |
| lego Cloudflare provider | **Only** TXT/DNS-01; this module builds its own CF HTTP client for Zone/DNS Record APIs |
## Core Model
```mermaid
erDiagram
CF_CONNECTIONS ||--o| DNS_ACCOUNTS : optional_import
CF_POINTING_GROUPS ||--o{ CF_POINTING_MEMBERS : contains
ZONE_DOMAINS ||--o| CF_POINTING_MEMBERS : pointed_as
NODES ||--o{ CF_POINTING_GROUPS : primary
NODES ||--o{ CF_POINTING_GROUPS : backup
NODES ||--o{ CF_POINTING_GROUPS : active
CF_CONNECTIONS {
uint id PK
string source
uint dns_account_id
string authorization
string status
time verified_at
}
CF_POINTING_GROUPS {
uint id PK
string name
uint primary_node_id
uint backup_node_id
uint active_node_id
bool default_proxied
bool enabled
}
CF_POINTING_MEMBERS {
uint id PK
uint group_id
uint zone_domain_id UK
bool proxied
string cf_zone_id
string cf_record_id
string desired_ip
string sync_status
string last_error
time synced_at
}
```
### `of_cf_connections` (one valid connection globally)
| Field | Description |
| --- | --- |
| `source` | `dns_account` \| `standalone` |
| `dns_account_id` | references `of_dns_accounts` (type=cloudflare) when `source=dns_account` |
| `authorization` | encrypted storage when `source=standalone`, payload shape `{"api_token":"..."}`, consistent with DNS accounts; the API **never returns it** |
| `status` / `verified_at` | connectivity check result and time |
**Token resolution:** `dns_account` → decrypt the associated account; `standalone` → decrypt this row. Associated account deleted or validation failed → module not ready, sync forbidden.
**Recommended permissions:** Cloudflare API Token with `Zone:Read`, `DNS:Edit`.
### `of_cf_pointing_groups`
| Field | Description |
| --- | --- |
| `name` | display name |
| `primary_node_id` | primary node |
| `backup_node_id` | backup (nullable; phase 1 stores only) |
| `active_node_id` | currently effective node; equals primary in phase 1; rewritten by phase 2 failover |
| `default_proxied` | group default orange-cloud; **only affects newly added members** |
| `enabled` | whether to participate in sync |
Constraints: primary and backup must not be the same node; the node chosen as the active target must have a valid IPv4.
### `of_cf_pointing_members`
| Field | Description |
| --- | --- |
| `group_id` | owning group |
| `zone_domain_id` | globally unique: one domain belongs to at most one group |
| `proxied` | member orange-cloud (the only runtime basis) |
| `cf_zone_id` / `cf_record_id` | Cloudflare cache for idempotent updates |
| `desired_ip` / `sync_status` / `last_error` / `synced_at` | desired and sync state |
`sync_status`: `pending` \| `syncing` \| `ok` \| `error`.
No physical foreign keys; `zone_domain_id` unique index; query indexes on `group_id` etc.
## Orange-Cloud Priority
1. **Member `proxied`**: the only basis written to CF during sync.
2. **Group `default_proxied`**: copied to `proxied` when a member is **added**.
3. Later changes to the group default **do not rewrite** existing members.
## Sync Semantics
### Desired State
The OpenFlare DB tables are the Source of Truth. Each member expects:
| Item | Value |
| --- | --- |
| type | `A` |
| name | the ZoneDomain's FQDN |
| content | the group `active_node`'s IPv4 |
| proxied | member `proxied` |
| ttl | forced Auto by CF when orange-cloud is on; unified default (e.g. 300) when off |
Phase 1 does not write AAAA. Node IP not a valid IPv4 → that member is `error`.
### Triggers
| Trigger | Behavior |
| --- | --- |
| Manual sync (all / group / member) | reconcile |
| Member added | initialize `proxied`, then enqueue sync |
| Member removed / group deleted | delete the remote A managed by this module by default (configurable keep) |
| Primary node / active / member `proxied` changed | re-sync the corresponding scope |
| Node IP changed (heartbeat or manual) | enqueue members whose `active_node_id` points to that node |
| Token not ready | refuse sync |
Phase 1 does not do scheduled full reconciliation.
### Reconcile (single member, idempotent)
1. Resolve the CF Zone by the FQDN's registrable root domain, cache `cf_zone_id`.
2. With a `cf_record_id`, prefer Update; if stale, list by `name+type=A`.
3. **0 records** → Create; **exactly 1** → take over and Update; **multiple** → fail and tell the user to clean up in CF.
4. Write back `cf_record_id`, `desired_ip`, `sync_status`, `synced_at` / `last_error`.
5. On rate limiting, retry with bounded backoff.
**Ownership:** only manage records cached by this module or taken over as "the only same-name A"; do not clear the Zone or touch other record types. After a user edits in the CF console, the next sync overwrites with the OpenFlare desired state.
### Execution Carrier
* Single record: can sync on the request path.
* Whole group / per-node batch: Asynq tasks (`cloudflare:sync_member` / `sync_group` / `sync_by_node`), registered in `bootstrap`.
* Per-member mutex to prevent concurrent double-writes.
* Node IP change path delivers tasks **best-effort**, not blocking the heartbeat.
## API (Admin Panel)
Prefix: `/api/v1/d/cloudflare`, Session admin auth. Package: `internal/apps/openflare/cloudflare/`; routes: `internal/router/v1/openflare/register_cloudflare.go`.
| Resource | Method & Path |
| --- | --- |
| Connection | `GET/PUT /connection`, `POST /connection/verify`, `POST /connection/clear` |
| Overview | `GET /overview` |
| Groups | `GET/POST /groups`, `GET /groups/:id`, `POST /groups/:id/update|delete|sync` |
| Members | `GET/POST /groups/:id/members`, `POST .../members/:memberId/update|remove|sync` |
| Available domains | `GET /domains/available` |
* Success `response.OK`; failure `response.Abort*`; the Token is **never** returned in JSON.
* Handlers separated from `logics.go`; the CF client is abstracted behind an interface for replaceability.
## Frontend
* Navigation: `frontend/lib/navigation/openflare-nav.ts` adds **Cloudflare** → `/cloudflare` (near Website Management / DNS Accounts).
* Routes:
* `/cloudflare`: overview; guide to configure when not ready
* `/cloudflare/settings`: mixed Token config and test connection
* `/cloudflare/groups`, `/cloudflare/groups/[id]`: list and detail (members, orange-cloud, sync)
* Services: independent service under `frontend/lib/services/openflare/`, extending `BaseService`.
* Pages follow the existing title-bar and component-split conventions; destructive actions need double confirmation.
* Copy that must be visible: sync overwrites module-managed A records; multiple same-name A records need manual cleanup; removal deletes remote records by default; phase 1 has no automatic failover.
## Errors and Security
* User-visible copy is module-internal constants; internal errors log via `pkg/logger`.
* Typical: token not configured, invalid token, node without IP, no CF Zone, multiple same-name A records, rate limiting.
* Token is only decrypted server-side for use; responses and logs must never contain plaintext tokens.
## Data Migration
* goose both dialects (PG/SQLite) create the three tables; defaults match Go zero values.
## Key Decision Summary
| Decision | Conclusion |
| --- | --- |
| Module shape | standalone Cloudflare pointing module, not embedded Zone fields |
| Token | mixed: imported from DNS account or encrypted standalone |
| Domain granularity | ZoneDomain (FQDN) |
| Record shape | single A → active node IPv4 |
| Failover | phase 2; heartbeat offline; phase 1 only stores backup/active |
| Orange-cloud | member-level effective; group default only initializes |
| SoT | DB tables as desired state drive CF |