checkpoint: implement wgcloak v1 foundations

This commit is contained in:
h3cx committed 2026-09-14 22:55:07 +02:00
commit 313ab00765
45 files changed
+12058

No files matched your search

+73
View File
@@ -0,0 +1,73 @@
# Architecture
wgcloak is a single Linux userspace process with a `client` or `server` role.
The core moves opaque datagrams between a local UDP socket and a transport
session. It does not know whether the payload is WireGuard, DNS, or another UDP
protocol.
```text
CLIENT SERVER
local UDP peer transport listener
│ │
▼ ▼
local listener ── tunnel core ── transport ── authenticated session
│
▼
dedicated connected UDP backend socket
│
▼
configured UDP backend
```
## Ownership boundaries
- The local listener receives datagrams and remembers the most recently active
local peer for return traffic. One active peer per v1 tunnel is sufficient.
- The core owns session lifecycle, limits, datagram-size checks, reconnect
policy, and statistics. Payload bytes remain opaque.
- A transport owns connection establishment and wire representation. Every
transport exposes the same logical send/receive/close behavior and a maximum
datagram size.
- Authentication is a separate concern. Direct UDP uses the documented
challenge/HMAC packet protocol; TLS-capable transports authenticate the
client after transport establishment.
- The server creates a backend UDP socket only after client authentication and
maps that socket to one session. A backend response therefore has natural
session ownership without inspecting the backend protocol.
## Transport lifecycle
The client state machine is:
```text
STARTING → RESOLVING → PROBING → AUTHENTICATING → CONNECTED
│ │
└───────────┘ failure
▼
RECONNECTING
│
└→ PROBING
```
In `auto` mode the target order is direct UDP, QUIC DATAGRAM, HTTP/3
CONNECT-UDP, then TLS/TCP. Authentication rejection is a configuration error,
not permission to probe indefinitely. Network failures use bounded exponential
backoff with jitter.
## Server isolation
One malformed or unauthenticated packet must be rejected without terminating
the listener or affecting another client. Server limits cover active sessions,
per-source sessions, pending handshakes, handshake duration, authentication
attempts, and maximum datagram size. Expiry closes the backend socket and
releases replay/session state.
## Privileges and filesystem
Normal operation uses ordinary UDP/TCP/Unix sockets and needs no root,
`CAP_NET_ADMIN`, or `CAP_SYS_ADMIN`. `CAP_NET_BIND_SERVICE` is optional when a
reviewed deployment must bind a port below 1024. The control socket is local
and protected by filesystem ownership/mode; it is not a network listener.
This document describes the intended v1 architecture. Check the milestone
matrix before relying on a transport or control command in a particular build.
+109
View File
@@ -0,0 +1,109 @@
# Development state
Checkpoint: 2026-09-14, end of day. Development is paused at a compilable
checkpoint; no commit was made.
## Branch and worktree
- Branch: `main`.
- `git status --short` reports the project files as untracked, including
`Cargo.toml`, `Cargo.lock`, `src/`, `tests/`, documentation, examples, and
packaging files. There is no useful committed baseline to compare against in
this checkout.
- Multiple agents have been working in the shared directory. Review all diffs
and ownership boundaries before committing; in particular, do not assume an
untracked file was created by this task or that a concurrent edit is final.
## Implemented and wired behavior
The current tree contains the following work, subject to integration review:
- Operational assets: MIT `LICENSE`, Linux-focused `README.md`, `.gitignore`,
client/server TOML examples, architecture/protocol/security/operations/
troubleshooting documents, an implementation checklist, and systemd client
and server templates.
- TLS/TCP transport primitives in `src/transport/tls_tcp.rs`: bounded
incremental/coalesced u16-big-endian framing, truncated/oversized-frame
handling, PEM/DER certificate and private-key loading, challenge-response
authentication, authenticated data/heartbeat/close framing, idle timeout,
and bounded shutdown.
- HTTP/3 support primitives in `src/transport/http3.rs`: CONNECT-UDP request
validation, QUIC varints, RFC 9297 HTTP Datagram context encoding, capsule
encoding, and project authentication messages. The module does not fake
CONNECT-UDP with POST or ordinary streams.
- Runtime orchestration in `src/transport/mod.rs`, `src/client/mod.rs`, and
`src/server/mod.rs` instantiates direct UDP, QUIC, and TLS/TCP connectors or
listeners. TLS backend creation occurs after the authenticated session is
established. HTTP/3 currently returns an explicit `Unsupported` result from
runtime selection because the actual h3 datagram adapter is not complete.
- The client and server start the Unix control server. Client reconnect and
reload commands currently cause a reconnect attempt; server shutdown is
handled by the encrypted listener paths, while direct-UDP/control lifecycle
still needs review.
- `h3-quinn` was enabled with its `datagram` feature; `tokio-rustls` and
`rustls-native-certs` were added for TLS runtime integration.
## Files changed in this work
Primary files touched by the operations/integration work are:
- `Cargo.toml`, `Cargo.lock`
- `src/transport/mod.rs`
- `src/transport/tls_tcp.rs`
- `src/transport/http3.rs`
- `src/client/mod.rs`
- `src/server/mod.rs`
- `tests/tls_tcp.rs`, `tests/http3.rs`, `tests/integration_transports.rs`
- `.gitignore`, `LICENSE`, `README.md`
- `examples/*`, `packaging/systemd/*`, and the other files under `docs/`
`src/transport/quic.rs` was edited by another concurrent agent and is outside
the TLS/integration ownership listed above. Preserve that ownership boundary
when resolving conflicts.
## Verification and current failures
- TOML examples were parsed successfully with Python `tomllib`.
- TLS/TCP focused tests passed: `cargo test --test tls_tcp --quiet` (7 tests).
- HTTP/3 focused tests passed: `cargo test --test http3 --quiet` (4 tests).
- `cargo check` passed after the transport integration edits.
- The TLS loopback integration test compiles but currently panics while
constructing a rustls configuration because no process-level crypto provider
is installed:
`Could not automatically determine process-level CryptoProvider from Rustls crate features.`
The runtime/test setup must explicitly install the `ring` provider (or enable
exactly one provider feature) before building rustls configurations.
- The supervisor's latest failure reprobe reports that the QUIC focused tests
now pass. Run the complete suite again after fixing the rustls provider; the
earlier complete-suite run had QUIC handshake-timeout failures before the
concurrent QUIC-agent fixes landed.
- Clippy has not reached a clean result. Existing included test/source modules
produce many dead-code and lint diagnostics, and integration code also needs
a final lint pass after the provider fix.
## Remaining gaps, prioritized
1. Fix and verify explicit rustls crypto-provider initialization, then run the
TLS loopback test and the complete test suite.
2. Finish a real h3 0.0.8/h3-quinn 0.0.10 HTTP/3 CONNECT-UDP datagram adapter,
or retain the explicit `Unsupported` path with a precise documented
dependency/API blocker. Do not substitute POST, capsules-only streams, or
ordinary HTTP request bodies for HTTP Datagrams.
3. Review runtime lifecycle semantics: direct-UDP control shutdown wiring,
actual file-backed config reload, client TLS heartbeat scheduling, and
authentication-error mapping so auto fallback stops on rejected credentials.
4. Run repository-wide formatting, tests, and clippy; inspect the shared
untracked tree and reconcile concurrent `quic.rs` changes before any commit.
## First three tasks tomorrow
1. Install/select the rustls `ring` provider once at runtime and in the
integration test setup; rerun TLS loopback plus `cargo test`.
2. Inspect the enabled h3-quinn datagram API and implement/test the smallest
standards-correct CONNECT-UDP client/server adapter; otherwise record the
exact unsupported API/dependency condition in the public docs.
3. Perform an end-to-end lifecycle review and repository-wide `fmt`, check,
test, and clippy pass, resolving only changes within each agent's ownership.
+60
View File
@@ -0,0 +1,60 @@
# Implementation and acceptance checklist
This is a coordinator checklist, not a claim that the items are complete.
Mark an item only with code evidence, tests, and an integration result. Record
the commit/test command or issue beside each checked item. A feature marked
planned in the other documents must remain visibly planned here until proven.
## Milestones 1–6
- [ ] **1 — authenticated direct relay:** local UDP listener; direct UDP
handshake; HMAC-authenticated data; replay window; keepalive/expiry; one
backend UDP socket per authenticated client; multi-client isolation; TOML;
logs and basic stats.
- [ ] **2 — QUIC DATAGRAM:** certificate loading and hostname verification;
client challenge/response; datagrams; keepalive/MTU; reconnect; multi-client
handling without core API redesign.
- [ ] **3 — automatic selection:** `mode = "auto"`; direct UDP then QUIC;
bounded probes/retries; auth rejection stops probing; jittered reconnect;
status reports selected transport.
- [ ] **4 — HTTP/3 CONNECT-UDP:** standard CONNECT-UDP/HTTP Datagram semantics;
authentication; backend mapping; reuse QUIC infrastructure where practical.
- [ ] **5 — TLS/TCP fallback:** TLS listener; client authentication; bounded
big-endian length framing; bidirectional datagram reconstruction; fallback
integration; expected head-of-line behavior documented.
- [ ] **6 — operational polish:** Unix control socket; status/stats/reconnect
commands; config validation; systemd units; capability guidance; certificate
and configuration reload; benchmarks; fuzzing; installation docs.
## v1 completion criteria (spec §89)
- [ ] 1. Multiple clients concurrently reach one UDP backend.
- [ ] 2. No WireGuard-specific logic exists.
- [ ] 3. Direct UDP works reliably.
- [ ] 4. QUIC DATAGRAM works reliably.
- [ ] 5. HTTP/3 CONNECT-UDP works.
- [ ] 6. TLS/TCP fallback works.
- [ ] 7. Automatic transport selection works.
- [ ] 8. Authentication is enforced on every public transport.
- [ ] 9. Direct UDP has replay and integrity protection.
- [ ] 10. Direct UDP NAT rebinding works.
- [ ] 11. Sessions expire and clean up correctly.
- [ ] 12. The server survives malformed and unauthenticated traffic.
- [ ] 13. The daemon runs unprivileged except optional bind capability.
- [ ] 14. TOML configuration is clearly validated.
- [ ] 15. Meaningful status and statistics are available.
- [ ] 16. Reconnects occur automatically.
- [ ] 17. Automated integration tests cover multi-client operation.
## Required evidence before v1 sign-off
- [ ] Unit tests cover parsing, malformed input, auth, key derivation, replay,
config validation, and timeout logic.
- [ ] Integration tests cover direct client/server, concurrent clients,
source-port change, auth rejection, restart/reconnect, backend outage,
oversized datagrams, and expiry; later transports have equivalent coverage.
- [ ] Fuzz targets exercise direct packets, TCP frames, and auth messages.
- [ ] `cargo test`, formatting, linting, and any documented integration/bench
commands pass from a clean checkout.
- [ ] Review confirms no payload/secret logging, no unsafe privilege grant, and
no accidental WireGuard/routing/firewall coupling.
+93
View File
@@ -0,0 +1,93 @@
# Operations
This guide assumes a dedicated `wgcloak` system user and `/etc/wgcloak/` for
configuration. Adjust paths for a user service or a different packaging
layout.
## First installation
```bash
sudo install -Dm755 target/release/wgcloak /usr/bin/wgcloak
sudo install -d -o wgcloak -g wgcloak -m0750 /etc/wgcloak/clients
sudo install -o wgcloak -g wgcloak -m0600 client.secret /etc/wgcloak/client.secret
```
Create server client secrets separately and transfer them through a protected
channel. Do not put real secret contents in the repository. Validate a config
before starting a service when `check-config` is available in the current
build:
```bash
sudo -u wgcloak wgcloak check-config --config /etc/wgcloak/client.toml
```
The command is a target v1 CLI and is delivered in the operational-polish
milestone; confirm with `wgcloak --help`.
## Starting and stopping
Foreground operation is useful for first-run diagnostics:
```bash
wgcloak client --config /etc/wgcloak/client.toml
wgcloak server --config /etc/wgcloak/server.toml
```
SIGTERM and SIGINT should stop accepting new sessions, stop local ingestion,
close backend sockets, flush useful metrics, and exit within a bounded timeout.
A transport failure should reconnect with jitter; it should not terminate the
daemon. The systemd templates set restart-on-failure for this reason.
## Control socket (intended interface)
The daemon should expose a Unix-domain control socket at
`$XDG_RUNTIME_DIR/wgcloak.sock` (for a system service, the template maps this
to its private runtime directory). Filesystem permissions, not a TCP port,
authorize access. The initial control protocol may be newline-delimited JSON.
Target commands are:
```text
status current role/state, selected transport, endpoint, MTU and uptime
stats counters for traffic, drops, auth failures, reconnects and sessions
reconnect ask the client to leave the current transport and probe again
reload re-read supported configuration (not full hot reload in early builds)
shutdown request graceful process shutdown
```
The exact request/response field names are implementation-owned until the
control protocol lands. A client should never send credentials or payloads
through this socket. `status` and `stats` are read-only; `reconnect`, `reload`,
and `shutdown` are state-changing and must remain local-admin operations.
## systemd deployment
Review and copy the units under `packaging/systemd/`. They assume:
- binary `/usr/bin/wgcloak`;
- user/group `wgcloak`;
- configuration `/etc/wgcloak/client.toml` or `server.toml`;
- runtime socket under a private `/run` directory;
- secret and certificate paths readable by the service user.
After review:
```bash
sudo install -Dm0644 packaging/systemd/wgcloak-client.service /etc/systemd/system/wgcloak-client.service
sudo systemctl daemon-reload
sudo systemctl enable --now wgcloak-client.service
systemctl status wgcloak-client.service
journalctl -u wgcloak-client.service -f
```
For a low port such as 443, uncomment `AmbientCapabilities=CAP_NET_BIND_SERVICE`
and its bounding-set companion in the server unit only after confirming the
unit still starts. Direct UDP examples use an unprivileged port.
## Safe rollout
Start with direct UDP and one test client. Check local listener binding,
server authentication, backend reachability, return-peer behavior, counters,
and clean shutdown. Add one transport milestone at a time. Keep an independent
path to the host while changing a public listener and retain the previous
configuration until a restart test succeeds.
+72
View File
@@ -0,0 +1,72 @@
# Protocol overview
The application protocol, tunnel session, transport, and authentication remain
independent. The application payload is opaque and is never parsed as
WireGuard (or any other protocol).
## Direct UDP wire protocol (target v1)
Direct messages use protocol version `1`, network byte order, and a 12-byte
common header:
```text
+---------+---------+---------+---------+
| version | type | flags | hdr_len |
| u8 | u8 | u8 | u8 |
+---------------------------------------+
| session_id (u64) |
+---------------------------------------+
```
Packet types are `HELLO (0x01)`, `ACCEPT (0x02)`, `REJECT (0x03)`,
`DATA (0x10)`, `KEEPALIVE (0x11)`, and `CLOSE (0x12)`. Unknown or malformed
types are rejected or ignored according to context and never crash the daemon.
`HELLO` has session ID zero and carries a bounded client ID, timestamp,
16-byte client nonce, and a 32-byte HMAC-SHA256 authenticator over the
canonical version/client/timestamp/nonce encoding. The server checks a default
60-second clock window and remembers accepted nonces for replay protection.
`ACCEPT` assigns a random, non-zero 64-bit session ID and returns a server
nonce, idle timeout, maximum datagram size, and an authenticator. `REJECT`
returns only a small reason class; it must not disclose secret material.
Authenticated `DATA` and `KEEPALIVE` packets contain the common header, a
32-bit direction-specific sequence number, a 128-bit truncated HMAC tag, and
the optional payload. Session keys are derived with HKDF-SHA256 from the
configured secret, both nonces, and the session ID. Implementations must use
maintained cryptographic libraries and constant-time tag comparison.
Sequence windows provide duplicate/replay detection and diagnostics but do not
reorder UDP. After MAC and replay checks, a valid packet may update the peer
address, allowing NAT rebinding. A session expires after the configured idle
timeout (target default 120 seconds), then its backend socket and replay state
are released.
The core rejects oversized application datagrams, increments a counter, and
logs at debug/warn level. v1 does not fragment datagrams; a conservative
starting limit is 1200 bytes.
## Encapsulated transports
- **Direct UDP:** lowest overhead and native datagrams; uses the authenticated
protocol above and provides integrity but not outer confidentiality.
- **QUIC DATAGRAM:** TLS 1.3 protects the transport; client shared-secret
challenge/response still authenticates the configured client.
- **HTTP/3 CONNECT-UDP:** uses legitimate MASQUE CONNECT-UDP and HTTP Datagram
semantics over QUIC. It is for compatibility, not fingerprint spoofing.
- **TLS/TCP:** uses the same client authentication model. Each datagram is
framed as a big-endian `u16` payload length followed by up to 65535 bytes;
the configured lower limit should normally apply. TCP head-of-line blocking
is expected.
QUIC, HTTP/3, and TLS/TCP are planned by milestones 2, 4, and 5 respectively.
The direct packet details are a protocol target; confirm the implementation and
integration tests before treating them as interoperable guarantees.
## Authentication and resource order
The server performs cheap bounds/version checks, authentication and rate-limit
checks before allocating a backend UDP socket. Public listeners are hostile:
limits, handshake timeouts, per-source caps, and malformed-packet handling are
part of the protocol boundary.
+49
View File
@@ -0,0 +1,49 @@
# Security model
## What is protected
The server treats every public listener as hostile. v1 requires a configured
client ID and at least 256 bits of entropy in its shared secret. Direct UDP
authenticates every session and every data packet, rejects replay, tolerates
authenticated NAT rebinding, and does not trust a session ID by itself.
TLS-capable transports authenticate the server through normal X.509 hostname
verification and authenticate the client with a post-connection HMAC
challenge/response.
Secrets and application payloads must not appear in logs. Cryptographic
operations belong to maintained Rust libraries; wgcloak does not implement
encryption, HMAC, HKDF, SHA, or TLS itself.
## Local permissions
Use separate secret files rather than inline values. Secret files should be
owned by the service account and mode `0600`; the parent configuration
directory should not be writable by untrusted users. The Unix control socket
must be created in a private runtime directory with restrictive ownership and
mode (normally `0600`). Do not put it in a world-writable directory.
The systemd templates apply `NoNewPrivileges`, a read-only system view, private
temporary storage, restricted address families, and other process hardening.
Review `ReadWritePaths`, certificate access, and local policy before enabling
them. Grant only `CAP_NET_BIND_SERVICE` when a low port is essential. Never
grant `CAP_NET_ADMIN` or `CAP_SYS_ADMIN` for normal operation.
## Threat boundaries
The relay protects session admission and transport integrity; it does not make
the encapsulated application secure. Direct UDP is not encrypted by wgcloak,
so use QUIC, HTTP/3, or TLS/TCP when outer confidentiality is needed. A shared
secret is a credential: rotate it out of band and avoid putting it in command
lines, shell history, or issue reports.
Resource limits and bounded parsing reduce denial-of-service exposure, but a
public relay still needs host firewalling, monitoring, and capacity planning.
Authentication failure must stop probing rather than causing endless fallback
traffic.
## Explicit non-goals
v1 does not configure WireGuard, create TUN/TAP interfaces, manage routes or
firewalls, provide traffic-shaping or browser/TLS-fingerprint evasion, hide
traffic metadata from a compromised host, cluster servers, or support multiple
simultaneous client tunnels. These boundaries are intentional.
+65
View File
@@ -0,0 +1,65 @@
# Troubleshooting
Check the simplest layer that can explain the symptom, then move outward.
## Process exits immediately
- Run the same command in the foreground and read the fatal configuration
error.
- Check that every secret/certificate path exists and is readable by the
service account, but not by unrelated users.
- Run `wgcloak --help` and, where implemented, `wgcloak check-config` to spot a
command or schema mismatch.
- Confirm listener addresses are valid and no other process owns the port.
## Client cannot connect
- Verify DNS resolution and the configured server name/port.
- Confirm the selected transport is implemented in this build; an `auto` list
may mention future transports.
- For direct UDP, check both directions in the host firewall and allow the
server's UDP listener.
- For TLS transports, verify the hostname matches the certificate and do not
disable certificate verification.
- Authentication rejection means a bad/unknown client or secret and must not
be diagnosed as a firewall failure. Fix credentials before retrying.
## Authentication failures or repeated probing
Check client ID spelling, secret-file contents, ownership/mode, and system
clock skew. Direct HELLO timestamps have a target default 60-second window;
replayed client nonces are rejected. Repeated invalid credentials should stop
fallback probing rather than generate traffic across every transport.
## Connected but no application traffic
- Confirm the local application sends to the configured local UDP address.
- Confirm the server backend is listening at `[backend].target`.
- Check `status`/`stats` or structured logs for selected transport, maximum
datagram size, dropped/oversized datagrams, and backend errors.
- v1 remembers one most-recent local source peer per client tunnel. Concurrent
source peers can replace that return destination and produce a warning.
- No application-layer fragmentation exists: reduce payload size when the
configured transport limit is exceeded (starting target is 1200 bytes).
## Session disappears
The target idle timeout is 120 seconds, refreshed by authenticated data or
keepalive. Check keepalive settings, NAT expiry, server resource limits, and
whether the client is reconnecting after a transport failure. A valid direct
UDP packet can update the peer address after NAT rebinding; a spoofed packet
without a valid MAC cannot.
## Control socket unavailable
Check the daemon's `XDG_RUNTIME_DIR`, socket ownership/mode, and whether a
stale socket path remains after an unclean exit. The control interface is local
only and may be absent until the operational-polish milestone is implemented.
## Host hardening prevents startup
Read the unit's journal output. Common causes are a secret/certificate path
outside the unit's read paths, a changed binary location, or an address family
not allowed by `RestrictAddressFamilies`. Correct the reviewed unit rather
than weakening all hardening directives. `CAP_NET_BIND_SERVICE` is only needed
for ports below 1024.