mirror of
https://github.com/cloudflare/cloudflared.git
synced 2026-06-23 04:10:20 +00:00
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:
@@ -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
@@ -25,7 +25,7 @@ const (
|
|||||||
|
|
||||||
// Action messages for each probe outcome.
|
// 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."
|
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."
|
actionHTTP2Blocked = "Allow outbound TCP on port 7844."
|
||||||
actionAPIUnreachable = "cloudflared will still run, but automatic software updates are unavailable. " +
|
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."
|
"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)"
|
noDNSTarget = "No DNS target (Using edge flag)"
|
||||||
|
|
||||||
// Details messages for CheckResult.
|
// Details messages for CheckResult.
|
||||||
detailsNoAddressesReturned = "No addresses returned"
|
dnsNoAddressesReturned = "No addresses returned"
|
||||||
detailsResolvedSuccessfully = "Resolved successfully"
|
dnsResolvedSuccessfully = "DNS Resolved successfully"
|
||||||
detailsHandshakeFailed = "Handshake failed"
|
detailsQUICHandshakeFailed = "QUIC connection failed"
|
||||||
detailsHandshakeSuccessful = "Handshake successful"
|
detailsQUICHandshakeSuccessful = "QUIC connection successful"
|
||||||
detailsBlockedOrUnreachable = "Blocked or unreachable"
|
detailsHTTP2BlockedOrUnreachable = "HTTP/2 connection is blocked or unreachable"
|
||||||
detailsTLSHandshakeSuccessful = "TLS handshake successful"
|
detailsHTTP2HandshakeSuccessful = "HTTP/2 connection successful"
|
||||||
detailsConnectionFailed = "Connection failed"
|
detailsAPIConnectionFailed = "API Connection failed"
|
||||||
detailsTCPPortReachable = "TCP port reachable (TLS not validated)"
|
detailsApiReachable = "API is reachable"
|
||||||
detailsDNSPrerequisiteFailed = "DNS prerequisite failed"
|
detailsDNSPrerequisiteFailed = "DNS prerequisite failed"
|
||||||
detailsTLSConfigFailed = "TLS configuration failed"
|
detailsTLSConfigFailed = "TLS configuration failed"
|
||||||
|
|
||||||
// Region hostname templates.
|
// Region hostname templates.
|
||||||
region1Global = "region1.v2.argotunnel.com"
|
region1Global = "region1.v2.argotunnel.com"
|
||||||
@@ -140,7 +140,7 @@ func probeDNS(
|
|||||||
|
|
||||||
addrGroups, err := resolver.Resolve(region)
|
addrGroups, err := resolver.Resolve(region)
|
||||||
if err != nil || len(addrGroups) == 0 {
|
if err != nil || len(addrGroups) == 0 {
|
||||||
detail := detailsNoAddressesReturned
|
detail := dnsNoAddressesReturned
|
||||||
if err != nil {
|
if err != nil {
|
||||||
detail = err.Error()
|
detail = err.Error()
|
||||||
}
|
}
|
||||||
@@ -158,12 +158,12 @@ func probeDNS(
|
|||||||
group := addrGroups[i]
|
group := addrGroups[i]
|
||||||
if len(group) == 0 {
|
if len(group) == 0 {
|
||||||
resolved = append(resolved, ResolvedTarget{
|
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 {
|
} else {
|
||||||
resolved = append(resolved, ResolvedTarget{
|
resolved = append(resolved, ResolvedTarget{
|
||||||
Addrs: group,
|
Addrs: group,
|
||||||
DNSResult: newDNSCheckResult(target, Pass, detailsResolvedSuccessfully, ""),
|
DNSResult: newDNSCheckResult(target, Pass, dnsResolvedSuccessfully, ""),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,7 +209,7 @@ func probeQUIC(
|
|||||||
Component: componentUDPConnectivity,
|
Component: componentUDPConnectivity,
|
||||||
Target: targetPortQUIC,
|
Target: targetPortQUIC,
|
||||||
ProbeStatus: Fail,
|
ProbeStatus: Fail,
|
||||||
Details: detailsHandshakeFailed,
|
Details: detailsQUICHandshakeFailed,
|
||||||
Action: actionQUICBlocked,
|
Action: actionQUICBlocked,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,7 +223,7 @@ func probeQUIC(
|
|||||||
Component: componentUDPConnectivity,
|
Component: componentUDPConnectivity,
|
||||||
Target: targetPortQUIC,
|
Target: targetPortQUIC,
|
||||||
ProbeStatus: Pass,
|
ProbeStatus: Pass,
|
||||||
Details: detailsHandshakeSuccessful,
|
Details: detailsQUICHandshakeSuccessful,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,7 +243,7 @@ func probeHTTP2(ctx context.Context, tlsConfig *tls.Config, dialer TCPDialer, ad
|
|||||||
Component: componentTCPConnectivity,
|
Component: componentTCPConnectivity,
|
||||||
Target: targetPortHTTP2,
|
Target: targetPortHTTP2,
|
||||||
ProbeStatus: Fail,
|
ProbeStatus: Fail,
|
||||||
Details: detailsBlockedOrUnreachable,
|
Details: detailsHTTP2BlockedOrUnreachable,
|
||||||
Action: actionHTTP2Blocked,
|
Action: actionHTTP2Blocked,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -254,7 +254,7 @@ func probeHTTP2(ctx context.Context, tlsConfig *tls.Config, dialer TCPDialer, ad
|
|||||||
Component: componentTCPConnectivity,
|
Component: componentTCPConnectivity,
|
||||||
Target: targetPortHTTP2,
|
Target: targetPortHTTP2,
|
||||||
ProbeStatus: Pass,
|
ProbeStatus: Pass,
|
||||||
Details: detailsTLSHandshakeSuccessful,
|
Details: detailsHTTP2HandshakeSuccessful,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,7 +273,7 @@ func probeManagementAPI(ctx context.Context, dialer ManagementDialer) CheckResul
|
|||||||
Component: componentCloudflareAPI,
|
Component: componentCloudflareAPI,
|
||||||
Target: targetAPI,
|
Target: targetAPI,
|
||||||
ProbeStatus: Fail,
|
ProbeStatus: Fail,
|
||||||
Details: detailsConnectionFailed,
|
Details: detailsAPIConnectionFailed,
|
||||||
Action: actionAPIUnreachable,
|
Action: actionAPIUnreachable,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -284,7 +284,7 @@ func probeManagementAPI(ctx context.Context, dialer ManagementDialer) CheckResul
|
|||||||
Component: componentCloudflareAPI,
|
Component: componentCloudflareAPI,
|
||||||
Target: targetAPI,
|
Target: targetAPI,
|
||||||
ProbeStatus: Pass,
|
ProbeStatus: Pass,
|
||||||
Details: detailsTCPPortReachable,
|
Details: detailsApiReachable,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,11 +363,11 @@ func resolveStaticEdge(addrs []string, log *zerolog.Logger) []ResolvedTarget {
|
|||||||
if len(resolved) > 0 {
|
if len(resolved) > 0 {
|
||||||
targets = append(targets, ResolvedTarget{
|
targets = append(targets, ResolvedTarget{
|
||||||
Addrs: resolved,
|
Addrs: resolved,
|
||||||
DNSResult: newDNSCheckResult(addr, Pass, detailsResolvedSuccessfully, ""),
|
DNSResult: newDNSCheckResult(addr, Pass, dnsResolvedSuccessfully, ""),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
targets = append(targets, ResolvedTarget{
|
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
@@ -124,7 +124,7 @@ func TestProbeDNS_Success(t *testing.T) {
|
|||||||
assert.Equal(t, ProbeTypeDNS, targets[0].DNSResult.Type)
|
assert.Equal(t, ProbeTypeDNS, targets[0].DNSResult.Type)
|
||||||
assert.Equal(t, testRegion1Global, targets[0].DNSResult.Target)
|
assert.Equal(t, testRegion1Global, targets[0].DNSResult.Target)
|
||||||
assert.Equal(t, Pass, targets[0].DNSResult.ProbeStatus)
|
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) {
|
func TestProbeDNS_MultipleRegions(t *testing.T) {
|
||||||
@@ -184,7 +184,7 @@ func TestProbeDNS_EmptyResults(t *testing.T) {
|
|||||||
require.Len(t, targets, 2)
|
require.Len(t, targets, 2)
|
||||||
assert.Empty(t, targets[0].Addrs)
|
assert.Empty(t, targets[0].Addrs)
|
||||||
assert.Equal(t, Fail, targets[0].DNSResult.ProbeStatus)
|
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) {
|
func TestProbeDNS_EmptyGroup(t *testing.T) {
|
||||||
@@ -200,7 +200,7 @@ func TestProbeDNS_EmptyGroup(t *testing.T) {
|
|||||||
require.Len(t, targets, 1)
|
require.Len(t, targets, 1)
|
||||||
assert.Empty(t, targets[0].Addrs)
|
assert.Empty(t, targets[0].Addrs)
|
||||||
assert.Equal(t, Fail, targets[0].DNSResult.ProbeStatus)
|
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) {
|
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, ProbeTypeQUIC, result.Type)
|
||||||
assert.Equal(t, Pass, result.ProbeStatus)
|
assert.Equal(t, Pass, result.ProbeStatus)
|
||||||
assert.Equal(t, detailsHandshakeSuccessful, result.Details)
|
assert.Equal(t, detailsQUICHandshakeSuccessful, result.Details)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProbeQUIC_DialError(t *testing.T) {
|
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, ProbeTypeQUIC, result.Type)
|
||||||
assert.Equal(t, Fail, result.ProbeStatus)
|
assert.Equal(t, Fail, result.ProbeStatus)
|
||||||
assert.Equal(t, detailsHandshakeFailed, result.Details)
|
assert.Equal(t, detailsQUICHandshakeFailed, result.Details)
|
||||||
assert.Equal(t, actionQUICBlocked, result.Action)
|
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, ProbeTypeQUIC, result.Type)
|
||||||
assert.Equal(t, Pass, result.ProbeStatus)
|
assert.Equal(t, Pass, result.ProbeStatus)
|
||||||
assert.Equal(t, detailsHandshakeSuccessful, result.Details)
|
assert.Equal(t, detailsQUICHandshakeSuccessful, result.Details)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProbeQUIC_ContextTimeout(t *testing.T) {
|
func TestProbeQUIC_ContextTimeout(t *testing.T) {
|
||||||
@@ -291,7 +291,7 @@ func TestProbeQUIC_ContextTimeout(t *testing.T) {
|
|||||||
result := probeQUIC(context.Background(), testTLSConfig, dialer, addr, &logger)
|
result := probeQUIC(context.Background(), testTLSConfig, dialer, addr, &logger)
|
||||||
|
|
||||||
assert.Equal(t, Fail, result.ProbeStatus)
|
assert.Equal(t, Fail, result.ProbeStatus)
|
||||||
assert.Equal(t, detailsHandshakeFailed, result.Details)
|
assert.Equal(t, detailsQUICHandshakeFailed, result.Details)
|
||||||
}
|
}
|
||||||
|
|
||||||
// probeHTTP2 tests.
|
// probeHTTP2 tests.
|
||||||
@@ -310,7 +310,7 @@ func TestProbeHTTP2_Success(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, ProbeTypeHTTP2, result.Type)
|
assert.Equal(t, ProbeTypeHTTP2, result.Type)
|
||||||
assert.Equal(t, Pass, result.ProbeStatus)
|
assert.Equal(t, Pass, result.ProbeStatus)
|
||||||
assert.Equal(t, detailsTLSHandshakeSuccessful, result.Details)
|
assert.Equal(t, detailsHTTP2HandshakeSuccessful, result.Details)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProbeHTTP2_DialError(t *testing.T) {
|
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, ProbeTypeHTTP2, result.Type)
|
||||||
assert.Equal(t, Fail, result.ProbeStatus)
|
assert.Equal(t, Fail, result.ProbeStatus)
|
||||||
assert.Equal(t, detailsBlockedOrUnreachable, result.Details)
|
assert.Equal(t, detailsHTTP2BlockedOrUnreachable, result.Details)
|
||||||
assert.Equal(t, actionHTTP2Blocked, result.Action)
|
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, "Cloudflare API", result.Component)
|
||||||
assert.Equal(t, "api.cloudflare.com:443", result.Target)
|
assert.Equal(t, "api.cloudflare.com:443", result.Target)
|
||||||
assert.Equal(t, Pass, result.ProbeStatus)
|
assert.Equal(t, Pass, result.ProbeStatus)
|
||||||
assert.Equal(t, detailsTCPPortReachable, result.Details)
|
assert.Equal(t, detailsApiReachable, result.Details)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProbeManagementAPI_DialError(t *testing.T) {
|
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, ProbeTypeManagementAPI, result.Type)
|
||||||
assert.Equal(t, Fail, result.ProbeStatus)
|
assert.Equal(t, Fail, result.ProbeStatus)
|
||||||
assert.Equal(t, detailsConnectionFailed, result.Details)
|
assert.Equal(t, detailsAPIConnectionFailed, result.Details)
|
||||||
assert.Equal(t, actionAPIUnreachable, result.Action)
|
assert.Equal(t, actionAPIUnreachable, result.Action)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -516,7 +516,7 @@ func TestProbeQUIC_IPv6Address(t *testing.T) {
|
|||||||
result := probeQUIC(context.Background(), testTLSConfig, dialer, addr, &logger)
|
result := probeQUIC(context.Background(), testTLSConfig, dialer, addr, &logger)
|
||||||
|
|
||||||
assert.Equal(t, Pass, result.ProbeStatus)
|
assert.Equal(t, Pass, result.ProbeStatus)
|
||||||
assert.Equal(t, detailsHandshakeSuccessful, result.Details)
|
assert.Equal(t, detailsQUICHandshakeSuccessful, result.Details)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IPv6 address tests for probeHTTP2.
|
// IPv6 address tests for probeHTTP2.
|
||||||
@@ -571,7 +571,7 @@ func TestResolveStaticEdge_InvalidAddr(t *testing.T) {
|
|||||||
require.Len(t, targets, 1)
|
require.Len(t, targets, 1)
|
||||||
assert.Equal(t, "not-a-valid-addr", targets[0].DNSResult.Target)
|
assert.Equal(t, "not-a-valid-addr", targets[0].DNSResult.Target)
|
||||||
assert.Equal(t, Fail, targets[0].DNSResult.ProbeStatus)
|
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)
|
assert.Empty(t, targets[0].Addrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+48
-48
@@ -25,11 +25,11 @@ func allPassReport() Report {
|
|||||||
RunID: fixedRunID,
|
RunID: fixedRunID,
|
||||||
SuggestedProtocol: new(connection.QUIC),
|
SuggestedProtocol: new(connection.QUIC),
|
||||||
Results: []CheckResult{
|
Results: []CheckResult{
|
||||||
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region1.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: "Resolved successfully"},
|
{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: "Handshake successful"},
|
{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: "TLS handshake successful"},
|
{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: "Reachable"},
|
{Type: ProbeTypeManagementAPI, Component: "Cloudflare API", Target: "api.cloudflare.com:443", ProbeStatus: Pass, Details: detailsApiReachable},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,18 +41,18 @@ func quicBlockedReport() Report {
|
|||||||
RunID: fixedRunID,
|
RunID: fixedRunID,
|
||||||
SuggestedProtocol: new(connection.HTTP2),
|
SuggestedProtocol: new(connection.HTTP2),
|
||||||
Results: []CheckResult{
|
Results: []CheckResult{
|
||||||
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region1.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: "Resolved successfully"},
|
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region2.v2.argotunnel.com", ProbeStatus: Pass, Details: dnsResolvedSuccessfully},
|
||||||
{
|
{
|
||||||
Type: ProbeTypeQUIC,
|
Type: ProbeTypeQUIC,
|
||||||
Component: "UDP Connectivity",
|
Component: "UDP Connectivity",
|
||||||
Target: "Port 7844 (QUIC)",
|
Target: "Port 7844 (QUIC)",
|
||||||
ProbeStatus: Fail,
|
ProbeStatus: Fail,
|
||||||
Details: "Handshake failed",
|
Details: detailsQUICHandshakeFailed,
|
||||||
Action: "Allow outbound QUIC on port 7844. cloudflared will use http2 in the meantime.",
|
Action: actionQUICBlocked,
|
||||||
},
|
},
|
||||||
{Type: ProbeTypeHTTP2, Component: "TCP Connectivity", Target: "Port 7844 (HTTP/2)", ProbeStatus: Pass, Details: "TLS handshake successful"},
|
{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: "Reachable"},
|
{Type: ProbeTypeManagementAPI, Component: "Cloudflare API", Target: "api.cloudflare.com:443", ProbeStatus: Pass, Details: detailsApiReachable},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,10 +64,10 @@ func apiFailReport() Report {
|
|||||||
RunID: fixedRunID,
|
RunID: fixedRunID,
|
||||||
SuggestedProtocol: new(connection.QUIC),
|
SuggestedProtocol: new(connection.QUIC),
|
||||||
Results: []CheckResult{
|
Results: []CheckResult{
|
||||||
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region1.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: "Resolved successfully"},
|
{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: "Handshake successful"},
|
{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: "TLS handshake successful"},
|
{Type: ProbeTypeHTTP2, Component: "TCP Connectivity", Target: "Port 7844 (HTTP/2)", ProbeStatus: Pass, Details: detailsHTTP2HandshakeSuccessful},
|
||||||
{
|
{
|
||||||
Type: ProbeTypeManagementAPI,
|
Type: ProbeTypeManagementAPI,
|
||||||
Component: "Cloudflare API",
|
Component: "Cloudflare API",
|
||||||
@@ -86,14 +86,14 @@ func bothTransportsBlockedReport() Report {
|
|||||||
RunID: fixedRunID,
|
RunID: fixedRunID,
|
||||||
SuggestedProtocol: nil,
|
SuggestedProtocol: nil,
|
||||||
Results: []CheckResult{
|
Results: []CheckResult{
|
||||||
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region1.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: "Resolved successfully"},
|
{Type: ProbeTypeDNS, Component: "DNS Resolution", Target: "region2.v2.argotunnel.com", ProbeStatus: Pass, Details: dnsResolvedSuccessfully},
|
||||||
{
|
{
|
||||||
Type: ProbeTypeQUIC,
|
Type: ProbeTypeQUIC,
|
||||||
Component: "UDP Connectivity",
|
Component: "UDP Connectivity",
|
||||||
Target: "Port 7844 (QUIC)",
|
Target: "Port 7844 (QUIC)",
|
||||||
ProbeStatus: Fail,
|
ProbeStatus: Fail,
|
||||||
Details: "Handshake failed",
|
Details: detailsQUICHandshakeFailed,
|
||||||
Action: "Allow outbound QUIC and/or TCP on port 7844 to the Cloudflare edge.",
|
Action: "Allow outbound QUIC and/or TCP on port 7844 to the Cloudflare edge.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -101,9 +101,9 @@ func bothTransportsBlockedReport() Report {
|
|||||||
Component: "TCP Connectivity",
|
Component: "TCP Connectivity",
|
||||||
Target: "Port 7844 (HTTP/2)",
|
Target: "Port 7844 (HTTP/2)",
|
||||||
ProbeStatus: Fail,
|
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 := "" +
|
want := "" +
|
||||||
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
|
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
|
||||||
"COMPONENT TARGET STATUS DETAILS\n" +
|
"COMPONENT TARGET STATUS DETAILS\n" +
|
||||||
"DNS Resolution region1.v2.argotunnel.com PASS Resolved successfully\n" +
|
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully\n" +
|
||||||
"DNS Resolution region2.v2.argotunnel.com PASS Resolved successfully\n" +
|
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully\n" +
|
||||||
"UDP Connectivity Port 7844 (QUIC) PASS Handshake successful\n" +
|
"UDP Connectivity Port 7844 (QUIC) PASS QUIC connection successful\n" +
|
||||||
"TCP Connectivity Port 7844 (HTTP/2) PASS TLS handshake successful\n" +
|
"TCP Connectivity Port 7844 (HTTP/2) PASS HTTP/2 connection successful\n" +
|
||||||
"Cloudflare API api.cloudflare.com:443 PASS Reachable\n" +
|
"Cloudflare API api.cloudflare.com:443 PASS API is reachable\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"SUMMARY: Environment is healthy. cloudflared will use 'quic' as primary protocol.\n" +
|
"SUMMARY: Environment is healthy. cloudflared will use 'quic' as primary protocol.\n" +
|
||||||
"--------------------------------------------------------------------------------\n"
|
"--------------------------------------------------------------------------------\n"
|
||||||
@@ -153,12 +153,12 @@ func TestString_QuicBlocked(t *testing.T) {
|
|||||||
want := "" +
|
want := "" +
|
||||||
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
|
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
|
||||||
"COMPONENT TARGET STATUS DETAILS\n" +
|
"COMPONENT TARGET STATUS DETAILS\n" +
|
||||||
"DNS Resolution region1.v2.argotunnel.com PASS Resolved successfully\n" +
|
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully\n" +
|
||||||
"DNS Resolution region2.v2.argotunnel.com PASS Resolved successfully\n" +
|
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully\n" +
|
||||||
"UDP Connectivity Port 7844 (QUIC) FAIL Handshake failed\n" +
|
"UDP Connectivity Port 7844 (QUIC) FAIL QUIC connection failed\n" +
|
||||||
"TCP Connectivity Port 7844 (HTTP/2) PASS TLS handshake successful\n" +
|
"TCP Connectivity Port 7844 (HTTP/2) PASS HTTP/2 connection successful\n" +
|
||||||
"Cloudflare API api.cloudflare.com:443 PASS Reachable\n" +
|
"Cloudflare API api.cloudflare.com:443 PASS API is reachable\n" +
|
||||||
"WARNING: Allow outbound QUIC on port 7844. cloudflared will use http2 in the meantime.\n" +
|
"WARNING: Allow outbound QUIC traffic on port 7844 or use HTTP2.\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"SUMMARY: Environment ready with degraded transport. cloudflared will proceed using 'http2'.\n" +
|
"SUMMARY: Environment ready with degraded transport. cloudflared will proceed using 'http2'.\n" +
|
||||||
"--------------------------------------------------------------------------------\n"
|
"--------------------------------------------------------------------------------\n"
|
||||||
@@ -170,10 +170,10 @@ func TestString_APIFail(t *testing.T) {
|
|||||||
want := "" +
|
want := "" +
|
||||||
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
|
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
|
||||||
"COMPONENT TARGET STATUS DETAILS\n" +
|
"COMPONENT TARGET STATUS DETAILS\n" +
|
||||||
"DNS Resolution region1.v2.argotunnel.com PASS Resolved successfully\n" +
|
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully\n" +
|
||||||
"DNS Resolution region2.v2.argotunnel.com PASS Resolved successfully\n" +
|
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully\n" +
|
||||||
"UDP Connectivity Port 7844 (QUIC) PASS Handshake successful\n" +
|
"UDP Connectivity Port 7844 (QUIC) PASS QUIC connection successful\n" +
|
||||||
"TCP Connectivity Port 7844 (HTTP/2) PASS TLS handshake successful\n" +
|
"TCP Connectivity Port 7844 (HTTP/2) PASS HTTP/2 connection successful\n" +
|
||||||
"Cloudflare API api.cloudflare.com:443 FAIL Connection refused\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" +
|
"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" +
|
"\n" +
|
||||||
@@ -187,11 +187,11 @@ func TestString_BothTransportsBlocked(t *testing.T) {
|
|||||||
want := "" +
|
want := "" +
|
||||||
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
|
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
|
||||||
"COMPONENT TARGET STATUS DETAILS\n" +
|
"COMPONENT TARGET STATUS DETAILS\n" +
|
||||||
"DNS Resolution region1.v2.argotunnel.com PASS Resolved successfully\n" +
|
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully\n" +
|
||||||
"DNS Resolution region2.v2.argotunnel.com PASS Resolved successfully\n" +
|
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully\n" +
|
||||||
"UDP Connectivity Port 7844 (QUIC) FAIL Handshake failed\n" +
|
"UDP Connectivity Port 7844 (QUIC) FAIL QUIC connection failed\n" +
|
||||||
"TCP Connectivity Port 7844 (HTTP/2) FAIL Blocked or unreachable\n" +
|
"TCP Connectivity Port 7844 (HTTP/2) FAIL HTTP/2 connection is blocked or unreachable\n" +
|
||||||
"Cloudflare API api.cloudflare.com:443 PASS Reachable\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" +
|
"ERROR: Allow outbound QUIC and/or TCP on port 7844 to the Cloudflare edge.\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"SUMMARY: Environment has critical failures. cloudflared may not be able to establish a tunnel.\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
|
status string
|
||||||
details string
|
details string
|
||||||
}{
|
}{
|
||||||
{"DNS Resolution", "region1.v2.argotunnel.com", "pass", "Resolved successfully"},
|
{"DNS Resolution", "region1.v2.argotunnel.com", "pass", dnsResolvedSuccessfully},
|
||||||
{"DNS Resolution", "region2.v2.argotunnel.com", "pass", "Resolved successfully"},
|
{"DNS Resolution", "region2.v2.argotunnel.com", "pass", dnsResolvedSuccessfully},
|
||||||
{"UDP Connectivity", "Port 7844 (QUIC)", "pass", "Handshake successful"},
|
{"UDP Connectivity", "Port 7844 (QUIC)", "pass", detailsQUICHandshakeSuccessful},
|
||||||
{"TCP Connectivity", "Port 7844 (HTTP/2)", "pass", "TLS handshake successful"},
|
{"TCP Connectivity", "Port 7844 (HTTP/2)", "pass", detailsHTTP2HandshakeSuccessful},
|
||||||
{"Cloudflare API", "api.cloudflare.com:443", "pass", "Reachable"},
|
{"Cloudflare API", "api.cloudflare.com:443", "pass", detailsApiReachable},
|
||||||
}
|
}
|
||||||
for i, exp := range expected {
|
for i, exp := range expected {
|
||||||
e := entries[i]
|
e := entries[i]
|
||||||
@@ -312,7 +312,7 @@ func TestLogEvent_QuicBlocked(t *testing.T) {
|
|||||||
assert.Equal(t, "fail", quic.Status)
|
assert.Equal(t, "fail", quic.Status)
|
||||||
assert.Equal(t, "UDP Connectivity", quic.Component)
|
assert.Equal(t, "UDP Connectivity", quic.Component)
|
||||||
assert.Equal(t, "Port 7844 (QUIC)", quic.Target)
|
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)
|
assert.Equal(t, fixedRunID.String(), quic.RunID)
|
||||||
|
|
||||||
// Summary: not a hard fail (HTTP/2 still works), protocol falls back to http2.
|
// 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.
|
// Both transport rows carry status=fail.
|
||||||
assert.Equal(t, "fail", entries[2].Status)
|
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, "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]
|
summary := entries[len(entries)-1]
|
||||||
require.NotNil(t, summary.HardFail)
|
require.NotNil(t, summary.HardFail)
|
||||||
|
|||||||
@@ -66,9 +66,6 @@ type TunnelConfig struct {
|
|||||||
// NoPrechecks disables connectivity pre-checks at startup.
|
// NoPrechecks disables connectivity pre-checks at startup.
|
||||||
NoPrechecks bool
|
NoPrechecks bool
|
||||||
|
|
||||||
// Prechecks enables connectivity pre-checks at startup.
|
|
||||||
Prechecks bool
|
|
||||||
|
|
||||||
NamedTunnel *connection.TunnelProperties
|
NamedTunnel *connection.TunnelProperties
|
||||||
ProtocolSelector connection.ProtocolSelector
|
ProtocolSelector connection.ProtocolSelector
|
||||||
EdgeTLSConfigs map[connection.Protocol]*tls.Config
|
EdgeTLSConfigs map[connection.Protocol]*tls.Config
|
||||||
|
|||||||
Reference in New Issue
Block a user