TUN-10391: Add precheck integration tests

Adding integration tests for cloudflared pre-checks. This tests pre-check functionality to ensure it is working as expected.
This commit is contained in:
Miguel da Costa Martins Marcelino
2026-05-22 21:58:48 +00:00
parent 905d983d14
commit 4494eee13d
5 changed files with 582 additions and 86 deletions
+499
View File
@@ -0,0 +1,499 @@
#!/usr/bin/env python
"""
Integration tests for cloudflared connectivity pre-checks (TUN-10391).
Scope
-----
These tests verify the end-to-end behavior of cloudflared pre-checks:
- that the human-readable table written to stdout has the correct structure
and content,
- that structured JSON log lines are emitted with the expected fields, and
- that running the `diag` subcommand against a live tunnel instance produces a
zip archive that contains prechecks.json.
They do NOT cover every failure mode of the precheck logic — those are owned
by the unit tests in prechecks/checker_test.go which use mock dialers.
At the integration level the only reliable way to induce specific failure modes
without real firewall intervention is:
- --edge <unreachable>: StaticEdgeDNSResolver resolves the literal IP
directly (DNS row = PASS), then both QUIC and HTTP/2 probes time out
-> hard fail (both transports blocked).
This does NOT exercise the DNS-failure -> transport-skip path.
DNS failure and Management API failure cannot be triggered via CLI flags alone;
they require network-level intervention outside the component-test harness.
stdout design
-------------
fmt.Println(report.String()) runs inside a goroutine that is started
concurrently with the tunnel. We poll a --logfile for the "precheck complete"
sentinel before leaving the `with` block, ensuring the goroutine has finished.
We then call cfd.terminate(). After the `with` block exits, the process is
dead and all output has been captured by CloudflaredProcess's background reader
thread (stderr is merged into stdout). We read the accumulated lines from
cfd.stdout_lines.
"""
import json
import os
import re
import subprocess
import time
import zipfile as zipfilemod
from constants import METRICS_PORT
from util import LOGGER, start_cloudflared, wait_tunnel_ready
# stdout table constants
TABLE_WIDTH = 80
HEADER_LINE = "--- CONNECTIVITY PRE-CHECKS " + "-" * (TABLE_WIDTH - len("--- CONNECTIVITY PRE-CHECKS ") - 1)
COL_HEADER = "COMPONENT" # first token of the column-header row
SEPARATOR = "-" * TABLE_WIDTH
# Component names (probes.go: componentXxx)
COMP_DNS = "DNS Resolution"
COMP_QUIC = "UDP Connectivity"
COMP_H2 = "TCP Connectivity"
COMP_API = "Cloudflare API"
# Target labels used in the rendered table.
#
# probeRegion() (checker.go:216) always overwrites the Target field of
# whatever CheckResult the inner probe function returns with the regionTarget
# hostname, so QUIC and HTTP/2 rows carry the same region hostname as the
# corresponding DNS row — not the "Port 7844 (QUIC/HTTP2)" strings that
# targetPortQUIC/targetPortHTTP2 define. Those port-label constants are only
# used in the empty-addrs SKIP branch and inside action message strings.
TARGET_API = "api.cloudflare.com:443"
TARGET_REGION1 = "region1.v2.argotunnel.com"
TARGET_REGION2 = "region2.v2.argotunnel.com"
# Details strings (probes.go: detailsXxx)
DETAILS_DNS_RESOLVED = "DNS Resolved successfully"
DETAILS_QUIC_OK = "QUIC connection successful"
DETAILS_HTTP2_OK = "HTTP/2 connection successful"
DETAILS_API_OK = "API is reachable"
DETAILS_QUIC_FAIL = "QUIC connection failed"
DETAILS_HTTP2_FAIL = "HTTP/2 connection is blocked or unreachable"
# Status labels (result.go: xyzStatus)
PASS = "PASS"
FAIL = "FAIL"
SKIP = "SKIP"
# Action prefixes (result.go: renderActions)
PREFIX_ERROR = "ERROR: "
PREFIX_WARNING = "WARNING: "
# Action messages (probes.go: actionXxx)
ACTION_QUIC_BLOCKED = "Allow outbound QUIC traffic on port 7844 or use HTTP2."
ACTION_HTTP2_BLOCKED = "Allow outbound TCP on port 7844."
# Exact summary lines (result.go: summaryLine)
SUMMARY_HEALTHY = "SUMMARY: Environment is healthy. cloudflared will use 'quic' as primary protocol."
SUMMARY_CRITICAL = "SUMMARY: Environment has critical failures. cloudflared may not be able to establish a tunnel."
# structured log constants (result.go)
LOG_MSG_PRECHECK = "precheck"
LOG_MSG_PRECHECK_COMPLETE = "precheck complete"
STATUS_PASS_LOG = "pass"
UNREACHABLE_EDGE = "192.0.2.1:7844"
# cloudflared dial timeout per probe: 5 s, up to 2 retries -> ~15 s total.
PRECHECK_POLL_TIMEOUT_SECS = 15
PRECHECK_POLL_INTERVAL_SECS = 1
# ---------- helpers ----------
def _poll_log_file_for_precheck_complete(log_file: str, timeout: float) -> list[dict]:
"""
Poll a JSON log file until a 'precheck complete' line appears or timeout
expires. Returns all precheck-related log lines found.
cloudflared's --logfile writes one JSON object per line. Polling keeps
the test fast on healthy networks and still tolerates slow CI hosts.
We re-read from the beginning of the file on every poll because the file
is append-only, small, and tracking a byte offset would add complexity with
no meaningful performance benefit for a ~15 s total window.
"""
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
lines = _read_precheck_log_lines_from_file(log_file)
if any(l.get("message") == LOG_MSG_PRECHECK_COMPLETE for l in lines):
return lines
time.sleep(PRECHECK_POLL_INTERVAL_SECS)
return _read_precheck_log_lines_from_file(log_file)
def _read_precheck_log_lines_from_file(log_file: str) -> list[dict]:
"""Parse all precheck-related JSON log lines from a --logfile path."""
result = []
try:
with open(log_file, "r") as f:
for raw_line in f:
raw_line = raw_line.strip()
if not raw_line:
continue
try:
obj = json.loads(raw_line)
except json.JSONDecodeError:
continue
msg = obj.get("message") or obj.get("msg", "")
if msg in (LOG_MSG_PRECHECK, LOG_MSG_PRECHECK_COMPLETE):
result.append(obj)
except FileNotFoundError:
pass
return result
# stdout table parse
class TableRow:
"""One data row parsed from the rendered precheck table."""
def __init__(self, component: str, target: str, status: str, details: str):
self.component = component
self.target = target
self.status = status
self.details = details
def __repr__(self):
return f"TableRow({self.component!r}, {self.target!r}, {self.status!r}, {self.details!r})"
def _parse_table(stdout: str) -> list[TableRow]:
"""
Parse the data rows from a precheck table in stdout.
text/tabwriter uses padding=2, so columns are separated by two or more
spaces. We skip the column-header row and stop at blank lines, SUMMARY,
separator, ERROR, or WARNING lines.
"""
rows = []
in_data = False
for line in stdout.splitlines():
if line.startswith("COMPONENT"):
in_data = True
continue
if not in_data:
continue
if (line == "" or line.startswith("SUMMARY") or line.startswith("---")
or line.startswith("ERROR") or line.startswith("WARNING")):
in_data = False
continue
parts = re.split(r" +", line.rstrip())
if len(parts) >= 3:
rows.append(TableRow(
component=parts[0],
target=parts[1],
status=parts[2],
details=parts[3] if len(parts) >= 4 else "",
))
return rows
def _rows_for(rows: list[TableRow], component: str) -> list[TableRow]:
return [r for r in rows if r.component == component]
# log assertions
def _assert_precheck_summary_log(
log_lines: list[dict],
*,
hard_fail: bool,
suggested_protocol: str | None = None,
):
"""Assert the 'precheck complete' summary log line has the expected fields."""
summary_lines = [l for l in log_lines if l.get("message") == LOG_MSG_PRECHECK_COMPLETE]
assert len(summary_lines) == 1, \
f"Expected exactly one '{LOG_MSG_PRECHECK_COMPLETE}' log line; got {summary_lines}"
summary = summary_lines[0]
assert summary.get("hard_fail") is hard_fail, \
f"Expected hard_fail={hard_fail} in summary log: {summary}"
if suggested_protocol is not None:
assert summary.get("suggested_protocol") == suggested_protocol, \
(f"Expected suggested_protocol={suggested_protocol!r}; "
f"got {summary.get('suggested_protocol')!r}")
# ---------- Tests ----------
class TestPrechecksHappyPath:
"""
On a healthy connection all probes pass. We assert:
- the full table structure (header, column header, separator)
- every row's component, target, status, and details
- no ERROR/WARNING action lines
- the exact summary line
- the structured log summary (hard_fail=false, suggested_protocol=quic)
"""
def test_prechecks_pass_on_healthy_connection(self, tmp_path, component_tests_config):
log_file = str(tmp_path / "cloudflared.log")
config = component_tests_config({"logfile": log_file})
with start_cloudflared(
tmp_path,
config,
cfd_pre_args=["tunnel", "--ha-connections", "1"],
cfd_args=["run"],
new_process=True,
capture_output=True,
) as cfd:
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
# Poll the log file for the sentinel before signalling the process.
log_lines = _poll_log_file_for_precheck_complete(
log_file, timeout=PRECHECK_POLL_TIMEOUT_SECS
)
# Signal shutdown.
cfd.terminate()
# The process is now dead. All output was captured by the background
# reader thread into cfd.stdout_lines (stderr is merged into stdout).
stdout = b"".join(cfd.stdout_lines).decode(errors="replace")
LOGGER.debug(f"[happy-path] stdout:\n{stdout}")
LOGGER.debug(f"[happy-path] log_lines:\n{log_lines}")
# ── table structure ──────────────────────────────────────────────────
# stderr is merged into stdout so log lines precede the table.
assert HEADER_LINE in stdout, \
f"Expected header line in output;\ngot:\n{stdout}"
assert COL_HEADER in stdout, \
f"Expected column header row in output;\ngot:\n{stdout}"
assert SEPARATOR in stdout, \
f"Expected closing separator in output;\ngot:\n{stdout}"
# ── row content ──────────────────────────────────────────────────────
rows = _parse_table(stdout)
assert len(rows) == 7, \
f"Expected 7 rows (2 DNS + 2 QUIC + 2 HTTP/2 + 1 API); got {len(rows)}: {rows}"
dns_rows = _rows_for(rows, COMP_DNS)
assert len(dns_rows) == 2, f"Expected 2 DNS rows; got {dns_rows}"
assert dns_rows[0].target == TARGET_REGION1
assert dns_rows[1].target == TARGET_REGION2
for r in dns_rows:
assert r.status == PASS, f"DNS row not PASS: {r}"
assert r.details == DETAILS_DNS_RESOLVED, f"DNS row details wrong: {r}"
quic_rows = _rows_for(rows, COMP_QUIC)
assert len(quic_rows) == 2, f"Expected 2 QUIC rows; got {quic_rows}"
assert quic_rows[0].target == TARGET_REGION1, f"QUIC row[0] target wrong: {quic_rows[0]}"
assert quic_rows[1].target == TARGET_REGION2, f"QUIC row[1] target wrong: {quic_rows[1]}"
for r in quic_rows:
assert r.status == PASS, f"QUIC row not PASS: {r}"
assert r.details == DETAILS_QUIC_OK, f"QUIC row details wrong: {r}"
h2_rows = _rows_for(rows, COMP_H2)
assert len(h2_rows) == 2, f"Expected 2 HTTP/2 rows; got {h2_rows}"
assert h2_rows[0].target == TARGET_REGION1, f"HTTP/2 row[0] target wrong: {h2_rows[0]}"
assert h2_rows[1].target == TARGET_REGION2, f"HTTP/2 row[1] target wrong: {h2_rows[1]}"
for r in h2_rows:
assert r.status == PASS, f"HTTP/2 row not PASS: {r}"
assert r.details == DETAILS_HTTP2_OK, f"HTTP/2 row details wrong: {r}"
api_rows = _rows_for(rows, COMP_API)
assert len(api_rows) == 1, f"Expected 1 API row; got {api_rows}"
assert api_rows[0].target == TARGET_API, f"API row target wrong: {api_rows[0]}"
assert api_rows[0].status == PASS, f"API row not PASS: {api_rows[0]}"
assert api_rows[0].details == DETAILS_API_OK, f"API row details wrong: {api_rows[0]}"
# ── no action lines ──────────────────────────────────────────────────
assert PREFIX_ERROR not in stdout, f"Unexpected ERROR action:\n{stdout}"
assert PREFIX_WARNING not in stdout, f"Unexpected WARNING action:\n{stdout}"
# ── exact summary line ───────────────────────────────────────────────
assert SUMMARY_HEALTHY in stdout, \
f"Expected healthy summary;\ngot:\n{stdout}"
# ── structured log ───────────────────────────────────────────────────
assert len(log_lines) > 0, \
"Expected at least one structured precheck log line in log file"
for line in log_lines:
if line.get("message") == LOG_MSG_PRECHECK:
assert line.get("status") == STATUS_PASS_LOG, \
f"Expected status=pass in precheck log line: {line}"
_assert_precheck_summary_log(log_lines, hard_fail=False, suggested_protocol="quic")
class TestPrechecksHardFail:
"""
When --edge points at an unreachable IP, StaticEdgeDNSResolver resolves
the literal address directly (DNS row = PASS), but both transport probes
time out -> hard fail. We assert:
- the full table structure
- DNS row: PASS (the literal IP was resolved)
- QUIC row: FAIL with correct details + ERROR action
- HTTP/2 row: FAIL with correct details + ERROR action
- API row: PASS (api.cloudflare.com:443 is independently reachable)
- the exact critical summary line
- the structured log summary (hard_fail=true)
This test does NOT call wait_tunnel_ready because the tunnel will not
connect to the unreachable address.
"""
def test_prechecks_hard_fail_when_edge_unreachable(self, tmp_path, component_tests_config):
log_file = str(tmp_path / "cloudflared.log")
config = component_tests_config({"logfile": log_file})
with start_cloudflared(
tmp_path,
config,
cfd_pre_args=[
"tunnel",
"--ha-connections", "1",
"--edge", UNREACHABLE_EDGE,
],
cfd_args=["run"],
new_process=True,
capture_output=True,
) as cfd:
log_lines = _poll_log_file_for_precheck_complete(
log_file, timeout=PRECHECK_POLL_TIMEOUT_SECS
)
cfd.terminate()
stdout = b"".join(cfd.stdout_lines).decode(errors="replace")
LOGGER.debug(f"[hard-fail] stdout:\n{stdout}")
LOGGER.debug(f"[hard-fail] log_lines:\n{log_lines}")
# ── table structure ──────────────────────────────────────────────────
# stderr is merged into stdout so log lines precede the table.
assert HEADER_LINE in stdout, \
f"Expected header line in output;\ngot:\n{stdout}"
assert COL_HEADER in stdout, \
f"Expected column header row in output;\ngot:\n{stdout}"
assert SEPARATOR in stdout, \
f"Expected closing separator in output;\ngot:\n{stdout}"
# ── row content ──────────────────────────────────────────────────────
rows = _parse_table(stdout)
assert len(rows) == 4, \
f"Expected 4 rows (1 DNS + 1 QUIC + 1 HTTP/2 + 1 API); got {len(rows)}: {rows}"
dns_rows = _rows_for(rows, COMP_DNS)
assert len(dns_rows) == 1, f"Expected 1 DNS row; got {dns_rows}"
assert dns_rows[0].target == UNREACHABLE_EDGE
assert dns_rows[0].status == PASS, f"DNS row not PASS: {dns_rows[0]}"
assert dns_rows[0].details == DETAILS_DNS_RESOLVED, f"DNS row details wrong: {dns_rows[0]}"
quic_rows = _rows_for(rows, COMP_QUIC)
assert len(quic_rows) == 1, f"Expected 1 QUIC row; got {quic_rows}"
assert quic_rows[0].target == UNREACHABLE_EDGE, f"QUIC row target wrong: {quic_rows[0]}"
assert quic_rows[0].status == FAIL, f"QUIC row not FAIL: {quic_rows[0]}"
assert quic_rows[0].details == DETAILS_QUIC_FAIL, f"QUIC row details wrong: {quic_rows[0]}"
h2_rows = _rows_for(rows, COMP_H2)
assert len(h2_rows) == 1, f"Expected 1 HTTP/2 row; got {h2_rows}"
assert h2_rows[0].target == UNREACHABLE_EDGE, f"HTTP/2 row target wrong: {h2_rows[0]}"
assert h2_rows[0].status == FAIL, f"HTTP/2 row not FAIL: {h2_rows[0]}"
assert h2_rows[0].details == DETAILS_HTTP2_FAIL, f"HTTP/2 row details wrong: {h2_rows[0]}"
api_rows = _rows_for(rows, COMP_API)
assert len(api_rows) == 1, f"Expected 1 API row; got {api_rows}"
assert api_rows[0].target == TARGET_API, f"API row target wrong: {api_rows[0]}"
assert api_rows[0].status == PASS, f"API row not PASS: {api_rows[0]}"
assert api_rows[0].details == DETAILS_API_OK, f"API row details wrong: {api_rows[0]}"
assert f"{PREFIX_ERROR}{ACTION_QUIC_BLOCKED}" in stdout, \
f"Expected QUIC ERROR action;\ngot:\n{stdout}"
assert f"{PREFIX_ERROR}{ACTION_HTTP2_BLOCKED}" in stdout, \
f"Expected HTTP/2 ERROR action;\ngot:\n{stdout}"
assert SUMMARY_CRITICAL in stdout, \
f"Expected critical summary;\ngot:\n{stdout}"
_assert_precheck_summary_log(log_lines, hard_fail=True, suggested_protocol=None)
class TestPreChecksDiag:
"""
Verify that `cloudflared tunnel diag` includes prechecks.json in the
diagnostic zip archive produced against a live tunnel instance.
The precheck job in diagnostic.go is gated on noDiagNetwork; we do NOT
pass --no-diag-network so prechecks.json must be present. We skip the
heavier collectors (logs, metrics, system, runtime) to keep the test fast.
The diag subcommand writes the zip to its current working directory. We
run it with cwd=tmp_path so the archive lands there and is cleaned up
automatically by pytest. We resolve config.cloudflared_binary to an
absolute path before changing cwd, because the binary path may be relative
to the original working directory.
"""
def test_diag_contains_prechecks_json(self, tmp_path, component_tests_config):
config = component_tests_config()
binary = os.path.abspath(config.cloudflared_binary)
with start_cloudflared(
tmp_path,
config,
cfd_pre_args=["tunnel", "--ha-connections", "1"],
cfd_args=["run"],
new_process=True,
capture_output=True,
) as cfd:
wait_tunnel_ready(tunnel_url=config.get_url(), require_min_connections=1)
# Run the diag subcommand as a one-shot process against the
# already-running instance. We skip log/metrics/system/runtime
# collectors; the network collector (which runs prechecks) is left
# enabled.
diag_result = subprocess.run(
[
binary,
"tunnel",
"diag",
"--metrics", f"localhost:{METRICS_PORT}",
"--no-diag-logs",
"--no-diag-metrics",
"--no-diag-system",
"--no-diag-runtime",
],
cwd=str(tmp_path),
capture_output=True,
timeout=60,
)
cfd.terminate()
diag_stdout = diag_result.stdout.decode(errors="replace")
diag_stderr = diag_result.stderr.decode(errors="replace")
LOGGER.debug(f"[diag] stdout:\n{diag_stdout}")
LOGGER.debug(f"[diag] stderr:\n{diag_stderr}")
assert diag_result.returncode == 0, (
f"cloudflared tunnel diag exited with code {diag_result.returncode}\n"
f"stdout:\n{diag_stdout}\nstderr:\n{diag_stderr}"
)
# Locate the zip file written to tmp_path by the diag command.
zip_files = list(tmp_path.glob("cloudflared-diag-*.zip"))
assert len(zip_files) == 1, \
f"Expected exactly one cloudflared-diag-*.zip in {tmp_path}; found {zip_files}"
zip_path = zip_files[0]
with zipfilemod.ZipFile(zip_path) as zf:
names = zf.namelist()
LOGGER.debug(f"[diag] zip contents: {names}")
assert "prechecks.json" in names, \
f"Expected prechecks.json in diag zip; got: {names}"
# Must be valid JSON containing at least the RunID field that
# prechecks.Run() always sets.
with zf.open("prechecks.json") as fh:
data = json.load(fh)
assert "RunID" in data, \
f"Expected RunID key in prechecks.json; got keys: {list(data.keys())}"
+22 -22
View File
@@ -25,7 +25,7 @@ const (
// Action messages for each probe outcome.
actionDNSFail = "Ensure your DNS resolver can resolve '%s'. Run: dig A %s @1.1.1.1. If that fails, contact your network administrator."
actionQUICBlocked = "QUIC traffic failed to connect to port 7844."
actionQUICBlocked = "Allow outbound QUIC traffic on port 7844 or use HTTP2."
actionHTTP2Blocked = "Allow outbound TCP on port 7844."
actionAPIUnreachable = "cloudflared will still run, but automatic software updates are unavailable. " +
"Ensure port 443 TCP to api.cloudflare.com is open if you want auto-updates."
@@ -43,16 +43,16 @@ const (
noDNSTarget = "No DNS target (Using edge flag)"
// Details messages for CheckResult.
detailsNoAddressesReturned = "No addresses returned"
detailsResolvedSuccessfully = "Resolved successfully"
detailsHandshakeFailed = "Handshake failed"
detailsHandshakeSuccessful = "Handshake successful"
detailsBlockedOrUnreachable = "Blocked or unreachable"
detailsTLSHandshakeSuccessful = "TLS handshake successful"
detailsConnectionFailed = "Connection failed"
detailsTCPPortReachable = "TCP port reachable (TLS not validated)"
detailsDNSPrerequisiteFailed = "DNS prerequisite failed"
detailsTLSConfigFailed = "TLS configuration failed"
dnsNoAddressesReturned = "No addresses returned"
dnsResolvedSuccessfully = "DNS Resolved successfully"
detailsQUICHandshakeFailed = "QUIC connection failed"
detailsQUICHandshakeSuccessful = "QUIC connection successful"
detailsHTTP2BlockedOrUnreachable = "HTTP/2 connection is blocked or unreachable"
detailsHTTP2HandshakeSuccessful = "HTTP/2 connection successful"
detailsAPIConnectionFailed = "API Connection failed"
detailsApiReachable = "API is reachable"
detailsDNSPrerequisiteFailed = "DNS prerequisite failed"
detailsTLSConfigFailed = "TLS configuration failed"
// Region hostname templates.
region1Global = "region1.v2.argotunnel.com"
@@ -140,7 +140,7 @@ func probeDNS(
addrGroups, err := resolver.Resolve(region)
if err != nil || len(addrGroups) == 0 {
detail := detailsNoAddressesReturned
detail := dnsNoAddressesReturned
if err != nil {
detail = err.Error()
}
@@ -158,12 +158,12 @@ func probeDNS(
group := addrGroups[i]
if len(group) == 0 {
resolved = append(resolved, ResolvedTarget{
DNSResult: newDNSCheckResult(target, Fail, detailsNoAddressesReturned, fmt.Sprintf(actionDNSFail, target, target)),
DNSResult: newDNSCheckResult(target, Fail, dnsNoAddressesReturned, fmt.Sprintf(actionDNSFail, target, target)),
})
} else {
resolved = append(resolved, ResolvedTarget{
Addrs: group,
DNSResult: newDNSCheckResult(target, Pass, detailsResolvedSuccessfully, ""),
DNSResult: newDNSCheckResult(target, Pass, dnsResolvedSuccessfully, ""),
})
}
}
@@ -209,7 +209,7 @@ func probeQUIC(
Component: componentUDPConnectivity,
Target: targetPortQUIC,
ProbeStatus: Fail,
Details: detailsHandshakeFailed,
Details: detailsQUICHandshakeFailed,
Action: actionQUICBlocked,
}
}
@@ -223,7 +223,7 @@ func probeQUIC(
Component: componentUDPConnectivity,
Target: targetPortQUIC,
ProbeStatus: Pass,
Details: detailsHandshakeSuccessful,
Details: detailsQUICHandshakeSuccessful,
}
}
@@ -243,7 +243,7 @@ func probeHTTP2(ctx context.Context, tlsConfig *tls.Config, dialer TCPDialer, ad
Component: componentTCPConnectivity,
Target: targetPortHTTP2,
ProbeStatus: Fail,
Details: detailsBlockedOrUnreachable,
Details: detailsHTTP2BlockedOrUnreachable,
Action: actionHTTP2Blocked,
}
}
@@ -254,7 +254,7 @@ func probeHTTP2(ctx context.Context, tlsConfig *tls.Config, dialer TCPDialer, ad
Component: componentTCPConnectivity,
Target: targetPortHTTP2,
ProbeStatus: Pass,
Details: detailsTLSHandshakeSuccessful,
Details: detailsHTTP2HandshakeSuccessful,
}
}
@@ -273,7 +273,7 @@ func probeManagementAPI(ctx context.Context, dialer ManagementDialer) CheckResul
Component: componentCloudflareAPI,
Target: targetAPI,
ProbeStatus: Fail,
Details: detailsConnectionFailed,
Details: detailsAPIConnectionFailed,
Action: actionAPIUnreachable,
}
}
@@ -284,7 +284,7 @@ func probeManagementAPI(ctx context.Context, dialer ManagementDialer) CheckResul
Component: componentCloudflareAPI,
Target: targetAPI,
ProbeStatus: Pass,
Details: detailsTCPPortReachable,
Details: detailsApiReachable,
}
}
@@ -363,11 +363,11 @@ func resolveStaticEdge(addrs []string, log *zerolog.Logger) []ResolvedTarget {
if len(resolved) > 0 {
targets = append(targets, ResolvedTarget{
Addrs: resolved,
DNSResult: newDNSCheckResult(addr, Pass, detailsResolvedSuccessfully, ""),
DNSResult: newDNSCheckResult(addr, Pass, dnsResolvedSuccessfully, ""),
})
} else {
targets = append(targets, ResolvedTarget{
DNSResult: newDNSCheckResult(addr, Fail, detailsNoAddressesReturned, fmt.Sprintf(actionDNSFail, addr, addr)),
DNSResult: newDNSCheckResult(addr, Fail, dnsNoAddressesReturned, fmt.Sprintf(actionDNSFail, addr, addr)),
})
}
}
+13 -13
View File
@@ -124,7 +124,7 @@ func TestProbeDNS_Success(t *testing.T) {
assert.Equal(t, ProbeTypeDNS, targets[0].DNSResult.Type)
assert.Equal(t, testRegion1Global, targets[0].DNSResult.Target)
assert.Equal(t, Pass, targets[0].DNSResult.ProbeStatus)
assert.Equal(t, detailsResolvedSuccessfully, targets[0].DNSResult.Details)
assert.Equal(t, dnsResolvedSuccessfully, targets[0].DNSResult.Details)
}
func TestProbeDNS_MultipleRegions(t *testing.T) {
@@ -184,7 +184,7 @@ func TestProbeDNS_EmptyResults(t *testing.T) {
require.Len(t, targets, 2)
assert.Empty(t, targets[0].Addrs)
assert.Equal(t, Fail, targets[0].DNSResult.ProbeStatus)
assert.Equal(t, detailsNoAddressesReturned, targets[0].DNSResult.Details)
assert.Equal(t, dnsNoAddressesReturned, targets[0].DNSResult.Details)
}
func TestProbeDNS_EmptyGroup(t *testing.T) {
@@ -200,7 +200,7 @@ func TestProbeDNS_EmptyGroup(t *testing.T) {
require.Len(t, targets, 1)
assert.Empty(t, targets[0].Addrs)
assert.Equal(t, Fail, targets[0].DNSResult.ProbeStatus)
assert.Equal(t, detailsNoAddressesReturned, targets[0].DNSResult.Details)
assert.Equal(t, dnsNoAddressesReturned, targets[0].DNSResult.Details)
}
func TestProbeDNS_RegionFlag(t *testing.T) {
@@ -236,7 +236,7 @@ func TestProbeQUIC_Success(t *testing.T) {
assert.Equal(t, ProbeTypeQUIC, result.Type)
assert.Equal(t, Pass, result.ProbeStatus)
assert.Equal(t, detailsHandshakeSuccessful, result.Details)
assert.Equal(t, detailsQUICHandshakeSuccessful, result.Details)
}
func TestProbeQUIC_DialError(t *testing.T) {
@@ -254,7 +254,7 @@ func TestProbeQUIC_DialError(t *testing.T) {
assert.Equal(t, ProbeTypeQUIC, result.Type)
assert.Equal(t, Fail, result.ProbeStatus)
assert.Equal(t, detailsHandshakeFailed, result.Details)
assert.Equal(t, detailsQUICHandshakeFailed, result.Details)
assert.Equal(t, actionQUICBlocked, result.Action)
}
@@ -274,7 +274,7 @@ func TestProbeQUIC_CloseErrorDoesNotAffectResult(t *testing.T) {
assert.Equal(t, ProbeTypeQUIC, result.Type)
assert.Equal(t, Pass, result.ProbeStatus)
assert.Equal(t, detailsHandshakeSuccessful, result.Details)
assert.Equal(t, detailsQUICHandshakeSuccessful, result.Details)
}
func TestProbeQUIC_ContextTimeout(t *testing.T) {
@@ -291,7 +291,7 @@ func TestProbeQUIC_ContextTimeout(t *testing.T) {
result := probeQUIC(context.Background(), testTLSConfig, dialer, addr, &logger)
assert.Equal(t, Fail, result.ProbeStatus)
assert.Equal(t, detailsHandshakeFailed, result.Details)
assert.Equal(t, detailsQUICHandshakeFailed, result.Details)
}
// probeHTTP2 tests.
@@ -310,7 +310,7 @@ func TestProbeHTTP2_Success(t *testing.T) {
assert.Equal(t, ProbeTypeHTTP2, result.Type)
assert.Equal(t, Pass, result.ProbeStatus)
assert.Equal(t, detailsTLSHandshakeSuccessful, result.Details)
assert.Equal(t, detailsHTTP2HandshakeSuccessful, result.Details)
}
func TestProbeHTTP2_DialError(t *testing.T) {
@@ -327,7 +327,7 @@ func TestProbeHTTP2_DialError(t *testing.T) {
assert.Equal(t, ProbeTypeHTTP2, result.Type)
assert.Equal(t, Fail, result.ProbeStatus)
assert.Equal(t, detailsBlockedOrUnreachable, result.Details)
assert.Equal(t, detailsHTTP2BlockedOrUnreachable, result.Details)
assert.Equal(t, actionHTTP2Blocked, result.Action)
}
@@ -347,7 +347,7 @@ func TestProbeManagementAPI_Success(t *testing.T) {
assert.Equal(t, "Cloudflare API", result.Component)
assert.Equal(t, "api.cloudflare.com:443", result.Target)
assert.Equal(t, Pass, result.ProbeStatus)
assert.Equal(t, detailsTCPPortReachable, result.Details)
assert.Equal(t, detailsApiReachable, result.Details)
}
func TestProbeManagementAPI_DialError(t *testing.T) {
@@ -362,7 +362,7 @@ func TestProbeManagementAPI_DialError(t *testing.T) {
assert.Equal(t, ProbeTypeManagementAPI, result.Type)
assert.Equal(t, Fail, result.ProbeStatus)
assert.Equal(t, detailsConnectionFailed, result.Details)
assert.Equal(t, detailsAPIConnectionFailed, result.Details)
assert.Equal(t, actionAPIUnreachable, result.Action)
}
@@ -516,7 +516,7 @@ func TestProbeQUIC_IPv6Address(t *testing.T) {
result := probeQUIC(context.Background(), testTLSConfig, dialer, addr, &logger)
assert.Equal(t, Pass, result.ProbeStatus)
assert.Equal(t, detailsHandshakeSuccessful, result.Details)
assert.Equal(t, detailsQUICHandshakeSuccessful, result.Details)
}
// IPv6 address tests for probeHTTP2.
@@ -571,7 +571,7 @@ func TestResolveStaticEdge_InvalidAddr(t *testing.T) {
require.Len(t, targets, 1)
assert.Equal(t, "not-a-valid-addr", targets[0].DNSResult.Target)
assert.Equal(t, Fail, targets[0].DNSResult.ProbeStatus)
assert.Equal(t, detailsNoAddressesReturned, targets[0].DNSResult.Details)
assert.Equal(t, dnsNoAddressesReturned, targets[0].DNSResult.Details)
assert.Empty(t, targets[0].Addrs)
}
+48 -48
View File
@@ -25,11 +25,11 @@ func allPassReport() Report {
RunID: fixedRunID,
SuggestedProtocol: new(connection.QUIC),
Results: []CheckResult{
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region1.v2.argotunnel.com", ProbeStatus: Pass, Details: "Resolved successfully"},
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region2.v2.argotunnel.com", ProbeStatus: Pass, Details: "Resolved successfully"},
{Type: ProbeTypeQUIC, Component: "UDP Connectivity", Target: "Port 7844 (QUIC)", ProbeStatus: Pass, Details: "Handshake successful"},
{Type: ProbeTypeHTTP2, Component: "TCP Connectivity", Target: "Port 7844 (HTTP/2)", ProbeStatus: Pass, Details: "TLS handshake successful"},
{Type: ProbeTypeManagementAPI, Component: "Cloudflare API", Target: "api.cloudflare.com:443", ProbeStatus: Pass, Details: "Reachable"},
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region1.v2.argotunnel.com", ProbeStatus: Pass, Details: dnsResolvedSuccessfully},
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region2.v2.argotunnel.com", ProbeStatus: Pass, Details: dnsResolvedSuccessfully},
{Type: ProbeTypeQUIC, Component: "UDP Connectivity", Target: "Port 7844 (QUIC)", ProbeStatus: Pass, Details: detailsQUICHandshakeSuccessful},
{Type: ProbeTypeHTTP2, Component: "TCP Connectivity", Target: "Port 7844 (HTTP/2)", ProbeStatus: Pass, Details: detailsHTTP2HandshakeSuccessful},
{Type: ProbeTypeManagementAPI, Component: "Cloudflare API", Target: "api.cloudflare.com:443", ProbeStatus: Pass, Details: detailsApiReachable},
},
}
}
@@ -41,18 +41,18 @@ func quicBlockedReport() Report {
RunID: fixedRunID,
SuggestedProtocol: new(connection.HTTP2),
Results: []CheckResult{
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region1.v2.argotunnel.com", ProbeStatus: Pass, Details: "Resolved successfully"},
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region2.v2.argotunnel.com", ProbeStatus: Pass, Details: "Resolved successfully"},
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region1.v2.argotunnel.com", ProbeStatus: Pass, Details: dnsResolvedSuccessfully},
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region2.v2.argotunnel.com", ProbeStatus: Pass, Details: dnsResolvedSuccessfully},
{
Type: ProbeTypeQUIC,
Component: "UDP Connectivity",
Target: "Port 7844 (QUIC)",
ProbeStatus: Fail,
Details: "Handshake failed",
Action: "Allow outbound QUIC on port 7844. cloudflared will use http2 in the meantime.",
Details: detailsQUICHandshakeFailed,
Action: actionQUICBlocked,
},
{Type: ProbeTypeHTTP2, Component: "TCP Connectivity", Target: "Port 7844 (HTTP/2)", ProbeStatus: Pass, Details: "TLS handshake successful"},
{Type: ProbeTypeManagementAPI, Component: "Cloudflare API", Target: "api.cloudflare.com:443", ProbeStatus: Pass, Details: "Reachable"},
{Type: ProbeTypeHTTP2, Component: "TCP Connectivity", Target: "Port 7844 (HTTP/2)", ProbeStatus: Pass, Details: detailsHTTP2HandshakeSuccessful},
{Type: ProbeTypeManagementAPI, Component: "Cloudflare API", Target: "api.cloudflare.com:443", ProbeStatus: Pass, Details: detailsApiReachable},
},
}
}
@@ -64,10 +64,10 @@ func apiFailReport() Report {
RunID: fixedRunID,
SuggestedProtocol: new(connection.QUIC),
Results: []CheckResult{
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region1.v2.argotunnel.com", ProbeStatus: Pass, Details: "Resolved successfully"},
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region2.v2.argotunnel.com", ProbeStatus: Pass, Details: "Resolved successfully"},
{Type: ProbeTypeQUIC, Component: "UDP Connectivity", Target: "Port 7844 (QUIC)", ProbeStatus: Pass, Details: "Handshake successful"},
{Type: ProbeTypeHTTP2, Component: "TCP Connectivity", Target: "Port 7844 (HTTP/2)", ProbeStatus: Pass, Details: "TLS handshake successful"},
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region1.v2.argotunnel.com", ProbeStatus: Pass, Details: dnsResolvedSuccessfully},
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region2.v2.argotunnel.com", ProbeStatus: Pass, Details: dnsResolvedSuccessfully},
{Type: ProbeTypeQUIC, Component: "UDP Connectivity", Target: "Port 7844 (QUIC)", ProbeStatus: Pass, Details: detailsQUICHandshakeSuccessful},
{Type: ProbeTypeHTTP2, Component: "TCP Connectivity", Target: "Port 7844 (HTTP/2)", ProbeStatus: Pass, Details: detailsHTTP2HandshakeSuccessful},
{
Type: ProbeTypeManagementAPI,
Component: "Cloudflare API",
@@ -86,14 +86,14 @@ func bothTransportsBlockedReport() Report {
RunID: fixedRunID,
SuggestedProtocol: nil,
Results: []CheckResult{
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region1.v2.argotunnel.com", ProbeStatus: Pass, Details: "Resolved successfully"},
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region2.v2.argotunnel.com", ProbeStatus: Pass, Details: "Resolved successfully"},
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region1.v2.argotunnel.com", ProbeStatus: Pass, Details: dnsResolvedSuccessfully},
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region2.v2.argotunnel.com", ProbeStatus: Pass, Details: dnsResolvedSuccessfully},
{
Type: ProbeTypeQUIC,
Component: "UDP Connectivity",
Target: "Port 7844 (QUIC)",
ProbeStatus: Fail,
Details: "Handshake failed",
Details: detailsQUICHandshakeFailed,
Action: "Allow outbound QUIC and/or TCP on port 7844 to the Cloudflare edge.",
},
{
@@ -101,9 +101,9 @@ func bothTransportsBlockedReport() Report {
Component: "TCP Connectivity",
Target: "Port 7844 (HTTP/2)",
ProbeStatus: Fail,
Details: "Blocked or unreachable",
Details: detailsHTTP2BlockedOrUnreachable,
},
{Type: ProbeTypeManagementAPI, Component: "Cloudflare API", Target: "api.cloudflare.com:443", ProbeStatus: Pass, Details: "Reachable"},
{Type: ProbeTypeManagementAPI, Component: "Cloudflare API", Target: "api.cloudflare.com:443", ProbeStatus: Pass, Details: detailsApiReachable},
},
}
}
@@ -137,11 +137,11 @@ func TestString_AllPass(t *testing.T) {
want := "" +
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
"COMPONENT TARGET STATUS DETAILS\n" +
"DNS Resolution region1.v2.argotunnel.com PASS Resolved successfully\n" +
"DNS Resolution region2.v2.argotunnel.com PASS Resolved successfully\n" +
"UDP Connectivity Port 7844 (QUIC) PASS Handshake successful\n" +
"TCP Connectivity Port 7844 (HTTP/2) PASS TLS handshake successful\n" +
"Cloudflare API api.cloudflare.com:443 PASS Reachable\n" +
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully\n" +
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully\n" +
"UDP Connectivity Port 7844 (QUIC) PASS QUIC connection successful\n" +
"TCP Connectivity Port 7844 (HTTP/2) PASS HTTP/2 connection successful\n" +
"Cloudflare API api.cloudflare.com:443 PASS API is reachable\n" +
"\n" +
"SUMMARY: Environment is healthy. cloudflared will use 'quic' as primary protocol.\n" +
"--------------------------------------------------------------------------------\n"
@@ -153,12 +153,12 @@ func TestString_QuicBlocked(t *testing.T) {
want := "" +
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
"COMPONENT TARGET STATUS DETAILS\n" +
"DNS Resolution region1.v2.argotunnel.com PASS Resolved successfully\n" +
"DNS Resolution region2.v2.argotunnel.com PASS Resolved successfully\n" +
"UDP Connectivity Port 7844 (QUIC) FAIL Handshake failed\n" +
"TCP Connectivity Port 7844 (HTTP/2) PASS TLS handshake successful\n" +
"Cloudflare API api.cloudflare.com:443 PASS Reachable\n" +
"WARNING: Allow outbound QUIC on port 7844. cloudflared will use http2 in the meantime.\n" +
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully\n" +
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully\n" +
"UDP Connectivity Port 7844 (QUIC) FAIL QUIC connection failed\n" +
"TCP Connectivity Port 7844 (HTTP/2) PASS HTTP/2 connection successful\n" +
"Cloudflare API api.cloudflare.com:443 PASS API is reachable\n" +
"WARNING: Allow outbound QUIC traffic on port 7844 or use HTTP2.\n" +
"\n" +
"SUMMARY: Environment ready with degraded transport. cloudflared will proceed using 'http2'.\n" +
"--------------------------------------------------------------------------------\n"
@@ -170,10 +170,10 @@ func TestString_APIFail(t *testing.T) {
want := "" +
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
"COMPONENT TARGET STATUS DETAILS\n" +
"DNS Resolution region1.v2.argotunnel.com PASS Resolved successfully\n" +
"DNS Resolution region2.v2.argotunnel.com PASS Resolved successfully\n" +
"UDP Connectivity Port 7844 (QUIC) PASS Handshake successful\n" +
"TCP Connectivity Port 7844 (HTTP/2) PASS TLS handshake successful\n" +
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully\n" +
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully\n" +
"UDP Connectivity Port 7844 (QUIC) PASS QUIC connection successful\n" +
"TCP Connectivity Port 7844 (HTTP/2) PASS HTTP/2 connection successful\n" +
"Cloudflare API api.cloudflare.com:443 FAIL Connection refused\n" +
"WARNING: cloudflared will still run, but automatic software updates are unavailable. Ensure port 443 TCP to api.cloudflare.com is open if you want auto-updates.\n" +
"\n" +
@@ -187,11 +187,11 @@ func TestString_BothTransportsBlocked(t *testing.T) {
want := "" +
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
"COMPONENT TARGET STATUS DETAILS\n" +
"DNS Resolution region1.v2.argotunnel.com PASS Resolved successfully\n" +
"DNS Resolution region2.v2.argotunnel.com PASS Resolved successfully\n" +
"UDP Connectivity Port 7844 (QUIC) FAIL Handshake failed\n" +
"TCP Connectivity Port 7844 (HTTP/2) FAIL Blocked or unreachable\n" +
"Cloudflare API api.cloudflare.com:443 PASS Reachable\n" +
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully\n" +
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully\n" +
"UDP Connectivity Port 7844 (QUIC) FAIL QUIC connection failed\n" +
"TCP Connectivity Port 7844 (HTTP/2) FAIL HTTP/2 connection is blocked or unreachable\n" +
"Cloudflare API api.cloudflare.com:443 PASS API is reachable\n" +
"ERROR: Allow outbound QUIC and/or TCP on port 7844 to the Cloudflare edge.\n" +
"\n" +
"SUMMARY: Environment has critical failures. cloudflared may not be able to establish a tunnel.\n" +
@@ -276,11 +276,11 @@ func TestLogEvent_AllPass(t *testing.T) {
status string
details string
}{
{"DNS Resolution", "region1.v2.argotunnel.com", "pass", "Resolved successfully"},
{"DNS Resolution", "region2.v2.argotunnel.com", "pass", "Resolved successfully"},
{"UDP Connectivity", "Port 7844 (QUIC)", "pass", "Handshake successful"},
{"TCP Connectivity", "Port 7844 (HTTP/2)", "pass", "TLS handshake successful"},
{"Cloudflare API", "api.cloudflare.com:443", "pass", "Reachable"},
{"DNS Resolution", "region1.v2.argotunnel.com", "pass", dnsResolvedSuccessfully},
{"DNS Resolution", "region2.v2.argotunnel.com", "pass", dnsResolvedSuccessfully},
{"UDP Connectivity", "Port 7844 (QUIC)", "pass", detailsQUICHandshakeSuccessful},
{"TCP Connectivity", "Port 7844 (HTTP/2)", "pass", detailsHTTP2HandshakeSuccessful},
{"Cloudflare API", "api.cloudflare.com:443", "pass", detailsApiReachable},
}
for i, exp := range expected {
e := entries[i]
@@ -312,7 +312,7 @@ func TestLogEvent_QuicBlocked(t *testing.T) {
assert.Equal(t, "fail", quic.Status)
assert.Equal(t, "UDP Connectivity", quic.Component)
assert.Equal(t, "Port 7844 (QUIC)", quic.Target)
assert.Equal(t, "Handshake failed", quic.Details)
assert.Equal(t, "QUIC connection failed", quic.Details)
assert.Equal(t, fixedRunID.String(), quic.RunID)
// Summary: not a hard fail (HTTP/2 still works), protocol falls back to http2.
@@ -354,9 +354,9 @@ func TestLogEvent_BothTransportsBlocked(t *testing.T) {
// Both transport rows carry status=fail.
assert.Equal(t, "fail", entries[2].Status)
assert.Equal(t, "Handshake failed", entries[2].Details)
assert.Equal(t, "QUIC connection failed", entries[2].Details)
assert.Equal(t, "fail", entries[3].Status)
assert.Equal(t, "Blocked or unreachable", entries[3].Details)
assert.Equal(t, "HTTP/2 connection is blocked or unreachable", entries[3].Details)
summary := entries[len(entries)-1]
require.NotNil(t, summary.HardFail)
-3
View File
@@ -66,9 +66,6 @@ type TunnelConfig struct {
// NoPrechecks disables connectivity pre-checks at startup.
NoPrechecks bool
// Prechecks enables connectivity pre-checks at startup.
Prechecks bool
NamedTunnel *connection.TunnelProperties
ProtocolSelector connection.ProtocolSelector
EdgeTLSConfigs map[connection.Protocol]*tls.Config