XTQ/Web/DocsDownload

Protection

Limits on backends, the browser check and its thresholds, scanner probe answers, and caps on connections and TLS handshakes.

This page collects every limit and defence xTQ Web has, for traffic spikes and attacks alike.

Layer Limit Default Over the limit
Connections XTQW_MAX_CONNS 16384 New connections are closed at once
TLS handshakes at once XTQW_MAX_HANDSHAKES 2 per CPU A connection waits up to XTQW_HANDSHAKE_WAIT_MS (800), then is closed
Time for TLS handshake and request headers XTQW_HEADER_TIMEOUT 10 s The connection is closed
Requests in flight per proxy pool limit in proxy 1024, wait 3 s 503 Server busy, retry in a moment
Open WebSockets XTQW_MAX_TUNNELS 16384 503 Server busy, retry in a moment
PHP requests per client address XTQW_PER_IP_SLOTS 4 503 Too many concurrent requests from your address, retry shortly
PHP requests in total learned from xTQ PHP workers plus queue Wait up to XTQW_EDGE_WAIT_MS (3000), then 503 Server busy, retry in a moment
Unknown browsers under load challenge, or built in for php on under load The browser check page
Scanner probes probes in a site on 404

Backend pools

Every proxy pool has a cap on requests in flight, 1024 by default. Past it, requests wait up to 3 seconds for a slot and then get a fast 503 with Server busy, retry in a moment and Retry-After: 2. The backends never see more than the cap. Change it with limit <n> [wait], or remove it with limit off. Details in Reverse proxy.

PHP sites

php sites have their own protection, all on by default:

  • a waiting room that lets only as many requests into xTQ PHP as it can hold,
  • a fair share of 4 requests in PHP at once per client address,
  • the browser check while xTQ PHP is overloaded,
  • the page cache, which keeps serving cached and stale pages to everyone.

Details in Handlers.

Browser check

The browser check sits in front of a route and lets real browsers through while keeping out clients that don't run JavaScript. Turn it on with challenge in a site or route block. For php sites it is built in and controlled by XTQW_CHALLENGE.

When it is shown

Setting Shown to visitors without a pass
challenge (same as challenge overload) While the backend that would answer is under heavy load
challenge always Always
php sites with XTQW_CHALLENGE=1 While xTQ PHP is overloaded

For a proxy pool, "under heavy load" starts when 75% of its in-flight cap is in use, or when any request is waiting for a slot. It ends when use drops below 50%. The gap keeps it from flickering. With limit off the share is measured against 1024. The change is logged:

