Skip to content

Latest commit

 

History

History
238 lines (188 loc) · 8 KB

File metadata and controls

238 lines (188 loc) · 8 KB

FlareSync Workflow

This document describes FlareSync’s runtime behavior (control flow, external calls, retries, and side effects) as implemented in the current codebase.

High-Level Flow

flowchart TD
  A["Process start"] --> B["Init logging (LOG_CONFIG_PATH or log4rs.yaml)"]
  B --> C["Load config from env/.env"]
  C --> D["Build reqwest Client (30s timeout)"]
  D --> S0["Write initial runtime status"]
  S0 --> E["Loop forever"]

  E --> F["Get current public IPv4"]
  F -->|quorum ok| S1["Update runtime status with public IP"]
  S1 --> G["For each DOMAIN_NAME"]
  F -->|error| F1["Log error; update runtime status; sleep 60s; continue loop"]
  F1 --> E

  G --> H["Fetch Cloudflare A record for domain"]
  H -->|record found| I["Compare record.content vs current IPv4"]
  H -->|no record| H1["Warn; write missing status; continue next domain"]
  H1 --> G

  I -->|same| I1["Log no update needed"]
  I -->|changed| J["Backup existing DNS record to ./backups"]
  J --> K["Update Cloudflare A record to current IPv4"]
  K --> K1["Log update success"]

  I1 --> S2["Write per-domain runtime status"]
  K1 --> S2
  S2 --> G

  G --> L["Sleep UPDATE_INTERVAL"]
  L -->|interval elapsed| E
  L -->|SIGINT/SIGTERM| X["Write shutdown status and exit"]
Loading

Inputs and Configuration

flowchart LR
  ENV["Environment + .env"] --> CFG["Config::from_env()"]
  CFG --> TOK["CLOUDFLARE_API_TOKEN (required)"]
  CFG --> ZID["CLOUDFLARE_ZONE_ID (required)"]
  CFG --> DOM["DOMAIN_NAME (required; comma/semicolon-separated; empty entries ignored)"]
  CFG --> INT["UPDATE_INTERVAL minutes (optional; defaults to 5; must be >= 1)"]
  CFG --> STS["STATUS_FILE_PATH (optional)"]
  ENV --> LOG["LOG_CONFIG_PATH (optional)"]
Loading
  • LOG_CONFIG_PATH defaults to log4rs.yaml if unset.
  • STATUS_FILE_PATH defaults to status/flaresync-status.json if unset.
  • DOMAIN_NAME may contain multiple entries separated by , or ;. Empty entries are dropped; if all entries are empty, startup fails.
  • UPDATE_INTERVAL is interpreted as minutes, defaults to 5 when unset, and must be >= 1.

Public IP Discovery (Multi-Source + Quorum)

FlareSync queries multiple public-IP endpoints concurrently and requires agreement by quorum to accept a result.

Sources (hardcoded)

  • https://api.ipify.org
  • https://checkip.amazonaws.com
  • https://ipv4.icanhazip.com

Policy

  • Fetch all three in parallel.
  • Accept the IPv4 address only if at least 2 out of 3 sources return the same value.
  • If quorum fails, treat as an error and retry later.
sequenceDiagram
  autonumber
  participant App as FlareSync
  participant S1 as ipify.org
  participant S2 as checkip.amazonaws.com
  participant S3 as icanhazip.com

  App->>S1: GET / (10s timeout, retries with backoff)
  App->>S2: GET / (10s timeout, retries with backoff)
  App->>S3: GET / (10s timeout, retries with backoff)

  alt 2-of-3 agree on IPv4
    S1-->>App: "203.0.113.10"
    S2-->>App: "203.0.113.10"
    S3-->>App: "203.0.113.11"
    App-->>App: quorum satisfied (203.0.113.10)
  else quorum not satisfied (or too many failures)
    S1-->>App: timeout or error
    S2-->>App: "203.0.113.10"
    S3-->>App: "203.0.113.11"
    App-->>App: error (no quorum)
    App-->>App: sleep 60s and retry loop
  end
Loading

Retry behavior per source

  • Per-attempt timeout: 10 seconds (request and response body).
  • Retry up to 3 times with exponential backoff (starting at 1s, doubling, capped).
  • Retries trigger on network failures and explicit timeout errors.

Cloudflare DNS Check/Update

