Security & Anti-Abuse¶
For file-and-line-level detail on rate-limiting protocols — see dixu_proxy/README.md
(sections "TCP tunnel protection" and "HTTP interstitial protection").
This page is a project-level overview of the threat model and what's implemented.
What data passes through the server¶
HTTP/HTTPS tunnels (default mode): TLS is terminated at the proxy — traffic is decrypted on the dix.su server and re-encrypted between server and client. The server can technically read request/response bodies in transit. If that's a concern, use private E2E mode (see below).
TCP tunnels (SSH, RDP, VNC, databases): the proxy forwards bytes without reading them — it uses the TLS SNI field in the ClientHello only for routing, then relays the raw TCP stream. The actual SSH/RDP/database session is encrypted end-to-end between the client and your device.
E2E private mode: TLS is not terminated on the server at all — the proxy is a raw byte relay. The private key never leaves the device. See the Private Mode section below.
TCP tunnel protection¶
TCP ports accept an arbitrary slug in the connection — connecting to a random slug is itself a suspicious signal, since legitimate clients connect to their own one or two slugs. Therefore:
DixuProxyWeb.Tcp.RateLimiter— three independent ETS tables:- Handshake attempt rate limit per IP (soft/hard limit → ban).
- Slug diversity detector ("diversity ban") — many different slugs from one IP within a time window → long ban (strong signal of scanning/enumeration).
- Concurrent connection limit per IP (protects against process/FD exhaustion).
- Unified deny messages — no difference in the response between "slug doesn't exist" and "slug exists but device is offline" (eliminates account enumeration via response text).
- TCP ports are listened to directly by
dixu_proxyvia Docker port-forwarding, not through nginx stream-proxy — otherwise the proxy would see nginx's IP for all clients and the rate limiter would not work at all.
HTTP protection (catalog-aware)¶
For HTTP, a "diversity ban" (TCP-style) doesn't work: the tunnel catalog implies legitimate browsing of 10–20+ different public tunnels in a few minutes. Two separate mechanisms with different goals:
DixuProxyWeb.Http.VisitGuard.allow?/1— request volume limit per IP (anti-scraping/anti-DDoS), independent of slug diversity.DixuProxyWeb.Http.VisitGuard.record_slug/2— a narrower detector for enumeration of private (non-catalog) slugs only. Before counting a slug toward the "suspicious diversity" counter, it checksDixuProxy.CatalogRegistry.approved?/1— catalog slugs are excluded, otherwise the catalog feature itself would be broken.
Incident log¶
Every ban (TCP RateLimiter or HTTP VisitGuard) is written to:
- File log —
INCIDENT_LOG_DIR/YYYY-MM-DD.log, lines:timestamp\tcategory\tip\tduration\treason. Mounted as a volume on the host (./INCIDENT/logs), survives container rebuilds. - Postgres table
tunnel_incidents(migration V36) — structured, withtarget_slugwhen a real account was found among the enumerated slugs.
Admin panel — "Incidents" tab at /admin — sees all platform incidents, filterable by IP/slug/category.
Email alert to owner — if the scan targeted a real tunnel slug and the owner's plan is VIP or above, an email notification is sent (30-minute cooldown per slug).
Dashboard — "Security" section (/auth/protected#security, VIP+ plan), API GET /api/incidents/my
— only incidents on your own slug.
Private mode: HTTP/HTTPS access gate¶
A private account (users.private_mode=true, see Billing) should be
accessible only to the owner, not to anyone who knows the URL.
- Session cookie visible on subdomains —
spring.webflux.session.cookie.domain=.{BASE_DOMAIN}(previously host-only ondix.su, invisible on{slug}.dix.su).SameSite=Laxdoesn't block this (same registrable domain). GET /internal/session/whoami—dixu_proxyforwards theCookieheader tomodulauthto check whether the request comes from the tunnel owner or an invitee. Unauthorized visitors get a stub page, not the actual content.
Private mode: E2E encryption (Phase 2, deployed)¶
A separate entry point e2e-<token>.{BASE_DOMAIN} where TLS is not terminated on the server.
device_clientruns an ACME v2 (DNS-01) challenge via Cloudflare to obtain a Let's Encrypt certificate for the E2E subdomain — the private key is generated on the device and never uploaded.dixu_proxyhas a raw-relay listener (DixuProxyWeb.Tcp.E2eRawListener, port 9443) that forwards bytes without decryption — routing is done via SNI in the ClientHello.- nginx
stream{}block routes by SNI — E2E subdomains go to the raw relay, regular subdomains to the TLS-terminating proxy.
Verified on production in Chrome and Firefox.
Defense-in-depth: INTERNAL_API_SECRET¶
modulauth and dixu_proxy communicate with each other over internal HTTP endpoints (/internal/*).
These are blocked at the nginx level (location ^~ /internal/ { return 404; }) on all public
vhosts. In addition, dixu_proxy requires a secret header X-Internal-Secret on all /internal/*
routes, checked via constant-time comparison (Plug.Crypto.secure_compare/2).
This means two independent protection layers: even if someone removes the nginx guard,
calls without the secret get 401. And if the secret is somehow missing from the environment,
dixu_proxy logs a warning at startup and an admin banner appears in the dashboard.
Fixed vulnerability: X-Forwarded-For spoofing¶
DixuProxyWeb.ClientIp was taking the first value from X-Forwarded-For, but nginx
appends the real client IP at the end of the chain. A client could bypass all IP-based
protections with a single header: curl -H 'X-Forwarded-For: 1.2.3.4'. Confirmed on
production (forged header lifted an active ban); fixed — now takes the last value
(the only segment added by nginx, the rest is client-controlled).
Lesson: when trusting headers from a reverse proxy, always verify exactly how nginx
forms the header (set vs add_x_forwarded_for) instead of assuming.