Hello community,
maybe someone has an idea.
I have an strange issue: I have set up traefik for TLS passthrough to another traefik which is terminating TLS and then forwarding the traffic to a nextcloud container.
It works fine with macOS NextCloud app and any browser I have tested so far.
It does not work with the iOS NextCloud app, which complains about the certificate, because the first traefik (let’s call it “passthrough-traefik) is serving the default traefik cert instead of passing the traffic through, for some reason.
Disabling HTTP3 on traefik fixes the issue.
Additional questions, to improve my knowledge (different LLMs gave me contradicting answers on this...)
- Is it possible to reverse proxy QUIC traffic at all? (I think yes, but I can see the nextcloud iOS app falling back to TCP somehow)
- Is it possible to do SNI routing on QUIC traffic? (assuming no ECH)
- If the answer to 2/ is no, is it possible to route based on
ClientIPfor example, or is any kind of routing not possible anymore?
Back to the issue I have:
I have see the below in the passthrough-Traefik logs:
traefik-public | 2026-04-02T19:29:54Z DBG github.com/traefik/traefik/v3/pkg/tls/tlsmanager.go:288 > Serving default certificate for request: "my.sub.domain.com"
I captured a pcap of the traffic from the nextcloud iOS app and I see it does QUIC at first, and then sends the clientHello over TCP (SNI is not encrypted, I can see it in the pcap).
Question 4: is a fallback from QUIC to TCP expected with the below config or should it allow it through?
docker-compose.yml
services:
traefik:
image: traefik:v3
container_name: traefik-public
network_mode: host
restart: unless-stopped
# Security hardening
security_opt:
- no-new-privileges:true
cap_drop:
- NET_RAW
command:
# Global settings
- "--global.checkNewVersion=false"
- "--global.sendAnonymousUsage=false"
# Access logs for CrowdSec
- "--accesslog=true"
- "--accesslog.format=json"
- "--accesslog.fields.headers.defaultmode=drop"
- "--accesslog.fields.headers.names.X-Forwarded-For=keep"
- "--accesslog.filepath=/var/log/traefik/access.log"
# EntryPoints (Only 443 TCP and UDP)
- "--entrypoints.https-IPv4.address=${VPS_PUBLIC_IPV4}:443"
- "--entrypoints.https-IPv6.address=${VPS_PUBLIC_IPV6}:443"
# HTTP/3 (QUIC) configuration
- "--entrypoints.https-IPv4.http3=true"
- "--entrypoints.https-IPv6.http3=true"
# Providers
- "--providers.docker=false"
- "--providers.file.filename=/etc/traefik/config/dynamic.yml"
- "--providers.file.watch=true"
- "--providers.file.debugLogGeneratedTemplate=true"
# API (Disabled for security)
- "--api.dashboard=false"
- "--api.insecure=false"
# Log level
- "--log.level=DEBUG"
# must load .env file into Traefik's service environment
# in order for dynamic.yml to use these variables in go templates.
env_file:
- .env
volumes:
# Dynamic configuration directory
- ${DOCKER_PATH}/traefik/dynamic.yml:/etc/traefik/config/dynamic.yml:ro
# for crowdsec
- ${DOCKER_PATH}/traefik/logs:/var/log/traefik
dynamic config contains multiple probably irrelevant things, I didn't filter out for fear of removing something helpful:
dynamic.yml
tcp:
routers:
immich-ipv4-router:
entryPoints: ["https-IPv4"]
rule: "HostSNI(`{{ env "PUBLIC_IMMICH_DOMAIN" }}`) && ClientIP(`0.0.0.0/0`)"
service: traefik-ipv4-service
tls:
passthrough: true
immich-ipv6-router:
entryPoints: ["https-IPv6"]
rule: "HostSNI(`{{ env "PUBLIC_IMMICH_DOMAIN" }}`) && ClientIP(`::/0`)"
service: traefik-ipv6-service
tls:
passthrough: true
nextcloud-ipv4-router:
entryPoints: ["https-IPv4"]
rule: "HostSNI(`{{ env "PUBLIC_NEXTCLOUD_DOMAIN" }}`) && ClientIP(`0.0.0.0/0`)"
service: traefik-ipv4-service
tls:
passthrough: true
nextcloud-ipv6-router:
entryPoints: ["https-IPv6"]
rule: "HostSNI(`{{ env "PUBLIC_NEXTCLOUD_DOMAIN" }}`) && ClientIP(`::/0`)"
service: traefik-ipv6-service
tls:
passthrough: true
collabora-ipv4-router:
entryPoints: ["https-IPv4"]
rule: "HostSNI(`{{ env "COLLABORA_DOMAIN" }}`) && ClientIP(`0.0.0.0/0`)"
service: traefik-ipv4-service
tls:
passthrough: true
collabora-ipv6-router:
entryPoints: ["https-IPv6"]
rule: "HostSNI(`{{ env "COLLABORA_DOMAIN" }}`) && ClientIP(`::/0`)"
service: traefik-ipv6-service
tls:
passthrough: true
services:
traefik-ipv4-service:
loadBalancer:
servers:
- address: "{{ env "LOCAL_TRAEFIK_IPV4" }}:443"
healthCheck:
interval: "30s"
timeout: "5s"
traefik-ipv6-service:
loadBalancer:
servers:
- address: "{{ env "LOCAL_TRAEFIK_IPV6" }}:443"
healthCheck:
interval: "30s"
timeout: "5s"
Environment details:
- Ubuntu 24
- Docker 29
- Traefik 3.6.9
I also found this, maybe related: