This document describes FlareSync’s runtime behavior (control flow, external calls, retries, and side effects) as implemented in the current codebase.
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"]
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)"]
LOG_CONFIG_PATHdefaults tolog4rs.yamlif unset.STATUS_FILE_PATHdefaults tostatus/flaresync-status.jsonif unset.DOMAIN_NAMEmay contain multiple entries separated by,or;. Empty entries are dropped; if all entries are empty, startup fails.UPDATE_INTERVALis interpreted as minutes, defaults to5when unset, and must be>= 1.
FlareSync queries multiple public-IP endpoints concurrently and requires agreement by quorum to accept a result.
Sources (hardcoded)
https://api.ipify.orghttps://checkip.amazonaws.comhttps://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
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.
For each configured domain, FlareSync:
- Fetches the existing A record matching that exact name in the given Zone.
- If a record exists, compares current record IP with current public IPv4.
- If different, backs up the record JSON to
./backups/and updates the record via Cloudflare API. - 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
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).
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
- Allowed: ASCII letters/digits plus
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
Cloudflare requests use bounded exponential backoff retries for transient failures:
- Network/HTTP transient: request-level failures, HTTP
429, HTTP5xx. - API-level transient: HTTP
200withsuccess=falsewhereerrorslook transient (e.g., Cloudflare code1015or messages suggesting rate limiting / temporary issues).
Non-transient Cloudflare API errors fail fast for that domain and FlareSync continues with the next domain.
Logging is initialized from:
LOG_CONFIG_PATHif set (Docker image sets this tolog4rs.docker.yaml)- Otherwise
log4rs.yaml
The app logs:
- Startup
- Current public IP
- Per-domain decisions (no record / no change / updated)
- Retry warnings and errors
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_atupdated_atlast_public_iplast_ip_check_atdomainslast_errorshutting_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.
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.
flowchart LR
IMG["Container image"] --> BIN["flaresync binary"]
IMG --> LC["LOG_CONFIG_PATH=log4rs.docker.yaml"]
VOL1["./backups"] -->|mounted to| APPBK["/app/backups"]
- The container image sets
LOG_CONFIG_PATH=log4rs.docker.yamlto log to stdout (useful fordocker logs). - Backups are typically volume-mounted so they persist across container restarts.
- Runtime status is typically volume-mounted from
./statusto/app/status. - Host-mounted
backupsandstatusdirectories must be writable by the configured container user. The default Compose file uses UID/GID1000:1000.