Apply Flow
This document describes how xp2p applies configuration changes via a strict service-owned apply mechanism. The goal is to make config updates atomic, audited, and safe to apply from services without relying on CLI flags.
Key Files and Directories
CONFIG_ROOT/.state/CONFIG_ROOT/.state/live/CONFIG_ROOT/.state/lkg/CONFIG_ROOT/.state/apply.requestCONFIG_ROOT/.state/apply.errorCONFIG_ROOT/xp2p-client.tomlCONFIG_ROOT/xp2p-server.tomlCONFIG_ROOT/config-client/CONFIG_ROOT/config-server/
The apply.request file is the trigger that asks the service layer to compile
and apply Desired inputs.
Actors
- CLI, UI, and manual edits: update Desired inputs and create
apply.request. - Service layer (
xp2p runor system service): readsapply.request, compiles Desired inputs into runtime artifacts, and cleans up.
High-Level Flow
- Update Desired inputs (
xp2p-*.tomland optional JSON snippets). - Write
apply.request. - Service detects request and compiles runtime configuration.
- Service clears
apply.requeston success, or writesapply.erroron failure. - Runtime behavior updates OS routes and TUN state (service layer only).
Desired Inputs
Desired inputs are always user-editable and live at stable paths:
CONFIG_ROOT/xp2p-client.tomlCONFIG_ROOT/xp2p-server.tomlCONFIG_ROOT/config-client/*.json(optional snippets)CONFIG_ROOT/config-server/*.json(optional snippets)
xp2p reads these inputs and compiles them into a final Xray configuration used by the runtime.
For recommended snippet filenames and routing rule insertion points, see Config compilation.
Read Rules and Exceptions
- Runtime behavior (service run, diagnostics, ping, OS routing) reads live runtime artifacts only and never reads Desired inputs directly.
- If Desired inputs change, an apply must be requested via
apply.request. - Deploy validation may start a temporary xray-core using a compiled config derived from Desired inputs, but it must not write to live or bypass apply.
Edit + Rollback Flow
This section describes the flow for manual edits, CLI edits, and rollback using a clear split between Desired, Live, and LKG. Apply requests are tracked via a marker file.
Directory Roles
- Desired: user-editable config inputs
CONFIG_ROOT/xp2p-*.tomlconfig-client/*.json,config-server/*.json- Live: active runtime config
CONFIG_ROOT/.state/live/- LKG: last known good snapshot (hidden)
CONFIG_ROOT/.state/lkg/
Live and LKG store compiled runtime artifacts (for example xray.json) together with apply metadata.
Manual Edit Flow
- User edits Desired files under
CONFIG_ROOT/orconfig-*/. - Watchers debounce bursts of writes.
apply.requestis created after edits settle to trigger service apply.- Service compiles Desired inputs and writes live runtime artifacts atomically.
- On success, the previous live artifact set is stored as LKG (optional).
CLI Edit Flow
- CLI writes updates into Desired.
apply.requestis created to trigger service apply.- Service compiles Desired inputs into live runtime artifacts.
- On success, the previous live artifact set is stored as LKG (optional).
Rollback Flow
- Apply fails (service/xray/health checks).
- Service restores live runtime artifacts from LKG (when available).
apply.erroris written with the request ID and failure reason.apply.requestremains so operators can see the requested change, but the service skips repeated apply attempts for the same request ID.- Service restarts using restored live artifacts and logs the failure.
Deploy Flow
Deploy flow details (including apply requests, temporary tunnel validation, and service start requirements) live in Deploy flow to avoid duplication.
Apply Request
The apply trigger file is created at:
CONFIG_ROOT/.state/apply.requestCONFIG_ROOT/.state/apply.error
It includes a role (client or server) and a request ID. The service
process watches for this file and treats it as the single source of truth
for apply work. When apply fails, the service writes apply.error with
the same request ID and failure reason.
Service Apply
On service start (or restart), the service:
- Reads
apply.request. - Compiles Desired inputs into live runtime artifacts.
- Removes
apply.request. - Writes LKG metadata on success (optional).
If apply fails, the service logs the error and keeps apply.request so the
operator can investigate or retry. The service will not retry the same
request ID once apply.error is recorded; a new apply request must be
created after fixing Desired inputs.
Routes and OS Changes
OS changes are applied only by the service layer:
- TUN creation and IP assignment.
- Routes and full-tunnel changes.
- DNS overrides (when enabled).
CLI commands and UI flows update Desired inputs and request apply. They do not touch OS-level state directly.
Runtime OS State Contract (TUN / routes / DNS)
This section defines the runtime contract for service-owned OS state to avoid visible flapping during restarts.
Ownership and Scope
- The service layer owns OS state (TUN, routes, DNS).
- The service layer must keep OS state consistent with the current Desired runtime mode.
- CLI/UI/manual edits must not directly modify OS state.
Mode-Driven Transitions
OS state transitions are driven by mode transitions, not by internal restarts:
- Enter full-tunnel (
client.tun_enabled=trueandclient.tun_mode=full): - Replace default routes with the TUN interface.
- Add bypass routes to all configured endpoints.
- Apply DNS override to
client.dns_servers(when configured). - Keep full-tunnel active while Desired remains in full-tunnel mode.
- Exit full-tunnel (Desired changes away from full-tunnel):
- Restore baseline default routes and remove bypass routes.
- Restore baseline DNS.
- Service stop/uninstall:
- Restore baseline routes/DNS (best effort) before exiting.
Restart and Cancellation Semantics
Service restarts caused by apply.request, file watchers, health checks, or crash recovery must not cause
route/DNS rollback if Desired remains in full-tunnel mode.
- A child-run cancellation (graceful restart) is not a mode change.
- Rollback/restore is allowed only on explicit stop, explicit mode switch, or hard failures that require leaving the mode.
Pending State and Retry (Windows)
On Windows, TUN readiness can be delayed or unstable across restarts (adapter disconnected, IPv4 missing, DAD not preferred). When Desired is full-tunnel but the adapter is not ready, runtime enters a pending state instead of rolling back OS state.
- Pending state is recorded in
CONFIG_ROOT/xp2p-client.tun-full.jsonasphase = "full_pending"with a stablepending_reason. - While pending, routes and DNS override are not applied.
- The service retries through restarts using exponential backoff (2s, 4s, 8s, ... capped at 30s) until the adapter is
up/preferred.
Mode Switching
Mode changes (split/full):
- Update
tun_enabled,tun_mode, andfull_tunnel_tagin Desired TOML. - Write
apply.request. - Service compiles config and restarts runtime as needed.
- In full mode, repeated route re-apply does not rewrite config; it only updates OS routes.
Common Failure Modes
- Invalid Desired TOML / invalid JSON snippets.
- Merge collisions (reserved tags, invalid rule order, conflicts).
- Service not running or apply request not detected.