Traefik client for Cloudflare Tailscale DNS
A first-party extension/plugin for the cloudflare-tailscale-dns project. This Docker image runs Traefik with dynamic configuration fetched from the Cloudflare Tailscale DNS sync service's SSE endpoint (/api/config). Traefik automatically routes traffic to Tailscale machines based on WAN domain DNS records.
| Feature | Description |
|---|---|
| Dynamic Configuration | Real-time Traefik configuration updates via Server-Sent Events (SSE) |
| Automatic Reconnection | Automatically reconnects to SSE endpoint with configurable retry delays |
| HTTP/HTTPS Routing | Routes HTTP and HTTPS traffic to Tailscale machines based on domain names |
| TCP Routing | Supports TCP routing with TLS passthrough for protocols like MySQL, SSH, RDP |
| ACL-Based Configuration | Routes configured via Tailscale ACL host entries using SRV record format |
| Multiple Entrypoints | Supports multiple Traefik entrypoints (web, websecure, tcp, custom) |
sequenceDiagram
participant GEN as generate-config
participant SSE as cloudflare-tailscale-dns<br/>/api/config
participant FILE as Dynamic Config File
participant TRAEFIK as Traefik
participant TS as Tailscale Machines
GEN->>SSE: Connect to SSE endpoint
SSE-->>GEN: machines-snapshot event
GEN->>GEN: Parse ACL hosts
GEN->>GEN: Generate Traefik config
GEN->>FILE: Write dynamic.yml
FILE->>TRAEFIK: File provider detects change
TRAEFIK->>TRAEFIK: Reload configuration
Note over TRAEFIK,TS: Routes traffic based on<br/>ACL host entries
TRAEFIK->>TS: Forward requests to<br/>Tailscale IPs
| Component | Description |
|---|---|
| generate-config | Rust binary that connects to SSE endpoint, receives configuration snapshots, and generates Traefik dynamic configuration JSON |
SSE Endpoint (/api/config) |
First-party API endpoint from the cloudflare-tailscale-dns project. Cloudflare Worker endpoint that streams machines-snapshot events containing Tailscale devices and ACL configuration |
| Traefik File Provider | Watches the dynamic configuration file and automatically reloads when changes are detected |
| Traefik | Reverse proxy that routes traffic to Tailscale machines based on the dynamic configuration |
| Requirement | Description |
|---|---|
| Docker | Docker installed and running |
| cloudflare-tailscale-dns | The cloudflare-tailscale-dns service must be deployed and accessible |
| SSE Endpoint | Access to the /api/config SSE endpoint from the cloudflare-tailscale-dns service |
| WAN Domain Records | The cloudflare-tailscale-dns service must have WAN domain records configured |
docker build -t cloudflare-tailscale-traefik .| Name | Type | Required | Description | Example/Default |
|---|---|---|---|---|
TRAEFIK_CONFIG_API_ENDPOINT |
Variable | Yes | The SSE (Server-Sent Events) endpoint URL from the cloudflare-tailscale-dns service that streams Traefik dynamic configuration. This first-party API endpoint streams configuration snapshots via SSE events (machines-snapshot) |
https://your-worker.your-subdomain.workers.dev/api/config |
TRAEFIK_DYNAMIC_CONFIG_FILE |
Variable | No | Path to the dynamic configuration file | /etc/traefik/dynamic.yml |
TRAEFIK_CONFIG_RETRY_DELAY_SECONDS |
Variable | No | Delay between reconnection attempts in seconds | 5 |
HTTP_PORT |
Variable | No | Port for HTTP entrypoint | 80 |
HTTPS_PORT |
Variable | No | Port for HTTPS entrypoint | 443 |
TCP_PORT |
Variable | No | Port for TCP with TLS entrypoint | 4433 |
Basic usage:
TRAEFIK_CONFIG_API_ENDPOINT=https://cloudflare-tailscale-dns.dlkn.workers.dev/api/config docker run -ti \
--name traefik \
-p 80:80 \
-p 443:443 \
-p 4433:4433 \
-p 8080:8080 \
-e TRAEFIK_CONFIG_API_ENDPOINT \
cloudflare-tailscale-traefikWith custom ports:
TRAEFIK_CONFIG_API_ENDPOINT=https://cloudflare-tailscale-dns.dlkn.workers.dev/api/config \
HTTP_PORT=8080 \
HTTPS_PORT=8443 \
TCP_PORT=9443 \
docker run -d \
--name traefik \
-p 8080:8080 \
-p 8443:8443 \
-p 9443:9443 \
-p 8080:8080 \
-e TRAEFIK_CONFIG_API_ENDPOINT \
-e HTTP_PORT \
-e HTTPS_PORT \
-e TCP_PORT \
cloudflare-tailscale-traefikThe Traefik static configuration is in traefik.yml.template and includes:
| Component | Description |
|---|---|
| Entrypoints | HTTP (port 80 by default, configurable via HTTP_PORT) with redirect to HTTPS, HTTPS (port 443 by default, configurable via HTTPS_PORT), TCP with TLS (port 4433 by default, configurable via TCP_PORT) |
| File Provider | Watches the dynamic configuration file generated by the generate-config binary |
| API Dashboard | Enabled on port 8080 (insecure mode - configure authentication for production) |
The dynamic configuration is streamed from the /api/config endpoint (provided by the cloudflare-tailscale-dns service) via Server-Sent Events (SSE). The generate-config binary processes SSE events and generates JSON in the following format:
{
"http": {
"routers": {
"router-server1-wan-example-com": {
"rule": "Host(`server1.wan.example.com`)",
"service": "service-server1-wan-example-com-http-3000",
"entryPoints": ["web"]
}
},
"services": {
"service-server1-wan-example-com-http-3000": {
"loadBalancer": {
"servers": [{"url": "http://100.64.0.1:3000"}]
}
}
}
},
"tcp": {
"routers": {
"router-database-wan-example-com-mysql": {
"rule": "HostSNI(`database.wan.example.com`)",
"service": "service-database-wan-example-com-mysql-3306",
"entryPoints": ["tcp"],
"tls": {"passthrough": false}
}
},
"services": {
"service-database-wan-example-com-mysql-3306": {
"loadBalancer": {
"servers": [{"address": "100.64.0.1:3306"}]
}
}
}
}
}Traefik routes are configured via Tailscale ACL host entries using the following SRV record style format:
_<traefik_entrypoint>_<ts_scheme>_<ts_port>._<tcp/udp>.<domain>
| Component | Description |
|---|---|
<traefik_entrypoint> |
The name of a Traefik entrypoint defined in traefik.yml.template. Must match exactly with an entrypoint defined in your Traefik configuration. For HTTP routers (http/https schemes): Determines which Traefik entrypoint to use (e.g., web, websecure). For TCP routers (all other schemes): Determines which Traefik entrypoint to use (e.g., tcp, tcp-secure) |
<ts_scheme> |
Backend URL scheme or protocol identifier. http or https: Creates HTTP routers, used to build the backend service URL scheme. All other schemes (e.g., tls, mysql, ssh, rdp): Creates TCP routers. If tls: Enables TLS passthrough (backend handles TLS). If anything else: Disables TLS passthrough (plain TCP connection) |
<ts_port> |
Backend service port on the Tailscale machine |
<tcp/udp> |
Transport protocol (only tcp is currently supported) |
<domain> |
Domain name for routing |
The available Traefik entrypoints are defined in traefik.yml.template. By default, the template includes:
| Entrypoint | Port | Description |
|---|---|---|
web |
80 (configurable via HTTP_PORT) |
HTTP entrypoint with redirect to HTTPS |
websecure |
443 (configurable via HTTPS_PORT) |
HTTPS entrypoint |
tcp |
4433 (configurable via TCP_PORT) |
TCP entrypoint |
You can add custom entrypoints by modifying traefik.yml.template. The <traefik_entrypoint> in your ACL host entries must match an entrypoint name defined in your Traefik configuration.
| Example | Entrypoint | Scheme | Port | Router Type | Backend | TLS Passthrough |
|---|---|---|---|---|---|---|
_web_http_3000._tcp.server1.wan.example.com |
web (80) |
http |
3000 | HTTP | http://100.64.0.1:3000 |
N/A |
_websecure_https_3000._tcp.server1.wan.example.com |
websecure (443) |
https |
3000 | HTTP | https://100.64.0.1:3000 |
N/A |
_tcp_tls_443._tcp.secure-service.wan.example.com |
tcp (4433) |
tls |
443 | TCP | 100.64.0.1:443 |
Yes |
_tcp_mysql_3306._tcp.database.wan.example.com |
tcp (4433) |
mysql |
3306 | TCP | 100.64.0.1:3306 |
No |
Note: The IP address used for backend services is taken directly from the Tailscale ACL hosts configuration. The ACL host value is used as the IP address for Traefik service backends.
The cloudflare-tailscale-dns service creates A records for each machine across three domains (TS, WAN, and LAN). The WAN domain records contain the public IP address (before router NAT), making them ideal for dyndns-style CNAME records without installing separate dyndns software.
Example: api.example.com → CNAME → server1.wan.example.com
This allows you to use custom domain names while leveraging the automatically managed WAN domain records. The WAN IPs typically point to Traefik, which then routes traffic to service backends based on ACL host entries.
Traefik Routing: WAN domain records point to Traefik (the public IP). Traefik then routes traffic to backend services using Tailscale IPs from ACL host entries. The machine running Traefik must have Tailscale running (tailscale up) to reach backend services via Tailscale IPs.
WAN vs Direct Backend: WAN IPs are not meant for direct service backend connections. They point to Traefik, which handles routing to the actual services based on domain names and ACL configuration.
sequenceDiagram
participant CLIENT as Client
participant DNS as Cloudflare DNS
participant TRAEFIK as Traefik<br/>(WAN IP)
participant TS as Tailscale Network
participant BACKEND as Backend Service<br/>(Tailscale IP)
Note over CLIENT,DNS: DNS Resolution Phase
CLIENT->>DNS: 1. Query api.example.com
DNS->>DNS: 2. CNAME → server1.wan.example.com
DNS->>DNS: 3. A Record → WAN IP (public, before NAT)
DNS-->>CLIENT: 4. Return WAN IP
Note over CLIENT,BACKEND: Traffic Routing Phase
alt HTTP/HTTPS Request
CLIENT->>TRAEFIK: 5. HTTP/HTTPS to WAN IP<br/>Host: api.example.com
TRAEFIK->>TRAEFIK: 6. Match domain to ACL entry<br/>_web_http_3000._tcp.server1.wan.example.com
TRAEFIK->>TS: 7. Route via Tailscale IP<br/>100.x.y.z:3000
TS->>BACKEND: 8. Forward to backend
BACKEND-->>TS: 9. Response
TS-->>TRAEFIK: 10. Response
TRAEFIK-->>CLIENT: 11. Response
else TCP Request (SSH/RDP)
CLIENT->>TRAEFIK: 5. TCP to WAN IP:4433<br/>SNI: server1.wan.example.com
TRAEFIK->>TRAEFIK: 6. Match SNI to ACL entry<br/>_tcp_ssh_22._tcp.server1.wan.example.com
TRAEFIK->>TS: 7. Route via Tailscale IP<br/>100.x.y.z:22
TS->>BACKEND: 8. Forward to backend
BACKEND-->>TS: 9. Data
TS-->>TRAEFIK: 10. Data
TRAEFIK-->>CLIENT: 11. Data
end
Flow Explanation:
DNS Resolution:
- Client queries
api.example.com - DNS resolves CNAME to
server1.wan.example.com - DNS resolves A record to WAN IP (public IP, before router NAT)
- Client receives WAN IP address
Traffic Routing: 5. Client sends HTTP/HTTPS/TCP traffic to Traefik at WAN IP 6. Traefik matches domain name (HTTP) or SNI (TCP) to ACL host entry 7. Traefik routes to backend service using Tailscale IP from ACL configuration 8. Traffic flows through Tailscale network to backend service 9-11. Response/data flows back through the same path
Traefik TCP routers use SNI (Server Name Indication) to route traffic based on domain names. Protocols like SSH and RDP don't send SNI headers by default, so you need a local proxy/port forward that adds the SNI header.
The Problem: Traefik uses HostSNI() rules to match connections. SSH/RDP clients don't send SNI, so Traefik can't route them.
Solution: Use a local proxy that listens on a local port, adds SNI headers, and forwards to Traefik's TCP entrypoint.
| Tool | Platform | Command Example |
|---|---|---|
| socat | Linux/macOS | socat TCP-LISTEN:2222,fork,reuseaddr OPENSSL:traefik-host:4433,verify=0,snihost=server1.wan.example.com |
| stunnel | Linux/macOS/Windows | Configure stunnel.conf with sni = server1.wan.example.com |
| HAProxy | Linux | Configure haproxy.cfg with sni str(server1.wan.example.com) |
| socat (WSL) | Windows | Same as Linux socat command |
- Add ACL entry:
_tcp_ssh_22._tcp.server1.wan.example.com - Start proxy:
socat TCP-LISTEN:2222,fork,reuseaddr OPENSSL:traefik-host:4433,verify=0,snihost=server1.wan.example.com - Connect:
ssh -p 2222 user@localhost
Note: Replace traefik-host with your Traefik hostname/IP and use custom TCP port if configured via TCP_PORT.
| Security Consideration | Recommendation |
|---|---|
| API Dashboard | The API dashboard is exposed on port 8080 in insecure mode by default. For production, configure authentication or disable the dashboard |
| SSE Endpoint | The SSE configuration endpoint (TRAEFIK_CONFIG_API_ENDPOINT) from cloudflare-tailscale-dns should be properly secured (HTTPS, authentication if needed) |
| Reconnection | The generate-config binary automatically reconnects if the SSE connection is lost, with configurable retry delays |
MIT