For each configured domain, FlareSync:

  1. Fetches the existing A record matching that exact name in the given Zone.
  2. If a record exists, compares current record IP with current public IPv4.
  3. If different, backs up the record JSON to ./backups/ and updates the record via Cloudflare API.
  4. If the record is missing, it logs a warning and does not create records.
sequenceDiagram
  autonumber
  participant App as FlareSync
  participant CF as Cloudflare API
  participant FS as Local filesystem

  App->>CF: GET /zones/{zone}/dns_records?type=A&name={domain}
  alt record exists
    CF-->>App: success=true, result=[DnsRecord]
    App-->>App: compare record.content vs current IPv4
    alt IP changed
      App->>FS: write ./backups/{timestamp}_{sanitized-name}_backup.json
      App->>CF: PUT /zones/{zone}/dns_records/{id} (content=current IPv4)
      CF-->>App: success=true
    else IP unchanged
      App-->>App: no update
    end
  else record missing
    CF-->>App: success=true, result=[]
    App-->>App: warn "No matching DNS record found"
  end
Loading

URL Encoding

The Cloudflare DNS-record lookup uses a structured query builder (not string concatenation), so the name= parameter is URL-encoded correctly for edge cases (e.g., wildcard names like *.example.com).

Backups (Side Effects)

When an update occurs:

  • ./backups/ is created if missing.
  • The existing DNS record is saved as pretty-printed JSON before the update.
  • The filename uses a sanitized version of the record name to avoid unsafe filesystem characters:
    • Allowed: ASCII letters/digits plus ., _, -
    • All other characters become _
    • Component is length-capped

Retry & Error Handling

stateDiagram-v2
  [*] --> Startup
  Startup --> Running: config ok
  Startup --> [*]: config error

  Running --> ResolveIP
  ResolveIP --> ResolveIP_Wait: error (no quorum or failures)
  ResolveIP_Wait --> Running: after 60s
  ResolveIP --> UpdateDomains: success (IPv4)

  UpdateDomains --> PerDomain
  state PerDomain {
    [*] --> FetchRecord
    FetchRecord --> NoRecord: empty result
    FetchRecord --> Compare: record found
    Compare --> NoChange: IP same
    Compare --> Backup: IP changed
    Backup --> UpdateRecord
    UpdateRecord --> Done
    NoRecord --> Done
    NoChange --> Done
  }

  UpdateDomains --> SleepInterval
  SleepInterval --> Running: after UPDATE_INTERVAL
Loading

Cloudflare retries

Cloudflare requests use bounded exponential backoff retries for transient failures:

  • Network/HTTP transient: request-level failures, HTTP 429, HTTP 5xx.
  • API-level transient: HTTP 200 with success=false where errors look transient (e.g., Cloudflare code 1015 or messages suggesting rate limiting / temporary issues).

Non-transient Cloudflare API errors fail fast for that domain and FlareSync continues with the next domain.

Logging

Logging is initialized from:

  • LOG_CONFIG_PATH if set (Docker image sets this to log4rs.docker.yaml)
  • Otherwise log4rs.yaml

The app logs:

  • Startup
  • Current public IP
  • Per-domain decisions (no record / no change / updated)
  • Retry warnings and errors

Runtime Status

FlareSync writes a JSON runtime status file after startup, IP-check results, per-domain results, errors, and shutdown.

Default path:

  • status/flaresync-status.json

Config override:

  • STATUS_FILE_PATH

The status file includes:

  • started_at
  • updated_at
  • last_public_ip
  • last_ip_check_at
  • domains
  • last_error
  • shutting_down

Status write failures are logged as warnings and do not stop DNS updates.

Status writes use a same-directory temporary file followed by a rename so readers do not observe partially written JSON.

Shutdown

FlareSync listens for SIGINT and SIGTERM. During IP discovery, per-domain Cloudflare work, and interval waits, a shutdown signal interrupts waiting, writes a final status file with shutting_down: true, and exits cleanly.

Deployment Notes (Docker)

flowchart LR
  IMG["Container image"] --> BIN["flaresync binary"]
  IMG --> LC["LOG_CONFIG_PATH=log4rs.docker.yaml"]
  VOL1["./backups"] -->|mounted to| APPBK["/app/backups"]
Loading
  • The container image sets LOG_CONFIG_PATH=log4rs.docker.yaml to log to stdout (useful for docker logs).
  • Backups are typically volume-mounted so they persist across container restarts.
  • Runtime status is typically volume-mounted from ./status to /app/status.
  • Host-mounted backups and status directories must be writable by the configured container user. The default Compose file uses UID/GID 1000:1000.