TEST_PLAN.md — deadlight-proxy

Version: v1.0 (May 2026)
Scope: Functional testing across Linux, macOS, and Android (native app)
Purpose: Systematic coverage of all major subsystems following initial
multi-platform binary release. This is a living document, results and notes
are recorded inline as testing progresses.

This document covers what was tested, how, and what was found. It aims to be an honest record of real-world validation on real hardware.


Platform Matrix

Tracking results across all three supported binary targets.

Test Area Linux/WSL macOS Android
1. Startup & Config
2. Health & Smoke
3. HTTP Proxy
4. HTTPS (CONNECT)
5. TLS Interception N/A
6. SOCKS4/5
7. Connection Pool
8. REST API
9. Plugins N/A
10. Web UI & SSE
11. VPN / TUN N/A N/A
12. Blog Cache
13. Edge / Resilience

Legend: ☑ Pass · ☒ Fail ·⚠ Partial / Known Issue · ☐ Not Yet Tested · N/A Not Applicable


Test Environment

Record your environment before starting. Differences here explain a lot of
failures.

Binary version:   ./deadlight -v
OS / arch:        uname -a
OpenSSL version:  openssl version
GLib version:     pkg-config --modversion glib-2.0
Network:          (note if behind NAT, VPN, corporate proxy, etc.)

1. Startup & Config

Why first: Nothing else is testable if the binary won't start or misreads
its config. This section establishes the baseline.

1.1 — Default startup (no config)

./deadlight -v

Expected:

Watch for:

1.2 — Config file loading

./deadlight -c deadlight.conf.example -v

Expected:

Watch for:

1.3 — CLI flag overrides

./deadlight -c deadlight.conf.example -p 9090 -v

Expected: Proxy binds to :9090, not :8080. Confirms CLI flags
take precedence over config.

1.4 — Daemon mode

./deadlight -d
ps aux | grep deadlight
kill <pid>

Expected: Process detaches from terminal, visible in process list,
terminates cleanly on SIGTERM.


2. Health & Smoke Tests

Why second: Fast, dependency-light checks you can run on every platform
and binary before deeper testing. These are your canary.

# Proxy port responding
curl -v http://localhost:8080/api/health

# Web UI port responding
curl -v http://localhost:8081

# Metrics endpoint
curl http://localhost:8080/api/metrics | jq .

Expected from /api/health:

{
  "status": "ok",
  "version": "..."
}

Expected from /api/metrics:

Watch for:


3. HTTP Proxy

Why here: The most fundamental proxy function. HTTPS, SOCKS, and TLS
interception all build on this path working correctly.

3.1 — Plain HTTP forwarding

curl -x http://localhost:8080 http://example.com -v

Expected: Full HTTP response from example.com. Verbose output should
show the CONNECT or forwarding handshake in the proxy log (if -v is running).

3.2 — Headers preserved

curl -x http://localhost:8080 http://httpbin.org/headers

Expected: Response shows your request headers as received by the upstream.
Check that X-Forwarded-For or proxy headers are present/absent as intended.

3.3 — Large response

curl -x http://localhost:8080 http://httpbin.org/bytes/10485760 -o /dev/null -w "%{speed_download}"

Expected: 10 MB downloads cleanly without truncation or connection drop.
Download speed is reasonable.

3.4 — Concurrent connections

for i in {1..20}; do
  curl -s -x http://localhost:8080 http://example.com -o /dev/null &
done
wait

Expected: All 20 requests complete. Check /api/metrics afterward for
connection counts and any error increments.


4. HTTPS (CONNECT Tunnelling)

Without TLS interception enabled, HTTPS should pass through as an opaque
tunnel via the CONNECT method.

4.1 — Basic HTTPS tunnel

curl -x http://localhost:8080 https://example.com -v

Expected: Successful HTTPS response. Proxy log should show a
CONNECT example.com:443 entry.

4.2 — Certificate verification intact

curl -x http://localhost:8080 https://expired.badssl.com/

Expected: curl reports a certificate error — the proxy should not
silently swallow TLS errors in tunnel mode.


5. TLS Interception

Prerequisite: CA cert generated and trusted by the test system.
See README — Install CA Certificate and
Generate new CA Certificate.

5.1 — CA cert setup

# Confirm CA cert is in place and trusted
curl -x http://localhost:8080 https://example.com -v 2>&1 | grep "issuer"

Expected: Issuer field shows your deadlight CA (deadmesh CA),
not the real upstream CA. This confirms interception is active.

5.2 — Certificate mimicry

curl -x http://localhost:8080 https://github.com -v 2>&1 | grep -E "subject|issuer|expire"

Expected: The presented cert's CN matches github.com (mimicry),
but the issuer is your local CA.

5.3 — Cert is re-signed, not reused

Check two different HTTPS destinations and confirm each gets a freshly
generated cert with the correct CN — not a wildcard or a cached mismatch.

5.4 — Plugin visibility (with interception active)

