1. Overview
bastionguard-pacd is the PAC (Proxy Auto-Config) daemon used by BastionGuard to implement Secure Payments routing. It dynamically generates and serves a proxy.pac file based on a user-managed allowlist (payments.json), and exposes a local stub proxy that triggers and forwards traffic to the sandboxed CEF proxy backend.
The daemon provides two cooperative services:
- PAC HTTP Server – serves
/proxy.pacto the browser (e.g., Firefox) - Stub Proxy Server – listens on a local port referenced by the PAC file and forwards connections to the CEF backend proxy (port separation enforced)
This design ensures that only traffic targeting payment domains is routed through the Secure Payments sandbox path, while all other traffic remains DIRECT.
2. Key Components and Data Sources
- payments.json (user):
~/.local/share/BastionGuard/payments/payments.json - payments.json (system seed):
/usr/share/BastionGuard/data/payments/payments.json - CEF proxy backend: expected to listen on
127.0.0.1:3130(default) - Stub proxy: exposed on
127.0.0.1:3129(default) - PAC server: exposed on
127.0.0.1:8765(default) - systemd user unit trigger:
systemctl --user start BastionGuard-cef.service
3. Architecture
3.1 Two-Port Proxy Design
The daemon enforces a split between:
- stub_port – where browsers connect (PAC points here)
- backend_port – where the sandboxed proxy actually runs
This separation is intentional:
- Stub proxy acts as a deterministic “trigger gate”
- Backend can be started on-demand
- Forwarding is transparent once backend is available
3.2 On-Demand Backend Activation
When a browser attempts to use the proxy (because the PAC matched a payment domain), the stub proxy:
- Executes a configured trigger command (default: start systemd user unit)
- Waits for the backend to become reachable
- Forwards the raw TCP stream between client and backend
If the backend does not become ready within the configured timeout, the stub proxy closes the client connection, causing the browser to raise a proxy error. This is considered correct behavior because Secure Payments must remain deterministic and must not silently fall back.
4. XDG Paths and payments.json Seeding
4.1 XDG Data Home Resolution
User storage follows XDG conventions:
- If
XDG_DATA_HOMEis set, it is used - Otherwise defaults to:
~/.local/share
4.2 payments.json Paths
- User path:
~/.local/share/BastionGuard/payments/payments.json - System seed path:
/usr/share/BastionGuard/data/payments/payments.json
4.3 Seeding Policy
The daemon implements a “seed-if-missing” rule:
- If the user file exists: never overwrite
- If missing and system file exists: copy system → user
- If both missing: create a minimal default JSON in the user location
The default list contains common payment gateways (PayPal, Stripe, Adyen, Klarna, etc.) and sets:
enabled: falseupdated_at: 1970-01-01(placeholder)
5. Domain Normalization and Validation
5.1 normalize_domain_or_url()
All entries in the allowlist are normalized before PAC generation. Normalization performs:
- Whitespace trimming
- Scheme removal (
https://,http://) - Path stripping (
/...) - Credential stripping (
user@host) - Port stripping (
:443) - Removal of leading
www. - Lowercasing
- Character allowlist enforcement:
a-z,0-9, dot and hyphen - Rejects invalid domains (too short, missing dot, dot at ends)
This prevents malformed or dangerous entries from being embedded into the PAC script.
6. Allowlist Loading
6.1 load_payments_domains_from_user_json()
This routine loads the allowlist from the user payments JSON:
- Ensures seeding has occurred
- Parses JSON and reads
listarray - Normalizes each entry via
normalize_domain_or_url() - Sorts and deduplicates the final domain vector
If JSON is corrupted or parsing fails, it returns an empty list (fail-safe: no payment domains, so PAC routes everything DIRECT).
7. PAC Generation
7.1 generate_pac(pay_domains, stub_host, stub_port)
Generates a valid PAC JavaScript file with the following behavior:
- Defines
dnsDomainIs(host, domain)supporting exact and subdomain matches - Implements
FindProxyForURL(url, host)with local bypass rules - If host matches a payment domain: returns
PROXY stub_host:stub_port - Otherwise: returns
DIRECT
7.2 Local/Private Network Bypass
The PAC file returns DIRECT for:
- Plain hostnames
- localhost
- 127.*
- 10.*
- 192.168.*
- 172.16.* through 172.31.* (implemented via pattern range rules)
This prevents routing local traffic into the secure-payments proxy path.
7.3 Deterministic Trigger Behavior
The generated PAC intentionally returns only:
PROXY 127.0.0.1:3129
and not:
PROXY ...; DIRECT
This ensures the Secure Payments path is deterministic. If the proxy fails, the browser fails the request, rather than silently leaking payment traffic outside the sandboxed environment.
8. PAC HTTP Server
8.1 Endpoint
The server listens on the configured PAC host/port and serves:
GET /proxy.pac
Other paths return 404. Non-GET methods return 405.
8.2 HTTP Headers
PAC responses include explicit no-cache directives:
Cache-Control: no-cache, no-store, must-revalidatePragma: no-cacheExpires: 0
This ensures domain changes in payments.json are reflected quickly without stale caching.
8.3 Cache Coherence
The PAC server maintains an in-memory cache:
PaymentsCache.domainsPaymentsCache.last_mtimePaymentsCache.loaded
On each request, the server compares the file mtime of the user payments JSON to detect updates and reloads only when needed.
9. Stub Proxy Server
9.1 Trigger Flow
On each incoming stub proxy connection:
- Evaluate whether to run the trigger (on-demand)
- Wait for backend availability (
wait_backend_up) - Connect to backend
- Forward raw bidirectional TCP stream
9.2 TriggerState (Rate Limit and Concurrency)
The daemon prevents trigger storms using:
std::atomic<bool> started– indicates that trigger was executed successfully at least oncestd::chrono::steady_clock::time_point last_start– last trigger attempt timestd::mutex mtx– protects the trigger state
A simple rate limit prevents retries more frequently than once per second.
9.3 Backend Readiness Probe
wait_backend_up(host, port, wait_ms) repeatedly attempts TCP connects until:
- connection succeeds, or
- the timeout expires
Attempts are performed every 50ms for responsiveness.
9.4 Bidirectional Forwarding
forward_bidirectional() spawns two pump threads:
- client → backend
- backend → client
Each pump:
- Reads chunks from one socket
- Writes them to the opposite socket
- On termination, shuts down the destination send-side (best effort)
At the end, both sockets are closed.
10. Runtime Model and Threading
The daemon runs multiple threads:
- Signal thread – handles SIGINT/SIGTERM/SIGHUP via Boost.Asio signal_set
- PAC server thread – accepts HTTP connections and serves PAC
- Stub proxy acceptor thread – accepts stub proxy connections
- Per-connection worker threads – each stub proxy client connection is handled in a detached thread
This design provides concurrency for multiple browser connections while keeping the PAC serving path fast and lightweight.
11. Signal Handling and Shutdown
The daemon listens for:
- SIGINT, SIGTERM – clean shutdown
- SIGHUP – treated as restart request (the daemon exits, allowing systemd restart policies)
Signal handling sets running=false, stops the signal io_context, and allows accept loops to terminate naturally.
12. Error Handling and Fail-Safe Behavior
- Corrupted JSON: returns empty allowlist → PAC routes all traffic DIRECT
- Backend not ready: stub proxy closes connection → browser proxy error (no silent fallback)
- Filesystem errors: seeding and atomic write are best-effort; failures do not crash the daemon
- Networking errors: logged to stderr; connections are closed cleanly
13. Security Considerations
- Allowlist-based routing: only domains in
payments.jsonare routed through the secure proxy path - Input validation: domain normalization rejects unsafe characters and malformed entries
- Deterministic policy: no PAC fallback to DIRECT for payment domains, preventing accidental policy bypass
- Local-only bind: default binding to
127.0.0.1limits exposure to the local machine - Trigger command: executed via shell (
system()); must be controlled and not user-injected
For hardening, consider:
- Replacing
system()trigger execution withexecve()style calls or systemd D-Bus APIs - Enforcing strict ownership/permissions on
payments.json - Adding structured logging with redaction if required
14. CLI Parameters
The daemon supports runtime configuration via command-line arguments:
--listen <host>– PAC server listen address--port <port>– PAC server port--stub-host <host>– stub proxy listen address--stub-port <port>– stub proxy port referenced by PAC--backend-host <host>– backend proxy target address--backend-port <port>– backend proxy target port--trigger-cmd <cmd>– command executed to start backend--backend-wait-ms <ms>– maximum wait time for backend readiness--help– prints usage
15. Operational Notes
- The PAC URL to configure in the browser is:
http://127.0.0.1:8765/proxy.pac - The stub proxy must remain reachable on the port referenced by the PAC file
- The backend proxy must listen on a different port and be started by the configured trigger
- Domain updates in
payments.jsonare picked up automatically based on file mtime changes