xtq-web: example.com /checkout/*: under heavy load, browser check on (75% of its in-flight cap)
xtq-web: example.com /checkout/*: load back to normal, browser check off (49% of its in-flight cap)

For a php site, xTQ Web asks xTQ PHP for its load every 150 ms. It counts as overloaded while at least XTQW_OVERLOAD_BUSY_PCT percent of the workers are busy (default 90), or the queue is at least XTQW_OVERLOAD_QUEUE_PCT percent full (default 60), or PHP can't be reached. It also switches on the moment xTQ PHP answers that its queue is full.

In overload mode, routes answered by files, redirect, respond or abort are never checked. They cost next to nothing.

What the visitor sees

A small page titled "Verifying your browser" with status 503, Retry-After: 1, Cache-Control: no-store, X-Xtq-Cache: CHALLENGE and a noindex robots tag. The page finds a number such that the SHA-256 hash of the challenge and that number starts with a given count of zero bits. It runs in a Web Worker so it keeps going in a background tab, and falls back to the page itself where Workers are blocked. A browser takes a moment. Then it posts the answer to /.xtq/verify, receives a pass cookie and reloads the page.

Setting Default Meaning
XTQW_POW_BITS 18 Zero bits required. Each extra bit doubles the work. Kept between 8 and 28
XTQW_SECRET random at each start Key that signs challenges and passes

A challenge must be solved within 5 minutes. The pass is the xtq_pass cookie, valid for 24 hours, set with Path=/, SameSite=Lax and HttpOnly. It is good on every route of the site that has a challenge. Passes are signed, so nothing is stored per visitor.

The cookie is also marked Secure when the visitor came over HTTPS: on an https listener, or through a proxy in trust that sends X-Forwarded-Proto: https. On plain HTTP it is set without Secure, so the check works there too.

Only a valid xtq_pass counts as a pass. Other cookies, such as an app's own login cookie, don't let a visitor skip the check.

/.xtq/verify answers 204 with the cookie on success, 403 invalid solution for a wrong or expired answer, and 400 for a malformed request. It reads at most 4 KB of body within 5 seconds.

Things to know

  • Without XTQW_SECRET, every restart and every reload makes a new key, and every visitor has to pass again. Set it to a long random string in /etc/xtq-web/env.
  • Clients that don't run JavaScript, such as curl, API clients, webhooks and most crawlers, never get a pass. While the check is on they get the 503 page. Keep challenge off API routes that machines call, or put it only on the routes browsers load.
  • In overload mode the check is off almost all the time. Use challenge always if you want to see it on a quiet site.

Scanner probes

Bots request paths like /.env or /../../etc/passwd on every server they find. xTQ Web answers them with 404 as soon as the site is chosen, before any route is looked at, so they never reach your apps.

A request is a probe when its path, lowercased, contains /../, ..\, %2e%2e, ..%2f, %252e or %c0%ae, or ends in /...

It is also a probe when the path, once percent-decoded, has any of:

  • a segment named .env or starting with .env.,
  • a segment named .git, .svn, .hg, .aws, .ssh, .docker, .kube, .npmrc, .htpasswd, .bash_history, id_rsa, .vscode, .idea, .ds_store or .htaccess,
  • a segment named phpinfo.php, info.php, server-status, actuator, cgi-bin, boaform, shell.php or eval-stdin.php,
  • a segment named wp-config.bak, config.php.bak, backup.sql, dump.sql or database.sql, or anything containing /wp-config.php. or /wp-config.php~,
  • the segments etc/passwd, etc/shadow, proc/self or vendor/phpunit in a row.

These match at any depth, so /api/.env and /static/cgi-bin/x count too. Paths like /.well-known/..., /.github-pages/, /environment or /blog/git-tips are not probes.

The answer is 404 with Cache-Control: no-store and a short plain-text note that repeats the path. Set XTQW_PROBE_MEMES=0 to answer with a plain Not Found instead. Probes are counted on the dashboard and as xtq_probes_total. A request for a hostname no site answers gets the plain 404 Not Found, whatever its path.

The check is on for every site. If an app of yours really serves one of these paths, such as Spring Boot's /actuator/health, turn it off for that site with probes off, directly in the site block:

site app.example.com {
    probes off
    proxy 127.0.0.1:8080
}

probes on is the default. probes goes only directly in a site block, not in a route, and covers the whole site. Other sites keep the check.

Connections and handshakes

Each open client connection takes one of XTQW_MAX_CONNS slots (default 16384). The number is capped at half the process's open-file limit, which xTQ Web raises to the hard limit at startup. The packaged unit sets that limit to 1048576. A connection over the cap is accepted and closed at once.

TLS handshakes are the most CPU-heavy work xTQ Web does, so at most XTQW_MAX_HANDSHAKES run at once (default two per CPU). A new TLS connection waits up to XTQW_HANDSHAKE_WAIT_MS (default 800) for a slot. If none frees up, the connection is closed before any cryptography starts. Under a connection flood the CPU stays bounded and the visitors already connected keep being served.

The TLS handshake must finish, and an HTTP/1.1 client must send its request headers, within XTQW_HEADER_TIMEOUT seconds (default 10). An HTTP/2 connection carries at most 256 requests at once and is pinged every 30 seconds.

The listen queue is 65535 deep, so a burst of new connections waits in the kernel instead of being refused. The kernel caps it at net.core.somaxconn.

TLS session resumption is on, so returning visitors skip most of the handshake cost.