cloudflared internals — build from source, ingress patterns, debugging

Build cloudflared từ Go source, ingress.yaml advanced patterns (path routing, HTTP/2, origin cert), tunnel info JSON cho monitoring, top 5 debug technique.

· 6 phút đọc

TL;DR

  • cloudflared là Go binary ~50MB, build từ source mất ~90 giây với make cloudflared — không có magic, chỉ là Go binary có vendored dependencies.
  • ingress.yaml không phải nginx — nó là ordered match list, rule đầu tiên match wins. Catch-all service: http_status:404 bắt buộc ở cuối, không có sẽ panic.
  • Path-based routing trong ingress là regex, không phải glob — path: "^/api/v1/.*" không phải /api/v1/*. Bug số một tôi từng debug.
  • originRequest block là nơi 90% performance issue được fix — keepAliveConnections, keepAliveTimeout, http2Origin, noTLSVerify, tlsTimeout. Default conservative, production thường cần tune.
  • cloudflared tunnel info <name> --output=json cho metric raw — number of active connections, latency per edge POP, last handshake. Build dashboard từ output này, không phải Cloudflare API.
  • Top 5 debug technique: --loglevel debug, --metrics 127.0.0.1:9090 (Prometheus), --protocol auto vs http2 vs quic, curl --resolve bypass DNS, tcpdump trên loopback của origin để xác nhận traffic arrival.
  • cloudflared không phải reverse proxy bình thường — nó tạo 4 QUIC connection outbound tới 4 edge POP khác nhau, request đến từ bất kỳ POP nào. Mental model “single connection” là sai.

Bài này dành cho ai

Tôi đã viết một Tunnel deep-dive ở góc nhìn architecture trước đó. Bài này là góc nhìn engineering — bạn đã chạy cloudflared tunnel run rồi, giờ cần đi sâu hơn: build từ source, ingress phức tạp, debug khi nó break, monitor production. Nếu bạn chỉ cần “tunnel hello world”, quay lại bài trước.

Build cloudflared từ source — vì sao đáng làm

Cloudflare ship pre-built binary cho mọi platform. 95% người dùng không cần build. Nhưng tôi build từ source vì 3 lý do:

  1. Supply chain audit — biết chính xác code nào đang chạy. Cloudflare ký binary nhưng không phải mọi org chấp nhận trust-by-signature.
  2. Custom patch — đôi khi cần tweak (timeout default, logging format, custom metric). Fork → patch → build.
  3. CI reproducibility — embed cloudflared trong container image, build trong CI pipeline để có byte-stable layer.
# Yêu cầu: Go 1.22+ (check go.mod để chắc)
git clone https://github.com/cloudflare/cloudflared
cd cloudflared

# Build cho host platform
make cloudflared

# Output: ./cloudflared (binary ~50MB)
./cloudflared --version
# cloudflared version 2025.1.0 (built 2025-01-15-...)

# Cross-compile cho linux/arm64 (Raspberry Pi, ARM EC2)
GOOS=linux GOARCH=arm64 go build -o cloudflared-arm64 \
  -ldflags "-X main.Version=custom-$(git rev-parse --short HEAD)" \
  ./cmd/cloudflared

# Static binary cho Alpine/distroless container
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
  -ldflags "-s -w -extldflags '-static'" \
  -o cloudflared-static ./cmd/cloudflared

-s -w strip debug info — binary giảm từ 50MB xuống ~35MB. -extldflags '-static' cho static binary, chạy được trong scratch container:

FROM golang:1.22-alpine AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -ldflags "-s -w" \
    -o /cloudflared ./cmd/cloudflared

FROM scratch
COPY --from=build /cloudflared /cloudflared
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
ENTRYPOINT ["/cloudflared", "tunnel", "run"]

Container image ~40MB tổng, không có shell, không có package manager — attack surface tối thiểu.

Module structure — biết để nhìn vào source khi cần

cmd/cloudflared/         # main entry, CLI parsing
connection/              # QUIC + HTTP/2 protocol handling
ingress/                 # Rule matching, origin proxy
tunnelrpc/               # RPC protocol giữa cloudflared và edge
proxy/                   # HTTP proxy logic, header rewriting
edgediscovery/           # Discover edge POP, region selection
diagnostic/              # `cloudflared tunnel diag` collector
quic/                    # QUIC connection management
management/              # Management API client (tunnel info, etc.)

Khi debug, file đầu tiên tôi mở là ingress/rule.go — logic match request → service. Nếu hành vi không như expect, đọc 200 dòng code này thường rõ.

Ingress.yaml — beyond hello world

90% tutorial dừng ở:

tunnel: my-tunnel
credentials-file: /etc/cloudflared/cred.json
ingress:
  - hostname: app.example.com
    service: http://localhost:8080
  - service: http_status:404

Production cần phức tạp hơn. Đây là config tôi dùng cho một deployment thật:

tunnel: prod-cluster-1
credentials-file: /etc/cloudflared/prod-cluster-1.json

# Global defaults — apply cho mọi ingress trừ khi override
originRequest:
  connectTimeout: 10s
  tlsTimeout: 10s
  tcpKeepAlive: 30s
  keepAliveConnections: 100
  keepAliveTimeout: 90s
  httpHostHeader: ""              # Pass original, don't rewrite
  disableChunkedEncoding: false
  http2Origin: false              # Bật từng route nếu origin support
  proxyConnectTimeout: 30s
  noHappyEyeballs: false

ingress:
  # 1. Healthcheck endpoint — public, no auth
  - hostname: app.example.com
    path: "^/health$"
    service: http://localhost:8080
    originRequest:
      connectTimeout: 2s          # Healthcheck phải nhanh
      noTLSVerify: false

  # 2. API path — gRPC over HTTP/2
  - hostname: app.example.com
    path: "^/api/grpc/.*"
    service: h2c://localhost:50051   # cleartext HTTP/2 cho gRPC
    originRequest:
      http2Origin: true

  # 3. Admin path — kèm originRequest với client cert
  - hostname: app.example.com
    path: "^/admin/.*"
    service: https://localhost:8443
    originRequest:
      originServerName: admin.internal
      caPool: /etc/cloudflared/internal-ca.pem
      noTLSVerify: false
      clientCertificate: /etc/cloudflared/client.pem
      clientCertificateKey: /etc/cloudflared/client-key.pem

  # 4. WebSocket route
  - hostname: ws.example.com
    service: http://localhost:9000
    originRequest:
      keepAliveConnections: 50
      keepAliveTimeout: 300s      # WS connection sống lâu

  # 5. SSH bastion qua tunnel
  - hostname: ssh.example.com
    service: ssh://localhost:22

  # 6. RDP
  - hostname: rdp.example.com
    service: rdp://localhost:3389

  # 7. Catch-all — BẮT BUỘC
  - service: http_status:404

Vài điều quan trọng từ config trên:

Path là regex, không phải glob. path: "^/api/v1/.*" đúng; path: "/api/v1/*" không match gì cả vì * không có ý nghĩa trong regex Go. Đọc code ingress/rule.go — nó dùng regexp.MatchString.

Rules là ordered match. Rule 1 (^/health$) match trước rule 7 (catch-all). Đặt rule cụ thể trước, generic sau. Nếu rule 1 viết path: ".*" đặt trên cùng, mọi request rơi vào đó và rule 2-6 không bao giờ chạy.

h2c:// cho gRPC cleartext. gRPC native dùng HTTP/2 over TLS. Nhưng nếu origin là internal Kubernetes pod không có TLS, dùng h2c:// schema. Đây là chỗ đa số setup gRPC qua tunnel fail vì dùng http:// (HTTP/1.1).

originServerName — khi service: https://localhost:8443 nhưng cert origin issued cho admin.internal, không phải localhost. SNI mismatch → TLS handshake fail. originServerName ép SNI value.

caPool — cho mTLS hoặc khi origin dùng private CA. Đường dẫn tới CA cert bundle, cloudflared verify origin cert chain bằng pool này thay vì system trust store.

originRequest — nơi performance được fix

Default của cloudflared conservative. Một số case tôi đã encounter:

Connection pool exhaustion. Origin là Spring Boot app, default keepAliveConnections: 100. Khi traffic burst, cloudflared open thêm connection thay vì reuse → origin thrash với TIME_WAIT socket. Tăng lên 500 + keepAliveTimeout: 300s fix.

TLS handshake slow tới origin. tlsTimeout: 10s default, nhưng origin internal cert chain dài (root → int1 → int2 → leaf) trên link xa, handshake mất 4s. Fix: tlsTimeout: 30s và ensure caPool chứa toàn bộ chain.

HTTP/2 multiplexing cho REST API. Default http2Origin: false → mỗi request một TCP connection. Origin Go HTTP server hỗ trợ HTTP/2 native, bật http2Origin: true giảm RTT 30%.

Happy Eyeballs ngược lại expectation. cloudflared bật noHappyEyeballs: false (default) — nó cố IPv6 trước IPv4. Nếu origin IPv4-only nhưng có AAAA record (DNS misconfig), connect fail rồi fallback IPv4 → +2s mỗi request. Set noHappyEyeballs: true nếu chắc chắn dùng một stack.

cloudflared tunnel info — JSON output cho monitoring

Đa số người dùng quên flag này:

cloudflared tunnel info prod-cluster-1 --output=json

Output:

{
  "id": "8b3c7e4f-...",
  "name": "prod-cluster-1",
  "createdAt": "2024-08-12T...",
  "connections": [
    {
      "id": "conn-1",
      "type": "quic",
      "openedAt": "2025-01-15T08:23:00Z",
      "originIP": "10.0.1.42",
      "edgeLocation": "SIN",
      "uptimeSeconds": 3600
    },
    {
      "id": "conn-2",
      "type": "quic",
      "edgeLocation": "HKG",
      "uptimeSeconds": 3580
    },
    {
      "id": "conn-3",
      "type": "quic",
      "edgeLocation": "NRT",
      "uptimeSeconds": 3550
    },
    {
      "id": "conn-4",
      "type": "quic",
      "edgeLocation": "ICN",
      "uptimeSeconds": 3590
    }
  ]
}

Bốn connection tới 4 POP khác nhau — đó là invariant của Tunnel architecture, không phải 1 connection. Nếu thấy < 4 trong output, tunnel đang degraded — POP có thể chưa connect lại sau network blip.

Script monitor đơn giản:

#!/bin/bash
# /usr/local/bin/cloudflared-health.sh
set -euo pipefail

INFO=$(cloudflared tunnel info "$1" --output=json)
CONN_COUNT=$(echo "$INFO" | jq '.connections | length')

if [ "$CONN_COUNT" -lt 4 ]; then
  echo "WARN: Only $CONN_COUNT/4 connections" >&2
  exit 1
fi

# Check latest handshake — nếu > 60s là stuck
NOW=$(date +%s)
echo "$INFO" | jq -r '.connections[] | .uptimeSeconds' | while read uptime; do
  if [ "$uptime" -lt 30 ]; then
    echo "WARN: Connection just reconnected — investigate"
  fi
done

Cron mỗi 60s, alert vào Slack/PagerDuty.

Metrics endpoint — Prometheus native

--metrics 127.0.0.1:9090 expose Prometheus metric. Bind localhost only, không expose public:

cloudflared tunnel \
  --config /etc/cloudflared/config.yml \
  --metrics 127.0.0.1:9090 \
  --loglevel info \
  run prod-cluster-1

Metric quan trọng (curl 127.0.0.1:9090/metrics):

# Request throughput per status
cloudflared_tunnel_request_count{status="200"} 142351
cloudflared_tunnel_request_count{status="502"} 12

# Connection metric
cloudflared_tunnel_active_connections 4
cloudflared_tunnel_total_connections 142
cloudflared_tunnel_connection_errors_total{type="quic"} 3

# Origin latency p99
cloudflared_proxy_response_by_code{code="200"} 142339
cloudflared_proxy_origin_request_duration_seconds_bucket{...}

# HTTP/2 vs QUIC negotiation
cloudflared_tunnel_concurrent_requests_per_tunnel 47

Alert rule baseline:

groups:
  - name: cloudflared
    rules:
      - alert: TunnelConnectionsBelow4
        expr: cloudflared_tunnel_active_connections < 4
        for: 5m
        labels: { severity: warning }

      - alert: TunnelOriginErrors
        expr: rate(cloudflared_proxy_response_by_code{code=~"5.."}[5m]) > 0.05
        for: 2m
        labels: { severity: critical }

      - alert: TunnelHighLatency
        expr: histogram_quantile(0.99,
              rate(cloudflared_proxy_origin_request_duration_seconds_bucket[5m])) > 2
        for: 5m
        labels: { severity: warning }

Top 5 debug technique

Production debugging của cloudflared, theo thứ tự tôi reach cho:

1. --loglevel debug + filter

cloudflared tunnel --loglevel debug run prod-cluster-1 2>&1 \
  | grep -E "ingress|origin|status code"

Log debug rất verbose — không grep sẽ chết drown. Pattern hữu ích:

  • ingress match → cho biết rule nào được match cho request
  • origin TLS handshake → debug TLS issue
  • connect: connection refused → origin không listen

2. --protocol auto vs http2 vs quic

cloudflared mặc định --protocol auto → cố QUIC trước, fallback HTTP/2. Nếu network drop UDP/443, QUIC fail âm thầm rồi mới fallback — tăng latency reconnect. Khi debug:

# Force HTTP/2 — easier để debug
cloudflared tunnel --protocol http2 run prod-cluster-1

# Force QUIC — confirm QUIC working
cloudflared tunnel --protocol quic run prod-cluster-1

Nếu HTTP/2 work nhưng QUIC fail → firewall block UDP/443 outbound. Nếu cả hai fail → credentials hoặc DNS issue.

3. curl --resolve bypass DNS

Khi nghi DNS issue:

# Test trực tiếp tới tunnel CNAME target
curl -v --resolve app.example.com:443:104.21.0.1 \
  https://app.example.com/health

# Hoặc test connectivity từ inside container nơi cloudflared chạy
docker exec -it cloudflared-container \
  wget -qO- http://localhost:8080/health

90% case “tunnel không hoạt động” thực ra là origin không reachable từ container chạy cloudflared.

4. tcpdump trên loopback origin

# Trên VM/container của origin, capture loopback
sudo tcpdump -i lo -nn -A 'tcp port 8080'

# Trigger request từ public URL trong tab khác
# Nếu thấy packet → cloudflared deliver tới origin OK
# Nếu không thấy → ingress.yaml rule không match hoặc service URL sai

Đây là smoking gun test — nếu tcpdump không thấy packet đến port origin, vấn đề ở cloudflared/ingress. Nếu thấy nhưng response 5xx, vấn đề ở origin app.

5. cloudflared tunnel diag — automated bundle

cloudflared tunnel diag --tunnel-id <id> > diag.txt

Generate diagnostic bundle: tunnel state, system info, recent logs, connection list. Khi mở support ticket với Cloudflare, attach file này — save vài round-trip.

Anti-pattern tôi từng thấy

1. Tunnel per environment trong cùng VM. Một developer run cloudflared cho dev, một cho staging, cùng VM. 8 QUIC connection outbound cùng lúc. Networking team cấm UDP burst. Fix: một tunnel với nhiều ingress hostname, route theo hostname trong ingress.yaml.

2. Ingress catch-all bằng service: http://localhost:8080. Mọi traffic không match rule rơi vào port 8080, bao gồm random scan từ internet (qua public hostname). App nhận traffic không expect, log scary. Fix: catch-all phải là service: http_status:404.

3. cloudflared chạy với root. Không cần root cho user-space TUN; tunnel daemon chỉ cần network egress. Tạo user cloudflared không shell, run service dưới user đó. Compromise binary không gây leo thang.

4. Quên originRequest.caPool cho private cert. Origin có cert ký bởi internal CA, cloudflared throw x509: certificate signed by unknown authority. Hoặc bật noTLSVerify: true (xấu, MITM possible trong VPC) hoặc thêm caPool đường dẫn CA bundle.

Bottom line

cloudflared không phải black box — nó là Go binary với codebase open-source readable. Build từ source mất 90 giây, giúp bạn audit và customize. 90% production issue là ingress.yaml regex sai hoặc originRequest chưa tune. Monitor qua --metrics Prometheus endpoint và tunnel info --output=json, không phải qua dashboard. Khi break, debug technique theo thứ tự: loglevel → protocol force → curl resolve → tcpdump → diag bundle. Hiểu được 4 QUIC connection outbound tới 4 POP khác nhau, không phải single connection, là mental model thay đổi cách bạn debug.

Checklist production cloudflared

  • Build từ source trong CI, embed vào container image
  • Static binary trong scratch container, attack surface tối thiểu
  • Chạy với non-root user
  • ingress.yaml có catch-all http_status:404 cuối
  • Path là regex (^...$), không phải glob
  • originRequest.keepAliveConnections ≥ 200 cho production
  • --metrics 127.0.0.1:9090 cho Prometheus scrape
  • Alert active_connections < 4
  • Alert 5xx rate > 5%
  • Backup credentials.json an toàn (KMS/Vault)
  • --loglevel info mặc định; debug chỉ khi cần
  • Document escalation cho lost credentials
  • HA: ≥ 2 cloudflared instance cùng tunnel ID, khác VM/region

Tham chiếu