From 95a63041dc87cc2551d7c99cf498f313549e6ea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 26 Oct 2026 16:43:34 +0100 Subject: [PATCH] rfc9211 --- deploy/varnish/Dockerfile | 2 +- deploy/varnish/default.vcl.template | 314 ++++++++++------------------ 2 files changed, 111 insertions(+), 205 deletions(-) diff --git a/deploy/varnish/Dockerfile b/deploy/varnish/Dockerfile index d20abc0..6b54899 100644 --- a/deploy/varnish/Dockerfile +++ b/deploy/varnish/Dockerfile @@ -1,7 +1,7 @@ # --- Stage 1: build varnish + modules --- FROM debian:trixie-slim AS builder -ARG VARNISH_VERSION=8.0.0 +ARG VARNISH_VERSION=9.1.0 ARG VARNISH_MODULES_VERSION=0.27.0 RUN apt-get update && apt-get install -y \ diff --git a/deploy/varnish/default.vcl.template b/deploy/varnish/default.vcl.template index 1b384b7..9e1e34e 100644 --- a/deploy/varnish/default.vcl.template +++ b/deploy/varnish/default.vcl.template @@ -1,33 +1,30 @@ - vcl 4.1; import vsthrottle; import std; -# ===== BACKEND ===== backend app { .host = "app"; .port = "${APP_PORT}"; } -# ===== ACL ===== acl purge { "127.0.0.1"; "::1"; } -# ===== RECV ===== sub vcl_recv { - # Wewnętrzne znaczniki - nie ufamy wartościom od klienta - unset req.http.X-Varnish-CS-Mode; - unset req.http.X-Varnish-CS-Entry; + # Internal markers can only be set by our own VCL. + unset req.http.X-RFC9211-State; + unset req.http.X-RFC9211-Entry; + unset req.http.X-RFC9211-TTL; + unset req.http.X-RFC9211-Synthetic; + unset req.http.Cache-Status; - # Rate limit: 200 / 10s, blokada 60s if (vsthrottle.is_denied(client.identity, 200, 10s, 60s)) { return (synth(429, "Too Many Requests")); } - # PURGE tylko lokalnie if (req.method == "PURGE") { if (!client.ip ~ purge) { return (synth(405, "Not allowed")); @@ -39,28 +36,25 @@ sub vcl_recv { set req.http.X-Forwarded-Proto = "https"; } - # Healthcheck / internal - if (req.url == "/healthcheck" || req.http.X-Internal-Check) { + if (req.url ~ "^/healthcheck(\?.*)?$" || req.http.X-Internal-Check) { return (pass); } - # WebSocket / socket.io - if (req.http.Upgrade ~ "(?i)websocket" || - req.url ~ "^/socket.io/") { + # Only WebSocket upgrades use pipe, not Socket.IO polling. + if (req.http.Upgrade ~ "(?i)websocket") { return (pipe); } + if (req.url ~ "^/socket\.io/") { + return (pass); + } - # Metody inne niż GET/HEAD if (req.method != "GET" && req.method != "HEAD") { return (pass); } - - # Authorization if (req.http.Authorization) { return (pass); } - # Accept-Encoding if (req.http.Accept-Encoding) { if (req.http.Accept-Encoding ~ "zstd") { set req.http.Accept-Encoding = "zstd"; @@ -73,9 +67,9 @@ sub vcl_recv { } } - # Statyczne zasoby - if (req.url ~ "^/static/" || - req.url ~ "\.(css|js|png|jpe?g|webp|svg|ico|woff2?)(\?.*)?$") { + # Public static resources do not vary by session cookie. + if (req.url ~ "^/(static|uploads)/" || + req.url ~ "(?i)\.(css|js|png|jpe?g|webp|svg|ico|woff2?)(\?.*)?$") { unset req.http.Cookie; return (hash); } @@ -83,156 +77,115 @@ sub vcl_recv { return (hash); } -# ===== PIPE ===== sub vcl_pipe { if (req.http.Upgrade) { set bereq.http.Upgrade = req.http.Upgrade; - set bereq.http.Connection = req.http.Connection; + set bereq.http.Connection = "Upgrade"; } } -# ===== HASH ===== sub vcl_hash { hash_data(req.url); - if (req.http.host) { hash_data(req.http.host); } else { hash_data(server.ip); } - if (req.http.Cookie) { hash_data(req.http.Cookie); } - if (req.http.Accept-Encoding) { hash_data(req.http.Accept-Encoding); } - if (req.http.X-Accept-Image) { hash_data(req.http.X-Accept-Image); } - - return (lookup); } -# ===== CACHE STATUS: HIT ===== +# Track actual request transitions; leave the final return to builtin VCL. sub vcl_hit { - # Zawartość świeża lub serwowana z grace - if (obj.ttl >= 0s || obj.ttl + obj.grace > 0s) { - set req.http.X-Varnish-CS-Mode = "hit"; - } else { - # Obiekt wygasł i wymaga pobrania - set req.http.X-Varnish-CS-Mode = "stale"; - } - - # Dalsze decyzje pozostawiamy builtin VCL + set req.http.X-RFC9211-State = "hit"; } -# ===== CACHE STATUS: MISS ===== sub vcl_miss { - if (req.http.X-Varnish-CS-Mode != "stale") { - set req.http.X-Varnish-CS-Mode = "miss"; - } + # 'miss' is intentionally generic: we cannot always distinguish + # uri-miss from vary-miss in this VCL hook. + set req.http.X-RFC9211-State = "miss"; } -# ===== CACHE STATUS: PASS ===== sub vcl_pass { - if (req.method != "GET" && req.method != "HEAD") { - set req.http.X-Varnish-CS-Mode = "method"; + if (req.method == "GET" || req.method == "HEAD") { + set req.http.X-RFC9211-State = "bypass"; } else { - set req.http.X-Varnish-CS-Mode = "bypass"; + set req.http.X-RFC9211-State = "method"; } } -# ===== BACKEND FETCH ===== sub vcl_backend_fetch { - # Nie przekazujemy znaczników do aplikacji - unset bereq.http.X-Varnish-CS-Mode; - unset bereq.http.X-Varnish-CS-Entry; + # Never forward diagnostic state to the application. + unset bereq.http.X-RFC9211-State; + unset bereq.http.X-RFC9211-Entry; + unset bereq.http.X-RFC9211-TTL; + unset bereq.http.X-RFC9211-Synthetic; } -# ===== BACKEND RESPONSE ===== sub vcl_backend_response { - # Zabezpieczenie wewnętrznych nagłówków - unset beresp.http.X-Varnish-CS-Detail; - unset beresp.http.X-Varnish-Local-Error; + # Origin must not control our local backend-error marker. + unset beresp.http.X-RFC9211-Synthetic; - # Respektuj no-store / private - if (beresp.http.Cache-Control ~ "(?i)no-store|private") { + # Never store backend 5xx responses. + if (beresp.status >= 500) { + set beresp.uncacheable = true; + set beresp.ttl = 0s; + return (deliver); + } + + if (beresp.http.Cache-Control ~ "(?i)no-store|private|no-cache" || + beresp.http.Surrogate-Control ~ "(?i)no-store" || + beresp.http.Vary == "*") { set beresp.uncacheable = true; set beresp.ttl = 0s; - set beresp.http.X-Varnish-CS-Detail = "uncacheable"; return (deliver); } - # Redirecty bez cache if (beresp.status >= 300 && beresp.status < 400) { set beresp.uncacheable = true; set beresp.ttl = 0s; - set beresp.http.X-Varnish-CS-Detail = "redirect"; return (deliver); } - # Statyczne: cache tylko HTTP 200 - if (bereq.url ~ "^/static/" || - bereq.url ~ "\.(css|js|png|jpe?g|webp|svg|ico|woff2?)(\?.*)?$") { + if (bereq.url ~ "^/(static|uploads)/" || + bereq.url ~ "(?i)\.(css|js|png|jpe?g|webp|svg|ico|woff2?)(\?.*)?$") { if (beresp.status != 200) { set beresp.uncacheable = true; set beresp.ttl = 0s; - set beresp.http.X-Varnish-CS-Detail = "invalid-static-status"; return (deliver); } } - # HTML zamiast JS/CSS - nie cache'uj - if (bereq.url ~ "\.(js|css)(\?.*)?$" && + if (bereq.url ~ "(?i)\.(js|css)(\?.*)?$" && beresp.http.Content-Type ~ "(?i)text/html") { set beresp.uncacheable = true; set beresp.ttl = 0s; - set beresp.http.X-Varnish-CS-Detail = "html-instead-of-static"; return (deliver); } - # Content-Type przy brakującym nagłówku - if (bereq.url ~ "\.js(\?.*)?$" && - !beresp.http.Content-Type) { - set beresp.http.Content-Type = - "application/javascript; charset=utf-8"; + if (bereq.url ~ "(?i)\.js(\?.*)?$" && !beresp.http.Content-Type) { + set beresp.http.Content-Type = "application/javascript; charset=utf-8"; + } + if (bereq.url ~ "(?i)\.css(\?.*)?$" && !beresp.http.Content-Type) { + set beresp.http.Content-Type = "text/css; charset=utf-8"; } - if (bereq.url ~ "\.css(\?.*)?$" && - !beresp.http.Content-Type) { - set beresp.http.Content-Type = - "text/css; charset=utf-8"; - } - - # ===== STATYCZNE ===== - if (bereq.url ~ "^/static/" || - bereq.url ~ "\.(css|js|png|jpe?g|webp|svg|ico|woff2?)(\?.*)?$") { - + if (bereq.url ~ "^/(static|uploads)/" || + bereq.url ~ "(?i)\.(css|js|png|jpe?g|webp|svg|ico|woff2?)(\?.*)?$") { unset beresp.http.Set-Cookie; - # Usuń Vary: Cookie if (beresp.http.Vary) { - set beresp.http.Vary = regsuball( - beresp.http.Vary, - "(?i)(^|,)[[:space:]]*Cookie[[:space:]]*(,|$)", - "\1" - ); - - set beresp.http.Vary = regsuball( - beresp.http.Vary, ",[[:space:]]*,", "," - ); - - set beresp.http.Vary = regsub( - beresp.http.Vary, "^[[:space:]]*,[[:space:]]*", "" - ); - - set beresp.http.Vary = regsub( - beresp.http.Vary, "[[:space:]]*,[[:space:]]*$", "" - ); - + set beresp.http.Vary = regsuball(beresp.http.Vary, "(?i)(^|,)[[:space:]]*Cookie[[:space:]]*(,|$)", "\1"); + set beresp.http.Vary = regsuball(beresp.http.Vary, ",[[:space:]]*,", ","); + set beresp.http.Vary = regsub(beresp.http.Vary, "^[[:space:]]*,[[:space:]]*", ""); + set beresp.http.Vary = regsub(beresp.http.Vary, "[[:space:]]*,[[:space:]]*$", ""); if (beresp.http.Vary ~ "^[[:space:]]*$") { unset beresp.http.Vary; } @@ -240,41 +193,24 @@ sub vcl_backend_response { if (!(beresp.http.Cache-Control ~ "(?i)(s-maxage|max-age)")) { set beresp.ttl = 24h; - set beresp.http.Cache-Control = - "public, max-age=86400, immutable"; + set beresp.http.Cache-Control = "public, max-age=86400, immutable"; } set beresp.grace = 1h; set beresp.keep = 24h; } - # ===== TTL ===== if (beresp.http.Cache-Control ~ "(?i)s-maxage=([0-9]+)") { - set beresp.ttl = std.duration( - regsub( - beresp.http.Cache-Control, - "(?i).*s-maxage=([0-9]+).*", - "\1" - ) + "s", 0s - ); + set beresp.ttl = std.duration(regsub(beresp.http.Cache-Control, "(?i).*s-maxage=([0-9]+).*", "\1") + "s", 0s); } else if (beresp.http.Cache-Control ~ "(?i)max-age=([0-9]+)") { - set beresp.ttl = std.duration( - regsub( - beresp.http.Cache-Control, - "(?i).*max-age=([0-9]+).*", - "\1" - ) + "s", 0s - ); + set beresp.ttl = std.duration(regsub(beresp.http.Cache-Control, "(?i).*max-age=([0-9]+).*", "\1") + "s", 0s); } else if (beresp.http.Expires) { set beresp.ttl = std.time(beresp.http.Expires, now) - now; - if (beresp.ttl < 0s) { set beresp.ttl = 0s; } - } else { - if (beresp.ttl <= 0s) { - set beresp.ttl = 60s; - } + } else if (beresp.ttl <= 0s) { + set beresp.ttl = 60s; } if (beresp.http.Cache-Control ~ "(?i)immutable") { @@ -282,117 +218,87 @@ sub vcl_backend_response { set beresp.keep = 24h; } - # ===== GZIP ===== - if (!beresp.http.Content-Encoding && - bereq.http.Accept-Encoding ~ "gzip") { - - if (beresp.http.Content-Type ~ - "(?i)text/|application/(javascript|json|xml)") { + if (!beresp.http.Content-Encoding && bereq.http.Accept-Encoding ~ "gzip") { + if (beresp.http.Content-Type ~ "(?i)text/|application/(javascript|json|xml)") { set beresp.do_gzip = true; } } - # ===== STREAMING ===== if (beresp.http.Content-Length && std.integer(beresp.http.Content-Length, 0) > 1048576) { set beresp.do_stream = true; } } -# ===== BACKEND ERROR ===== sub vcl_backend_error { - # Odpowiedź wygenerowana lokalnie - bez Cache-Status - set beresp.http.X-Varnish-Local-Error = "1"; + # Synthetic fetch errors were not served from cache. + set beresp.uncacheable = true; + set beresp.ttl = 0s; + set beresp.http.X-RFC9211-Synthetic = "1"; return (deliver); } -# ===== DELIVER: RFC 9211 ===== sub vcl_deliver { - unset req.http.X-Varnish-CS-Entry; - - # Nie oznaczamy lokalnych błędów jako odpowiedzi backendu - if (!resp.http.X-Varnish-Local-Error) { - - # HIT - również stale/grace dostarczony z cache - if (req.http.X-Varnish-CS-Mode == "hit") { - set req.http.X-Varnish-CS-Entry = "varnish; hit"; - - # TTL jako integer, również ujemny - set req.http.X-Varnish-CS-Entry = - req.http.X-Varnish-CS-Entry + - "; ttl=" + - std.real2integer(std.real(obj.ttl, 0.0), 0); - - # MISS / STALE - pobranie z backendu - } else if (req.http.X-Varnish-CS-Mode == "miss" || - req.http.X-Varnish-CS-Mode == "stale") { - - set req.http.X-Varnish-CS-Entry = - "varnish; fwd=" + req.http.X-Varnish-CS-Mode; - - if (!obj.uncacheable && obj.ttl > 0s) { - set req.http.X-Varnish-CS-Entry = - req.http.X-Varnish-CS-Entry + "; stored"; - - set req.http.X-Varnish-CS-Entry = - req.http.X-Varnish-CS-Entry + - "; ttl=" + - std.real2integer(std.real(obj.ttl, 0.0), 0); - } - - # PASS / metoda wymagająca forwardowania - } else if (req.http.X-Varnish-CS-Mode == "bypass" || - req.http.X-Varnish-CS-Mode == "method") { - - set req.http.X-Varnish-CS-Entry = - "varnish; fwd=" + req.http.X-Varnish-CS-Mode; - } - - # Opcjonalny detail z backend_response - if (req.http.X-Varnish-CS-Entry && - resp.http.X-Varnish-CS-Detail) { - set req.http.X-Varnish-CS-Entry = - req.http.X-Varnish-CS-Entry + - "; detail=" + resp.http.X-Varnish-CS-Detail; - } - - # RFC 9211: zachowaj wpisy poprzednich cache - if (req.http.X-Varnish-CS-Entry) { - if (resp.http.Cache-Status) { - set resp.http.Cache-Status = - resp.http.Cache-Status + ", " + - req.http.X-Varnish-CS-Entry; + # Cache-Status MUST be calculated per request, not cached as an origin header. + # Synthetic backend errors do not receive a local Cache-Status member. + if (resp.http.X-RFC9211-Synthetic) { + unset resp.http.Cache-Status; + } else { + if (req.http.X-RFC9211-State == "hit") { + # VCL DURATION -> STRING produces seconds with 3 decimal places. + # RFC 9211 ttl is an integer; negative subsecond TTL is rounded to -1. + set req.http.X-RFC9211-TTL = obj.ttl; + if (obj.ttl < 0s && obj.ttl > -1s) { + set req.http.X-RFC9211-TTL = "-1"; } else { - set resp.http.Cache-Status = - req.http.X-Varnish-CS-Entry; + set req.http.X-RFC9211-TTL = regsub(req.http.X-RFC9211-TTL, "\.[0-9]+$", ""); + } + set req.http.X-RFC9211-Entry = "varnish; hit; ttl=" + req.http.X-RFC9211-TTL; + } else if (req.http.X-RFC9211-State == "miss") { + set req.http.X-RFC9211-Entry = "varnish; fwd=miss"; + if (obj.uncacheable) { + set req.http.X-RFC9211-Entry = req.http.X-RFC9211-Entry + "; stored=?0"; + } else { + set req.http.X-RFC9211-Entry = req.http.X-RFC9211-Entry + "; stored"; + } + } else if (req.http.X-RFC9211-State == "method") { + set req.http.X-RFC9211-Entry = "varnish; fwd=method; stored=?0"; + } else if (req.http.X-RFC9211-State == "bypass") { + set req.http.X-RFC9211-Entry = "varnish; fwd=bypass; stored=?0"; + } + + # Preserve the existing upstream cache chain, append Varnish LAST. + # If the request path is unknown, avoid fabricating a bypass. + if (req.http.X-RFC9211-Entry) { + if (resp.http.Cache-Status) { + set resp.http.Cache-Status = resp.http.Cache-Status + ", " + req.http.X-RFC9211-Entry; + } else { + set resp.http.Cache-Status = req.http.X-RFC9211-Entry; } } } - # Czyszczenie wewnętrznych nagłówków - unset resp.http.X-Varnish-CS-Detail; - unset resp.http.X-Varnish-Local-Error; - unset resp.http.X-Pass-Reason; + unset resp.http.X-RFC9211-Synthetic; unset resp.http.X-Cache; - - # Age zachowujemy dla odpowiedzi z cache - if (obj.uncacheable || obj.hits == 0) { - unset resp.http.Age; - } - + unset resp.http.X-Pass-Reason; unset resp.http.Via; unset resp.http.X-Varnish; unset resp.http.Server; + unset resp.http.Content-Disposition; + # Preserve Age (it is standard HTTP cache metadata). } -# ===== SYNTH ===== sub vcl_synth { - # RFC 9211: bez Cache-Status dla lokalnych odpowiedzi + set resp.http.Cache-Control = "private, no-store, no-cache"; unset resp.http.Cache-Status; unset resp.http.X-Cache; + unset resp.http.X-Pass-Reason; + unset resp.http.Via; + unset resp.http.X-Varnish; + unset resp.http.Server; + unset resp.http.Content-Disposition; } -# ===== PURGE ===== sub vcl_purge { return (synth(200, "Purged")); }