If a logging or inspection plugin is loaded, confirm it can see decrypted
request bodies for HTTPS traffic. This validates the interception chain
is wired through to the plugin hooks.


6. SOCKS4/5

6.1 — SOCKS5 basic

curl --socks5 localhost:8080 http://example.com

Expected: Full HTTP response via SOCKS5 tunnel.

6.2 — SOCKS5 with HTTPS

curl --socks5 localhost:8080 https://example.com

6.3 — SOCKS4

curl --socks4 localhost:8080 http://example.com

6.4 — Protocol auto-detection

Start with no explicit protocol flags on the client side where possible.
The proxy should auto-detect SOCKS vs HTTP vs other based on the opening
bytes of the connection. Check the proxy log to confirm the correct
protocol handler was invoked for each request type.


7. Connection Pool

Goal: Confirm connections are being reused, health checks are running,
and the pool degrades gracefully under failure.

7.1 — Pool warming

Send 10 sequential requests to the same upstream host, then check
/api/metrics for pool hit/miss counts:

for i in {1..10}; do
  curl -s -x http://localhost:8080 http://example.com -o /dev/null
done
curl http://localhost:8080/api/metrics | jq '.pool'

Expected: Pool reuse count increasing after the first request.

7.2 — Pool health check / stale connection

# Start proxy, make a request to warm the pool
curl -x http://localhost:8080 http://example.com -o /dev/null

# Wait longer than any keepalive timeout (e.g. 90s), then request again
sleep 90
curl -x http://localhost:8080 http://example.com -v

Expected: Request succeeds — stale connection is detected and replaced,
not returned to the client broken.

7.3 — Upstream goes away

# Point at a local service you control, warm the pool, then kill the service
# Confirm the proxy handles the broken pooled connection gracefully

Expected: Clean error response to the client (not a hang or crash).


8. REST API

Work through every endpoint category systematically.

8.1 — System endpoints

curl http://localhost:8080/api/health
curl http://localhost:8080/api/system/ip
curl http://localhost:8080/api/metrics
curl http://localhost:8080/api/logs
curl http://localhost:8080/api/dashboard

Expected: All return valid JSON. system/ip returns your external IP.
logs returns a non-empty array after proxy activity.

8.2 — SSE stream

curl -N http://localhost:8080/api/stream
# (leave running, generate proxy traffic in another terminal)

Expected: Server-sent events appear in the terminal as proxy activity
occurs. Stream stays open indefinitely without timeout.

8.3 — Email (unauthenticated)

curl -X POST http://localhost:8080/api/email/send \
  -H "Content-Type: application/json" \
  -d '{"to":"your@email.com","subject":"Deadlight Test","body":"Test from TEST_PLAN.md"}'

Expected: 200 response with confirmation. Verify delivery.

8.4 — Email (HMAC auth)

PAYLOAD='{"from":"gnarzilla@deadlight.boo","to":"gnarzilla@deadlight.boo","subject":"Auth Test","body":"HMAC test"}'
SECRET="your-auth-secret"   # replace with actual value from deadlight-conf
SIG=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" -hex | cut -d' ' -f2)

# Confirm SIG is populated before sending
echo "SIG: $SIG"

curl -X POST http://localhost:8080/api/outbound/email \
  -H "Content-Type: application/json" \
  -H "X-Signature: sha256=$SIG" \
  -d "$PAYLOAD" \
  -v

Expected: Succeeds with valid signature.

# Confirm rejection without signature
curl -X POST http://localhost:8080/api/outbound/email \
  -H "Content-Type: application/json" \
  -d "$PAYLOAD"

Expected: 401 or 403.

8.5 — Federation

# Status
curl http://localhost:8080/api/federation/status

# List posts (may be empty initially)
curl http://localhost:8080/api/federation/posts

# Domain connectivity test
curl http://localhost:8080/api/federation/test/example.com

# Receive a post (self-test)
curl -X POST http://localhost:8080/api/federation/receive \
  -H "Content-Type: application/json" \
  -d '{"author":"testuser","content":"Hello from TEST_PLAN","source":"local"}'

# Confirm it's stored
curl http://localhost:8080/api/federation/posts

Expected: Post appears in the list after receive. Confirm file is
written to /var/lib/deadlight/federation/.

8.6 — Malformed requests

# Malformed JSON
curl -X POST http://localhost:8080/api/email/send \
  -H "Content-Type: application/json" \
  -d '{not valid json'

# Missing required fields
curl -X POST http://localhost:8080/api/email/send \
  -H "Content-Type: application/json" \
  -d '{}'

# Wrong method
curl -X DELETE http://localhost:8080/api/health

Expected: Sensible error responses (400, 405), no crashes, no
hung connections.


9. Plugins

9.1 — Plugin loading

Start with autoload = adblocker,ratelimiter in config. Check startup
log for confirmation both plugins loaded successfully.

9.2 — Adblocker

# Request a known ad network domain
curl -x http://localhost:8080 http://doubleclick.net/ -v

Expected: Request is blocked — either a 200 with empty/redirect
body, or a 403/204. Check proxy log for block event.

9.3 — Rate limiter

# Rapid-fire requests
for i in {1..50}; do
  curl -s -x http://localhost:8080 http://example.com \
    -o /dev/null -w "%{http_code}\n"
done

Expected: Responses eventually include 429 Too Many Requests
once the rate limit is hit. Confirm the limit resets after the window passes.

9.4 — Plugin disable

Set enabled = false in [plugins], hot-reload, confirm previously
blocked ad domain now passes through.


10. Web UI & SSE

10.1 — Web UI loads

open http://localhost:8081   # macOS
xdg-open http://localhost:8081  # Linux
# Android: open in Chrome

Expected: UI loads without console errors. Connection table is visible.

10.2 — Live connection table

Generate proxy traffic while watching the UI. Connection rows should
appear and update in real time via SSE without a page refresh.

10.3 — SSE reconnect

Kill and restart the proxy while the web UI is open. The UI should
reconnect to the SSE stream automatically without requiring a page reload.

10.4 — Mobile UI (Android)

On Android/Termux, access the web UI from Chrome on the same device
(http://localhost:8081). Confirm the layout is usable on a small screen.


11. VPN / TUN Mode

Requires root / sudo, and --privileged in Docker.
Mark N/A for macOS until native builds support it.

# Enable in config
[vpn]
enabled = true
tun_device = tun0

# Start proxy, confirm TUN device created
sudo ./deadlight -c deadlight.conf -v
ip link show tun0

Expected: tun0 device appears and is UP. Layer 3 traffic routed
through the device reaches upstream correctly.


12. Blog Cache

Requires: workers_url set to a reachable endpoint (or
https://deadlight.boo for live testing).

# Status
curl http://localhost:8080/api/blog/status

# List posts (populates cache on first hit)
curl http://localhost:8080/api/blog/posts | jq .

# Confirm cache file written
ls -lh /var/lib/deadlight/blog/

12.1 — Offline fallback

# While online, populate the cache
curl http://localhost:8080/api/blog/posts

# Simulate upstream unavailable (block the domain or set workers_url 
# to an unreachable address in config)
# Then request again
curl http://localhost:8080/api/blog/posts

Expected: Stale cached content is returned, not an error.
This is the core edge-native use case.


13. Edge / Resilience

These tests simulate the "hostile network" conditions called out in the
README as a core design target.

13.1 — Intermittent upstream

# Use curl's --max-time to simulate slow upstream
curl -x http://localhost:8080 http://httpbin.org/delay/10 --max-time 5

Expected: Proxy times out cleanly and returns an error, no hung
connection or zombie worker thread. Check metrics after — worker
count should be back to baseline.

13.2 — Client disconnect mid-transfer

Start a large download through the proxy, kill the client mid-transfer
(Ctrl+C), and confirm:

13.3 — Rapid connect/disconnect

for i in {1..100}; do
  curl -s -x http://localhost:8080 http://example.com \
    -o /dev/null --max-time 1 &
done
wait
curl http://localhost:8080/api/metrics | jq '.connections'

Expected: Active connection count returns to near-zero after all
requests complete. No crash.

13.4 — Restart under load

While running the concurrent test above, send SIGTERM to the proxy.

Expected: Clean shutdown — no partial log writes, no corrupted
cache files, no port left bound.


Known Issues & Notes

Record findings here as testing progresses.

# Area Platform Description Status
1

How to Report Results

File bugs at GitHub Issues
with the section number, platform, binary version, and the exact command
that failed.


What's Not Covered Here

These areas are out of scope for this test plan but worth future coverage:


Part of the Deadlight ecosystem..

### Known Issues

#	Area	Description	Status
1	Version	/api/health returns 1.0.0 not v1.1.0	Fixed
2	Identity	/api/health "proxy":"deadmesh"	✅ Fixed
3	Pool	Stale connection retry	✅ Fixed
4	Headers	X-Proxy-Id injected, not configurable	⚠️ Open
5	Config watcher	Double-reload clears auth_secret briefly	⚠️ Open
6	Byte count	Plain HTTP client→upstream bytes uncounted	⚠️ Open
7	TLS intercept	Binary octet-stream truncates ~9 KB		✅ Fixed
8	Pool metrics	failed overcounts under concurrent load	⚠️ Open
9	TLS validation	Hetzner rejection — cert expired, correct	✅ Not a bug
10	Cert pinning	Teams/Skype silently drop MITM — expected	ℹ️ By design
11	Method routing	All HTTP methods return 200 on all endpoints	❌ Open
12	Auth ordering	Field validation runs before auth check	⚠️ Open
13	Docs	README missing from field in outbound email example	⚠️ Open
14	HMAC auth	X-Signature header not found despite being present — likely case-sensitive header lookup	❌ Open

**Legend:** ☑ Pass · ☒ Fail ·⚠ Partial / Known Issue · ☐ Not Yet Tested · N/A Not Applicable