mirror of
https://github.com/cloudflare/cloudflared
synced 2026-06-08 13:33:07 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 52519f67e8 | |||
| 0e84636de9 | |||
| 4177dd6936 |
@@ -1,3 +1,6 @@
|
||||
2026.5.2
|
||||
- 2026-05-26 TUN-10391: Avoid using fmt.Println
|
||||
|
||||
2026.5.1
|
||||
- 2026-05-22 fix: Bump go to 1.26.3 and go.opentelemetry.io/otel and go-jose/v4 to fix CVE's
|
||||
- 2026-05-22 TUN-10391: Avoid blocking cloudflared due to logging
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package cliutil
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v2/altsrc"
|
||||
|
||||
@@ -57,3 +60,57 @@ func ConfigureLoggingFlags(shouldHide bool) []cli.Flag {
|
||||
FlagLogOutput,
|
||||
}
|
||||
}
|
||||
|
||||
// LogTable renders lines inside an ASCII table and logs each rendered row.
|
||||
func LogTable(log *zerolog.Logger, lines []string, title ...string) {
|
||||
tableTitle := ""
|
||||
if len(title) > 0 {
|
||||
tableTitle = title[0]
|
||||
}
|
||||
for _, line := range asciiBox(lines, tableTitle, 2) {
|
||||
if line != "" {
|
||||
log.Info().Msg(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// asciiBox wraps lines in a bordered ASCII box with an optional title row.
|
||||
func asciiBox(lines []string, title string, padding int) (box []string) {
|
||||
maxLen := maxLen(lines, title)
|
||||
spacer := strings.Repeat(" ", padding)
|
||||
border := "+" + strings.Repeat("-", maxLen+(padding*2)) + "+"
|
||||
box = append(box, border)
|
||||
if title != "" {
|
||||
box = append(box, renderBoxLine(centerLine(title, maxLen), maxLen, spacer))
|
||||
box = append(box, border)
|
||||
}
|
||||
for _, line := range lines {
|
||||
box = append(box, renderBoxLine(line, maxLen, spacer))
|
||||
}
|
||||
box = append(box, border)
|
||||
return
|
||||
}
|
||||
|
||||
// renderBoxLine pads a single line so it fills the box width.
|
||||
func renderBoxLine(line string, maxLen int, spacer string) string {
|
||||
return "|" + spacer + line + strings.Repeat(" ", maxLen-len(line)) + spacer + "|"
|
||||
}
|
||||
|
||||
// centerLine pads line evenly so it is centered within width.
|
||||
func centerLine(line string, width int) string {
|
||||
padding := width - len(line)
|
||||
leftPadding := padding / 2
|
||||
rightPadding := padding - leftPadding
|
||||
return strings.Repeat(" ", leftPadding) + line + strings.Repeat(" ", rightPadding)
|
||||
}
|
||||
|
||||
// maxLen returns the longest visible line length including the title.
|
||||
func maxLen(lines []string, title string) int {
|
||||
max := len(title)
|
||||
for _, line := range lines {
|
||||
if len(line) > max {
|
||||
max = len(line)
|
||||
}
|
||||
}
|
||||
return max
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package cliutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLogTableWithoutTitle(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
lines := captureTableLogs(t, []string{"first", "second"})
|
||||
|
||||
assert.Equal(t, []string{
|
||||
"+----------+",
|
||||
"| first |",
|
||||
"| second |",
|
||||
"+----------+",
|
||||
}, lines)
|
||||
}
|
||||
|
||||
func TestLogTableWithTitle(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
lines := captureTableLogs(t, []string{"first", "second"}, "TT")
|
||||
|
||||
assert.Equal(t, []string{
|
||||
"+----------+",
|
||||
"| TT |",
|
||||
"+----------+",
|
||||
"| first |",
|
||||
"| second |",
|
||||
"+----------+",
|
||||
}, lines)
|
||||
}
|
||||
|
||||
func captureTableLogs(t *testing.T, lines []string, title ...string) []string {
|
||||
t.Helper()
|
||||
|
||||
var buf bytes.Buffer
|
||||
logger := zerolog.New(&buf)
|
||||
|
||||
LogTable(&logger, lines, title...)
|
||||
|
||||
// nolint: prealloc
|
||||
var messages []string
|
||||
for _, line := range bytes.Split(bytes.TrimSpace(buf.Bytes()), []byte("\n")) {
|
||||
var entry struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(line, &entry))
|
||||
messages = append(messages, entry.Message)
|
||||
}
|
||||
|
||||
return messages
|
||||
}
|
||||
@@ -551,8 +551,8 @@ func runPrechecks(c *cli.Context, log *zerolog.Logger, region string) {
|
||||
|
||||
report := prechecks.Run(c.Context, c.String(cfdflags.CACert), cfg, log, dialers)
|
||||
|
||||
// Output the human-readable table to console
|
||||
fmt.Println(report.String())
|
||||
// Output the human-readable table
|
||||
cliutil.LogTable(log, report.String(), "CONNECTIVITY PRE-CHECKS")
|
||||
|
||||
// Also log structured results for log aggregation
|
||||
report.LogEvent(log)
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
)
|
||||
@@ -44,7 +45,7 @@ func RunQuickTunnel(sc *subcommandContext) error {
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to request quick Tunnel")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
// This will read the entire response into memory so we can print it in case of error
|
||||
rsp_body, err := io.ReadAll(resp.Body)
|
||||
@@ -76,12 +77,10 @@ func RunQuickTunnel(sc *subcommandContext) error {
|
||||
url = "https://" + url
|
||||
}
|
||||
|
||||
for _, line := range AsciiBox([]string{
|
||||
cliutil.LogTable(sc.log, []string{
|
||||
"Your quick Tunnel has been created! Visit it at (it may take some time to be reachable):",
|
||||
url,
|
||||
}, 2) {
|
||||
sc.log.Info().Msg(line)
|
||||
}
|
||||
})
|
||||
|
||||
if !sc.c.IsSet(flags.Protocol) {
|
||||
_ = sc.c.Set(flags.Protocol, "quic")
|
||||
@@ -116,26 +115,3 @@ type QuickTunnel struct {
|
||||
AccountTag string `json:"account_tag"`
|
||||
Secret []byte `json:"secret"`
|
||||
}
|
||||
|
||||
// Print out the given lines in a nice ASCII box.
|
||||
func AsciiBox(lines []string, padding int) (box []string) {
|
||||
maxLen := maxLen(lines)
|
||||
spacer := strings.Repeat(" ", padding)
|
||||
border := "+" + strings.Repeat("-", maxLen+(padding*2)) + "+"
|
||||
box = append(box, border)
|
||||
for _, line := range lines {
|
||||
box = append(box, "|"+spacer+line+strings.Repeat(" ", maxLen-len(line))+spacer+"|")
|
||||
}
|
||||
box = append(box, border)
|
||||
return
|
||||
}
|
||||
|
||||
func maxLen(lines []string) int {
|
||||
max := 0
|
||||
for _, line := range lines {
|
||||
if len(line) > max {
|
||||
max = len(line)
|
||||
}
|
||||
}
|
||||
return max
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ 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 the human-readable table written to the log output 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.
|
||||
@@ -25,15 +25,24 @@ without real firewall intervention is:
|
||||
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.
|
||||
stdout/stderr design
|
||||
--------------------
|
||||
The pre-checks table is emitted via cliutil.LogTable, which wraps the content
|
||||
in an ASCII box and logs each line at Info level through zerolog. zerolog
|
||||
writes to stderr, which the test harness merges into stdout (stderr=STDOUT in
|
||||
Popen). 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. We
|
||||
read the accumulated lines from cfd.stdout_lines.
|
||||
|
||||
Box format (cliutil.asciiBox with padding=2, title="CONNECTIVITY PRE-CHECKS"):
|
||||
+----...----+
|
||||
| CONNECTIVITY PRE-CHECKS | (centered title)
|
||||
+----...----+
|
||||
| COMPONENT TARGET ... | (content rows)
|
||||
...
|
||||
+----...----+
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -46,11 +55,13 @@ 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
|
||||
# ASCII box constants (cliutil.asciiBox, padding=2, title="CONNECTIVITY PRE-CHECKS")
|
||||
BOX_TITLE = "CONNECTIVITY PRE-CHECKS"
|
||||
BOX_BORDER_RE = re.compile(r"^\+(-+)\+$", re.MULTILINE) # matches +----...----+
|
||||
COL_HEADER = "COMPONENT" # first word of the column-header row
|
||||
|
||||
# zerolog console format: "2006-01-02T15:04:05Z LVL <message>"
|
||||
_LOG_PREFIX_RE = re.compile(r"^\S+ \w+ ")
|
||||
|
||||
# Component names (probes.go: componentXxx)
|
||||
COMP_DNS = "DNS Resolution"
|
||||
@@ -164,23 +175,46 @@ class TableRow:
|
||||
return f"TableRow({self.component!r}, {self.target!r}, {self.status!r}, {self.details!r})"
|
||||
|
||||
|
||||
def _strip_log_prefix(line: str) -> str:
|
||||
"""Remove the zerolog console prefix ('2006-01-02T15:04:05Z LVL ') if present."""
|
||||
return _LOG_PREFIX_RE.sub("", line, count=1)
|
||||
|
||||
|
||||
def _unbox_line(line: str) -> str:
|
||||
"""Strip the box border padding from a content line: '| text |' -> 'text'.
|
||||
|
||||
Accepts lines that may still carry a zerolog console prefix; the prefix is
|
||||
removed before the box delimiters are stripped.
|
||||
"""
|
||||
msg = _strip_log_prefix(line)
|
||||
if msg.startswith("|") and msg.endswith("|"):
|
||||
return msg[1:-1].strip()
|
||||
return msg.strip()
|
||||
|
||||
|
||||
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.
|
||||
The table is now wrapped in an ASCII box by cliutil.LogTable. Each
|
||||
content line has the form '| <content> |', optionally preceded by a
|
||||
zerolog console prefix. We strip both the prefix and the box borders
|
||||
before splitting on two-or-more spaces (text/tabwriter padding=2).
|
||||
|
||||
We skip the column-header row and stop at blank lines, SUMMARY, box
|
||||
border lines, ERROR, or WARNING lines.
|
||||
"""
|
||||
rows = []
|
||||
in_data = False
|
||||
for line in stdout.splitlines():
|
||||
for raw_line in stdout.splitlines():
|
||||
msg = _strip_log_prefix(raw_line)
|
||||
line = _unbox_line(raw_line)
|
||||
if line.startswith("COMPONENT"):
|
||||
in_data = True
|
||||
continue
|
||||
if not in_data:
|
||||
continue
|
||||
if (line == "" or line.startswith("SUMMARY") or line.startswith("---")
|
||||
if (line == "" or line.startswith("SUMMARY") or BOX_BORDER_RE.match(msg)
|
||||
or line.startswith("ERROR") or line.startswith("WARNING")):
|
||||
in_data = False
|
||||
continue
|
||||
@@ -261,14 +295,18 @@ class TestPrechecksHappyPath:
|
||||
LOGGER.debug(f"[happy-path] stdout:\n{stdout}")
|
||||
LOGGER.debug(f"[happy-path] log_lines:\n{log_lines}")
|
||||
|
||||
# Strip zerolog console prefixes so pattern matching works on raw messages.
|
||||
messages = "\n".join(_strip_log_prefix(l) for l in stdout.splitlines())
|
||||
|
||||
# ── 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, \
|
||||
# zerolog writes to stderr which is merged into stdout by the harness.
|
||||
# The table is wrapped in an ASCII box by cliutil.LogTable.
|
||||
assert BOX_TITLE in messages, \
|
||||
f"Expected box title '{BOX_TITLE}' in output;\ngot:\n{stdout}"
|
||||
assert COL_HEADER in messages, \
|
||||
f"Expected column header row in output;\ngot:\n{stdout}"
|
||||
assert SEPARATOR in stdout, \
|
||||
f"Expected closing separator in output;\ngot:\n{stdout}"
|
||||
assert BOX_BORDER_RE.search(messages), \
|
||||
f"Expected box border line (+---+) in output;\ngot:\n{stdout}"
|
||||
|
||||
# ── row content ──────────────────────────────────────────────────────
|
||||
rows = _parse_table(stdout)
|
||||
@@ -306,11 +344,11 @@ class TestPrechecksHappyPath:
|
||||
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}"
|
||||
assert PREFIX_ERROR not in messages, f"Unexpected ERROR action:\n{stdout}"
|
||||
assert PREFIX_WARNING not in messages, f"Unexpected WARNING action:\n{stdout}"
|
||||
|
||||
# ── exact summary line ───────────────────────────────────────────────
|
||||
assert SUMMARY_HEALTHY in stdout, \
|
||||
# ── summary line ─────────────────────────────────────────────────────
|
||||
assert SUMMARY_HEALTHY in messages, \
|
||||
f"Expected healthy summary;\ngot:\n{stdout}"
|
||||
|
||||
# ── structured log ───────────────────────────────────────────────────
|
||||
@@ -366,14 +404,18 @@ class TestPrechecksHardFail:
|
||||
LOGGER.debug(f"[hard-fail] stdout:\n{stdout}")
|
||||
LOGGER.debug(f"[hard-fail] log_lines:\n{log_lines}")
|
||||
|
||||
# Strip zerolog console prefixes so pattern matching works on raw messages.
|
||||
messages = "\n".join(_strip_log_prefix(l) for l in stdout.splitlines())
|
||||
|
||||
# ── 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, \
|
||||
# zerolog writes to stderr which is merged into stdout by the harness.
|
||||
# The table is wrapped in an ASCII box by cliutil.LogTable.
|
||||
assert BOX_TITLE in messages, \
|
||||
f"Expected box title '{BOX_TITLE}' in output;\ngot:\n{stdout}"
|
||||
assert COL_HEADER in messages, \
|
||||
f"Expected column header row in output;\ngot:\n{stdout}"
|
||||
assert SEPARATOR in stdout, \
|
||||
f"Expected closing separator in output;\ngot:\n{stdout}"
|
||||
assert BOX_BORDER_RE.search(messages), \
|
||||
f"Expected box border line (+---+) in output;\ngot:\n{stdout}"
|
||||
|
||||
# ── row content ──────────────────────────────────────────────────────
|
||||
rows = _parse_table(stdout)
|
||||
@@ -404,12 +446,12 @@ class TestPrechecksHardFail:
|
||||
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, \
|
||||
assert f"{PREFIX_ERROR}{ACTION_QUIC_BLOCKED}" in messages, \
|
||||
f"Expected QUIC ERROR action;\ngot:\n{stdout}"
|
||||
assert f"{PREFIX_ERROR}{ACTION_HTTP2_BLOCKED}" in stdout, \
|
||||
assert f"{PREFIX_ERROR}{ACTION_HTTP2_BLOCKED}" in messages, \
|
||||
f"Expected HTTP/2 ERROR action;\ngot:\n{stdout}"
|
||||
|
||||
assert SUMMARY_CRITICAL in stdout, \
|
||||
assert SUMMARY_CRITICAL in messages, \
|
||||
f"Expected critical summary;\ngot:\n{stdout}"
|
||||
|
||||
_assert_precheck_summary_log(log_lines, hard_fail=True, suggested_protocol=None)
|
||||
|
||||
+3
-18
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection/dialopts"
|
||||
cfdquic "github.com/cloudflare/cloudflared/quic"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -29,7 +30,7 @@ func DialQuic(
|
||||
connIndex uint8,
|
||||
logger *zerolog.Logger,
|
||||
opts dialopts.DialOpts,
|
||||
) (quic.Connection, error) {
|
||||
) (cfdquic.QUICConnection, error) {
|
||||
udpConn, err := createUDPConnForConnIndex(connIndex, localAddr, edgeAddr, opts, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -43,11 +44,7 @@ func DialQuic(
|
||||
}
|
||||
|
||||
// wrap the session, so that the UDPConn is closed after session is closed.
|
||||
conn = &wrapCloseableConnQuicConnection{
|
||||
conn,
|
||||
udpConn,
|
||||
}
|
||||
return conn, nil
|
||||
return cfdquic.NewQUICConnection(conn, udpConn)
|
||||
}
|
||||
|
||||
func createUDPConnForConnIndex(connIndex uint8, localIP net.IP, edgeIP netip.AddrPort, opts dialopts.DialOpts, logger *zerolog.Logger) (*net.UDPConn, error) {
|
||||
@@ -96,15 +93,3 @@ func createUDPConnForConnIndex(connIndex uint8, localIP net.IP, edgeIP netip.Add
|
||||
|
||||
return udpConn, err
|
||||
}
|
||||
|
||||
type wrapCloseableConnQuicConnection struct {
|
||||
quic.Connection
|
||||
udpConn *net.UDPConn
|
||||
}
|
||||
|
||||
func (w *wrapCloseableConnQuicConnection) CloseWithError(errorCode quic.ApplicationErrorCode, reason string) error {
|
||||
err := w.Connection.CloseWithError(errorCode, reason)
|
||||
_ = w.udpConn.Close()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ const (
|
||||
|
||||
// quicConnection represents the type that facilitates Proxying via QUIC streams.
|
||||
type quicConnection struct {
|
||||
conn quic.Connection
|
||||
conn cfdquic.QUICConnection
|
||||
logger *zerolog.Logger
|
||||
orchestrator Orchestrator
|
||||
datagramHandler DatagramSessionHandler
|
||||
@@ -54,10 +54,10 @@ type quicConnection struct {
|
||||
gracePeriod time.Duration
|
||||
}
|
||||
|
||||
// NewTunnelConnection takes a [quic.Connection] to wrap it for use with cloudflared application logic.
|
||||
// NewTunnelConnection takes a [cfdquic.QUICConnection] to wrap it for use with cloudflared application logic.
|
||||
func NewTunnelConnection(
|
||||
ctx context.Context,
|
||||
conn quic.Connection,
|
||||
conn cfdquic.QUICConnection,
|
||||
connIndex uint8,
|
||||
orchestrator Orchestrator,
|
||||
datagramSessionHandler DatagramSessionHandler,
|
||||
@@ -169,7 +169,7 @@ func (q *quicConnection) acceptStream(ctx context.Context) error {
|
||||
func (q *quicConnection) runStream(quicStream quic.Stream) {
|
||||
ctx := quicStream.Context()
|
||||
stream := cfdquic.NewSafeStreamCloser(quicStream, q.streamWriteTimeout, q.logger)
|
||||
defer stream.Close()
|
||||
defer func() { _ = stream.Close() }()
|
||||
|
||||
// we are going to fuse readers/writers from stream <- cloudflared -> origin, and we want to guarantee that
|
||||
// code executed in the code path of handleStream don't trigger an earlier close to the downstream write stream.
|
||||
|
||||
@@ -8,9 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
pkgerrors "github.com/pkg/errors"
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/rs/zerolog"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@@ -24,7 +22,6 @@ import (
|
||||
"github.com/cloudflare/cloudflared/packet"
|
||||
cfdquic "github.com/cloudflare/cloudflared/quic"
|
||||
"github.com/cloudflare/cloudflared/tracing"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
rpcquic "github.com/cloudflare/cloudflared/tunnelrpc/quic"
|
||||
)
|
||||
@@ -34,20 +31,18 @@ const (
|
||||
demuxChanCapacity = 16
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidDestinationIP = errors.New("unable to parse destination IP")
|
||||
)
|
||||
var errInvalidDestinationIP = pkgerrors.New("unable to parse destination IP")
|
||||
|
||||
// DatagramSessionHandler is a service that can serve datagrams for a connection and handle sessions from incoming
|
||||
// connection streams.
|
||||
type DatagramSessionHandler interface {
|
||||
Serve(context.Context) error
|
||||
|
||||
pogs.SessionManager
|
||||
tunnelpogs.SessionManager
|
||||
}
|
||||
|
||||
type datagramV2Connection struct {
|
||||
conn quic.Connection
|
||||
conn cfdquic.QUICConnection
|
||||
index uint8
|
||||
|
||||
// sessionManager tracks active sessions. It receives datagrams from quic connection via datagramMuxer
|
||||
@@ -69,7 +64,7 @@ type datagramV2Connection struct {
|
||||
}
|
||||
|
||||
func NewDatagramV2Connection(ctx context.Context,
|
||||
conn quic.Connection,
|
||||
conn cfdquic.QUICConnection,
|
||||
originDialer ingress.OriginUDPDialer,
|
||||
icmpRouter ingress.ICMPRouter,
|
||||
index uint8,
|
||||
@@ -166,7 +161,7 @@ func (q *datagramV2Connection) RegisterUdpSession(ctx context.Context, sessionID
|
||||
|
||||
session, err := q.sessionManager.RegisterSession(ctx, sessionID, originProxy)
|
||||
if err != nil {
|
||||
originProxy.Close()
|
||||
_ = originProxy.Close()
|
||||
log.Err(err).Str(datagramsession.LogFieldSessionID, datagramsession.FormatSessionID(sessionID)).Msgf("Failed to register udp session")
|
||||
tracing.EndWithErrorStatus(registerSpan, err)
|
||||
q.flowLimiter.Release()
|
||||
@@ -229,7 +224,7 @@ func (q *datagramV2Connection) closeUDPSession(ctx context.Context, sessionID uu
|
||||
}
|
||||
|
||||
stream := cfdquic.NewSafeStreamCloser(quicStream, q.streamWriteTimeout, q.logger)
|
||||
defer stream.Close()
|
||||
defer func() { _ = stream.Close() }()
|
||||
rpcClientStream, err := rpcquic.NewSessionClient(ctx, stream, q.rpcTimeout)
|
||||
if err != nil {
|
||||
// Log this at debug because this is not an error if session was closed due to lost connection
|
||||
|
||||
@@ -7,12 +7,12 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/ingress"
|
||||
"github.com/cloudflare/cloudflared/management"
|
||||
cfdquic "github.com/cloudflare/cloudflared/quic/v3"
|
||||
cfdquic "github.com/cloudflare/cloudflared/quic"
|
||||
cfdquicv3 "github.com/cloudflare/cloudflared/quic/v3"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
@@ -22,20 +22,20 @@ var (
|
||||
)
|
||||
|
||||
type datagramV3Connection struct {
|
||||
conn quic.Connection
|
||||
conn cfdquic.QUICConnection
|
||||
index uint8
|
||||
// datagramMuxer mux/demux datagrams from quic connection
|
||||
datagramMuxer cfdquic.DatagramConn
|
||||
metrics cfdquic.Metrics
|
||||
datagramMuxer cfdquicv3.DatagramConn
|
||||
metrics cfdquicv3.Metrics
|
||||
logger *zerolog.Logger
|
||||
}
|
||||
|
||||
func NewDatagramV3Connection(ctx context.Context,
|
||||
conn quic.Connection,
|
||||
sessionManager cfdquic.SessionManager,
|
||||
conn cfdquic.QUICConnection,
|
||||
sessionManager cfdquicv3.SessionManager,
|
||||
icmpRouter ingress.ICMPRouter,
|
||||
index uint8,
|
||||
metrics cfdquic.Metrics,
|
||||
metrics cfdquicv3.Metrics,
|
||||
logger *zerolog.Logger,
|
||||
) DatagramSessionHandler {
|
||||
log := logger.
|
||||
@@ -43,7 +43,7 @@ func NewDatagramV3Connection(ctx context.Context,
|
||||
Int(management.EventTypeKey, int(management.UDP)).
|
||||
Uint8(LogFieldConnIndex, index).
|
||||
Logger()
|
||||
datagramMuxer := cfdquic.NewDatagramConn(conn, sessionManager, icmpRouter, index, metrics, &log)
|
||||
datagramMuxer := cfdquicv3.NewDatagramConn(conn, sessionManager, icmpRouter, index, metrics, &log)
|
||||
|
||||
return &datagramV3Connection{
|
||||
conn,
|
||||
|
||||
@@ -17,12 +17,13 @@ import (
|
||||
reflect "reflect"
|
||||
time "time"
|
||||
|
||||
quic "github.com/quic-go/quic-go"
|
||||
quic0 "github.com/quic-go/quic-go"
|
||||
zerolog "github.com/rs/zerolog"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
|
||||
dialopts "github.com/cloudflare/cloudflared/connection/dialopts"
|
||||
allregions "github.com/cloudflare/cloudflared/edgediscovery/allregions"
|
||||
quic "github.com/cloudflare/cloudflared/quic"
|
||||
)
|
||||
|
||||
// MockDNSResolver is a mock of DNSResolver interface.
|
||||
@@ -176,10 +177,10 @@ func (m *MockQUICDialer) EXPECT() *MockQUICDialerMockRecorder {
|
||||
}
|
||||
|
||||
// DialQuic mocks base method.
|
||||
func (m *MockQUICDialer) DialQuic(ctx context.Context, quicConfig *quic.Config, tlsConfig *tls.Config, addr netip.AddrPort, localAddr net.IP, connIndex uint8, logger *zerolog.Logger, opts dialopts.DialOpts) (quic.Connection, error) {
|
||||
func (m *MockQUICDialer) DialQuic(ctx context.Context, quicConfig *quic0.Config, tlsConfig *tls.Config, addr netip.AddrPort, localAddr net.IP, connIndex uint8, logger *zerolog.Logger, opts dialopts.DialOpts) (quic.QUICConnection, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DialQuic", ctx, quicConfig, tlsConfig, addr, localAddr, connIndex, logger, opts)
|
||||
ret0, _ := ret[0].(quic.Connection)
|
||||
ret0, _ := ret[0].(quic.QUICConnection)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
@@ -197,19 +198,19 @@ type MockQUICDialerDialQuicCall struct {
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockQUICDialerDialQuicCall) Return(arg0 quic.Connection, arg1 error) *MockQUICDialerDialQuicCall {
|
||||
func (c *MockQUICDialerDialQuicCall) Return(arg0 quic.QUICConnection, arg1 error) *MockQUICDialerDialQuicCall {
|
||||
c.Call = c.Call.Return(arg0, arg1)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockQUICDialerDialQuicCall) Do(f func(context.Context, *quic.Config, *tls.Config, netip.AddrPort, net.IP, uint8, *zerolog.Logger, dialopts.DialOpts) (quic.Connection, error)) *MockQUICDialerDialQuicCall {
|
||||
func (c *MockQUICDialerDialQuicCall) Do(f func(context.Context, *quic0.Config, *tls.Config, netip.AddrPort, net.IP, uint8, *zerolog.Logger, dialopts.DialOpts) (quic.QUICConnection, error)) *MockQUICDialerDialQuicCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockQUICDialerDialQuicCall) DoAndReturn(f func(context.Context, *quic.Config, *tls.Config, netip.AddrPort, net.IP, uint8, *zerolog.Logger, dialopts.DialOpts) (quic.Connection, error)) *MockQUICDialerDialQuicCall {
|
||||
func (c *MockQUICDialerDialQuicCall) DoAndReturn(f func(context.Context, *quic0.Config, *tls.Config, netip.AddrPort, net.IP, uint8, *zerolog.Logger, dialopts.DialOpts) (quic.QUICConnection, error)) *MockQUICDialerDialQuicCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
+2
-1
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
edgedial "github.com/cloudflare/cloudflared/edgediscovery"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
|
||||
cfdquic "github.com/cloudflare/cloudflared/quic"
|
||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||
)
|
||||
|
||||
@@ -96,7 +97,7 @@ func (d *EdgeQUICDialer) DialQuic(
|
||||
connIndex uint8,
|
||||
logger *zerolog.Logger,
|
||||
opts dialopts.DialOpts,
|
||||
) (quic.Connection, error) {
|
||||
) (cfdquic.QUICConnection, error) {
|
||||
return connection.DialQuic(ctx, quicConfig, tlsConfig, addr, localAddr, connIndex, logger, opts)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection/dialopts"
|
||||
cfdquic "github.com/cloudflare/cloudflared/quic"
|
||||
|
||||
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
|
||||
)
|
||||
@@ -44,7 +45,7 @@ type QUICDialer interface {
|
||||
connIndex uint8,
|
||||
logger *zerolog.Logger,
|
||||
opts dialopts.DialOpts,
|
||||
) (quic.Connection, error)
|
||||
) (cfdquic.QUICConnection, error)
|
||||
}
|
||||
|
||||
// ManagementDialer abstracts the TCP dial to api.cloudflare.com:443 used by
|
||||
|
||||
+14
-51
@@ -10,19 +10,12 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// tableWidth is the total character width of the separator lines.
|
||||
tableWidth = 80
|
||||
|
||||
// Status names.
|
||||
passStatus = "PASS"
|
||||
failStatus = "FAIL"
|
||||
skipStatus = "SKIP"
|
||||
unknownStatus = "UNKNOWN"
|
||||
|
||||
// Section separators.
|
||||
sectionChar = "-"
|
||||
headerTitle = "CONNECTIVITY PRE-CHECKS"
|
||||
|
||||
// Log message constants.
|
||||
logMsgPrecheck = "precheck"
|
||||
logMsgPrecheckComplete = "precheck complete"
|
||||
@@ -35,8 +28,6 @@ const (
|
||||
logFieldDetails = "details"
|
||||
logFieldHardFail = "hard_fail"
|
||||
logFieldSuggestedProtocol = "suggested_protocol"
|
||||
|
||||
sep = " "
|
||||
)
|
||||
|
||||
// statusLabel returns the display label for a given Status.
|
||||
@@ -58,21 +49,9 @@ func (s Status) logString() string {
|
||||
return strings.ToLower(s.String())
|
||||
}
|
||||
|
||||
// separator returns a full-width horizontal line.
|
||||
func separator() string {
|
||||
return strings.Repeat(sectionChar, tableWidth)
|
||||
}
|
||||
|
||||
// header returns the top section title line.
|
||||
func header() string {
|
||||
leftDashes := strings.Repeat(sectionChar, 3)
|
||||
rightLen := tableWidth - len(leftDashes) - len(headerTitle) - len(sep)*2
|
||||
return leftDashes + sep + headerTitle + sep + strings.Repeat(sectionChar, rightLen)
|
||||
}
|
||||
|
||||
// renderTable uses text/tabwriter to format the results rows with
|
||||
// automatically aligned columns, returning the rendered string.
|
||||
func renderTable(results []CheckResult) string {
|
||||
// automatically aligned columns, returning the rendered lines.
|
||||
func renderTable(results []CheckResult) []string {
|
||||
var buf bytes.Buffer
|
||||
// minwidth=0, tabwidth=8, padding=2, padchar=' ', flags=0
|
||||
w := tabwriter.NewWriter(&buf, 0, 8, 2, ' ', 0)
|
||||
@@ -81,27 +60,27 @@ func renderTable(results []CheckResult) string {
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", r.Component, r.Target, r.ProbeStatus.statusLabel(), r.Details)
|
||||
}
|
||||
_ = w.Flush()
|
||||
return buf.String()
|
||||
return strings.Split(strings.TrimSuffix(buf.String(), "\n"), "\n")
|
||||
}
|
||||
|
||||
// renderActions collects all non-empty Action strings from results and returns
|
||||
// the formatted warning/error block that appears between the table and SUMMARY.
|
||||
// A Fail result is rendered as ERROR when the report is a hard fail, and as
|
||||
// WARNING otherwise (degraded but tunnel can still run).
|
||||
func renderActions(r Report) string {
|
||||
func renderActions(r Report) []string {
|
||||
hardFail := r.hasHardFail()
|
||||
var sb strings.Builder
|
||||
actions := make([]string, 0)
|
||||
for _, res := range r.Results {
|
||||
if res.Action == "" || res.ProbeStatus != Fail {
|
||||
continue
|
||||
}
|
||||
if hardFail {
|
||||
_, _ = fmt.Fprintf(&sb, "ERROR: %s\n", res.Action)
|
||||
actions = append(actions, fmt.Sprintf("ERROR: %s", res.Action))
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(&sb, "WARNING: %s\n", res.Action)
|
||||
actions = append(actions, fmt.Sprintf("WARNING: %s", res.Action))
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
return actions
|
||||
}
|
||||
|
||||
// summaryLine builds the SUMMARY: line based on the Report state.
|
||||
@@ -181,28 +160,12 @@ func (r Report) hasWarn() bool {
|
||||
return (quicFail != http2Fail) || apiFail
|
||||
}
|
||||
|
||||
// String renders the Report as a human-readable table suitable for os.Stdout.
|
||||
func (r Report) String() string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(header())
|
||||
sb.WriteString("\n")
|
||||
|
||||
sb.WriteString(renderTable(r.Results))
|
||||
|
||||
actions := renderActions(r)
|
||||
if actions != "" {
|
||||
sb.WriteString(actions)
|
||||
}
|
||||
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString(summaryLine(r))
|
||||
sb.WriteString("\n")
|
||||
|
||||
sb.WriteString(separator())
|
||||
sb.WriteString("\n")
|
||||
|
||||
return sb.String()
|
||||
// String renders the Report as human-readable table lines suitable for logging.
|
||||
func (r Report) String() []string {
|
||||
lines := renderTable(r.Results)
|
||||
lines = append(lines, renderActions(r)...)
|
||||
lines = append(lines, "", summaryLine(r))
|
||||
return lines
|
||||
}
|
||||
|
||||
// LogEvent emits each CheckResult as a structured zerolog log line, followed by
|
||||
|
||||
+57
-62
@@ -134,85 +134,80 @@ func dnsFailReport() Report {
|
||||
|
||||
func TestString_AllPass(t *testing.T) {
|
||||
t.Parallel()
|
||||
want := "" +
|
||||
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
|
||||
"COMPONENT TARGET STATUS DETAILS\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"
|
||||
want := []string{
|
||||
"COMPONENT TARGET STATUS DETAILS",
|
||||
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully",
|
||||
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully",
|
||||
"UDP Connectivity Port 7844 (QUIC) PASS QUIC connection successful",
|
||||
"TCP Connectivity Port 7844 (HTTP/2) PASS HTTP/2 connection successful",
|
||||
"Cloudflare API api.cloudflare.com:443 PASS API is reachable",
|
||||
"",
|
||||
"SUMMARY: Environment is healthy. cloudflared will use 'quic' as primary protocol.",
|
||||
}
|
||||
assert.Equal(t, want, allPassReport().String())
|
||||
}
|
||||
|
||||
func TestString_QuicBlocked(t *testing.T) {
|
||||
t.Parallel()
|
||||
want := "" +
|
||||
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
|
||||
"COMPONENT TARGET STATUS DETAILS\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"
|
||||
want := []string{
|
||||
"COMPONENT TARGET STATUS DETAILS",
|
||||
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully",
|
||||
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully",
|
||||
"UDP Connectivity Port 7844 (QUIC) FAIL QUIC connection failed",
|
||||
"TCP Connectivity Port 7844 (HTTP/2) PASS HTTP/2 connection successful",
|
||||
"Cloudflare API api.cloudflare.com:443 PASS API is reachable",
|
||||
"WARNING: Allow outbound QUIC traffic on port 7844 or use HTTP2.",
|
||||
"",
|
||||
"SUMMARY: Environment ready with degraded transport. cloudflared will proceed using 'http2'.",
|
||||
}
|
||||
assert.Equal(t, want, quicBlockedReport().String())
|
||||
}
|
||||
|
||||
func TestString_APIFail(t *testing.T) {
|
||||
t.Parallel()
|
||||
want := "" +
|
||||
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
|
||||
"COMPONENT TARGET STATUS DETAILS\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" +
|
||||
"SUMMARY: Environment ready with degraded transport. cloudflared will proceed using 'quic'.\n" +
|
||||
"--------------------------------------------------------------------------------\n"
|
||||
want := []string{
|
||||
"COMPONENT TARGET STATUS DETAILS",
|
||||
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully",
|
||||
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully",
|
||||
"UDP Connectivity Port 7844 (QUIC) PASS QUIC connection successful",
|
||||
"TCP Connectivity Port 7844 (HTTP/2) PASS HTTP/2 connection successful",
|
||||
"Cloudflare API api.cloudflare.com:443 FAIL Connection refused",
|
||||
"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.",
|
||||
"",
|
||||
"SUMMARY: Environment ready with degraded transport. cloudflared will proceed using 'quic'.",
|
||||
}
|
||||
assert.Equal(t, want, apiFailReport().String())
|
||||
}
|
||||
|
||||
func TestString_BothTransportsBlocked(t *testing.T) {
|
||||
t.Parallel()
|
||||
want := "" +
|
||||
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
|
||||
"COMPONENT TARGET STATUS DETAILS\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" +
|
||||
"--------------------------------------------------------------------------------\n"
|
||||
want := []string{
|
||||
"COMPONENT TARGET STATUS DETAILS",
|
||||
"DNS Resolution region1.v2.argotunnel.com PASS DNS Resolved successfully",
|
||||
"DNS Resolution region2.v2.argotunnel.com PASS DNS Resolved successfully",
|
||||
"UDP Connectivity Port 7844 (QUIC) FAIL QUIC connection failed",
|
||||
"TCP Connectivity Port 7844 (HTTP/2) FAIL HTTP/2 connection is blocked or unreachable",
|
||||
"Cloudflare API api.cloudflare.com:443 PASS API is reachable",
|
||||
"ERROR: Allow outbound QUIC and/or TCP on port 7844 to the Cloudflare edge.",
|
||||
"",
|
||||
"SUMMARY: Environment has critical failures. cloudflared may not be able to establish a tunnel.",
|
||||
}
|
||||
assert.Equal(t, want, bothTransportsBlockedReport().String())
|
||||
}
|
||||
|
||||
func TestString_DNSFail(t *testing.T) {
|
||||
t.Parallel()
|
||||
want := "" +
|
||||
"--- CONNECTIVITY PRE-CHECKS ----------------------------------------------------\n" +
|
||||
"COMPONENT TARGET STATUS DETAILS\n" +
|
||||
"DNS Resolution region1.v2.argotunnel.com FAIL No addresses returned\n" +
|
||||
"DNS Resolution region2.v2.argotunnel.com FAIL No addresses returned\n" +
|
||||
"UDP Connectivity Port 7844 (QUIC) SKIP DNS prerequisite failed\n" +
|
||||
"TCP Connectivity Port 7844 (HTTP/2) SKIP DNS prerequisite failed\n" +
|
||||
"Cloudflare API api.cloudflare.com:443 FAIL Connection refused\n" +
|
||||
"ERROR: Ensure your DNS resolver can resolve 'region1.v2.argotunnel.com'. Run: dig A region1.v2.argotunnel.com @1.1.1.1. If that fails, contact your network administrator.\n" +
|
||||
"\n" +
|
||||
"SUMMARY: Environment has critical failures. cloudflared may not be able to establish a tunnel.\n" +
|
||||
"--------------------------------------------------------------------------------\n"
|
||||
want := []string{
|
||||
"COMPONENT TARGET STATUS DETAILS",
|
||||
"DNS Resolution region1.v2.argotunnel.com FAIL No addresses returned",
|
||||
"DNS Resolution region2.v2.argotunnel.com FAIL No addresses returned",
|
||||
"UDP Connectivity Port 7844 (QUIC) SKIP DNS prerequisite failed",
|
||||
"TCP Connectivity Port 7844 (HTTP/2) SKIP DNS prerequisite failed",
|
||||
"Cloudflare API api.cloudflare.com:443 FAIL Connection refused",
|
||||
"ERROR: Ensure your DNS resolver can resolve 'region1.v2.argotunnel.com'. Run: dig A region1.v2.argotunnel.com @1.1.1.1. If that fails, contact your network administrator.",
|
||||
"",
|
||||
"SUMMARY: Environment has critical failures. cloudflared may not be able to establish a tunnel.",
|
||||
}
|
||||
assert.Equal(t, want, dnsFailReport().String())
|
||||
}
|
||||
|
||||
@@ -221,9 +216,9 @@ func TestString_EmptyResults(t *testing.T) {
|
||||
r := Report{RunID: fixedRunID, SuggestedProtocol: new(connection.QUIC)}
|
||||
out := r.String()
|
||||
// Must not panic and must still emit a valid skeleton.
|
||||
assert.Contains(t, out, "CONNECTIVITY PRE-CHECKS")
|
||||
assert.Contains(t, out, "SUMMARY:")
|
||||
assert.Contains(t, out, separator())
|
||||
require.Len(t, out, 3)
|
||||
assert.Contains(t, out[0], "COMPONENT")
|
||||
assert.Contains(t, out[2], "SUMMARY:")
|
||||
}
|
||||
|
||||
// LogEvent() / structured log renderer tests
|
||||
|
||||
+10
-6
@@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/cloudflare/cloudflared/packet"
|
||||
@@ -51,14 +50,14 @@ func (dm *DatagramMuxerV2) mtu() int {
|
||||
}
|
||||
|
||||
type DatagramMuxerV2 struct {
|
||||
session quic.Connection
|
||||
session QUICConnection
|
||||
logger *zerolog.Logger
|
||||
sessionDemuxChan chan<- *packet.Session
|
||||
packetDemuxChan chan Packet
|
||||
}
|
||||
|
||||
func NewDatagramMuxerV2(
|
||||
quicSession quic.Connection,
|
||||
quicSession QUICConnection,
|
||||
log *zerolog.Logger,
|
||||
sessionDemuxChan chan<- *packet.Session,
|
||||
) *DatagramMuxerV2 {
|
||||
@@ -110,7 +109,8 @@ func (dm *DatagramMuxerV2) SendPacket(pk Packet) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Demux reads datagrams from the QUIC connection and demuxes depending on whether it's a session or packet
|
||||
// ServeReceive reads datagrams from the QUIC connection and demuxes them
|
||||
// depending on whether it's a session or packet
|
||||
func (dm *DatagramMuxerV2) ServeReceive(ctx context.Context) error {
|
||||
for {
|
||||
msg, err := dm.session.ReceiveDatagram(ctx)
|
||||
@@ -144,8 +144,10 @@ func (dm *DatagramMuxerV2) demux(ctx context.Context, msgWithType []byte) error
|
||||
switch msgType {
|
||||
case DatagramTypeUDP:
|
||||
return dm.handleSession(ctx, msg)
|
||||
default:
|
||||
case DatagramTypeIP, DatagramTypeIPWithTrace, DatagramTypeTracingSpan:
|
||||
return dm.handlePacket(ctx, msg, msgType)
|
||||
default:
|
||||
return fmt.Errorf("unexpected datagram type %d", msgType)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,8 +191,10 @@ func (dm *DatagramMuxerV2) handlePacket(ctx context.Context, pk []byte, msgType
|
||||
Spans: spans,
|
||||
TracingIdentity: tracingIdentity,
|
||||
}
|
||||
case DatagramTypeUDP:
|
||||
return fmt.Errorf("unexpected datagram type %d in handlePacket", msgType)
|
||||
default:
|
||||
return fmt.Errorf("Unexpected datagram type %d", msgType)
|
||||
return fmt.Errorf("unexpected datagram type %d", msgType)
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package quic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
)
|
||||
|
||||
// QUICConnection defines the subset of [quic.Connection] methods used by cloudflared.
|
||||
// Consumers should accept this interface; producers should return [*ConnWithCloser].
|
||||
type QUICConnection interface {
|
||||
AcceptStream(ctx context.Context) (quic.Stream, error)
|
||||
OpenStream() (quic.Stream, error)
|
||||
OpenStreamSync(ctx context.Context) (quic.Stream, error)
|
||||
CloseWithError(code quic.ApplicationErrorCode, reason string) error
|
||||
Context() context.Context
|
||||
SendDatagram(payload []byte) error
|
||||
ReceiveDatagram(ctx context.Context) ([]byte, error)
|
||||
LocalAddr() net.Addr
|
||||
RemoteAddr() net.Addr
|
||||
ConnectionState() quic.ConnectionState
|
||||
}
|
||||
|
||||
// Compile-time assertion that *ConnWithCloser implements QUICConnection.
|
||||
var _ QUICConnection = (*ConnWithCloser)(nil)
|
||||
|
||||
var (
|
||||
// error returned when the [NewConnWithCloser] is called with a nil conn argument
|
||||
ErrNilQuicConnection = errors.New("the provided quic connection is nil")
|
||||
// error returned when the [NewConnWithCloser] is called with a nil closer argument
|
||||
ErrNilCloser = errors.New("the provided closer is nil")
|
||||
)
|
||||
|
||||
// ConnWithCloser wraps a [quic.Connection] and an [io.Closer] (typically the
|
||||
// underlying [*net.UDPConn]). When [CloseWithError] is called the QUIC
|
||||
// connection is closed first, then the closer is closed deterministically.
|
||||
//
|
||||
// A nil conn is only safe for [CloseWithError] (used in tests). All other
|
||||
// delegated methods will panic on a nil conn.
|
||||
type ConnWithCloser struct {
|
||||
conn quic.Connection
|
||||
closer io.Closer
|
||||
}
|
||||
|
||||
// NewQUICConnection returns a [*ConnWithCloser] that will close closer after
|
||||
// the QUIC connection is closed.
|
||||
func NewQUICConnection(conn quic.Connection, closer io.Closer) (*ConnWithCloser, error) {
|
||||
if conn == nil {
|
||||
return nil, ErrNilQuicConnection
|
||||
}
|
||||
|
||||
if closer == nil {
|
||||
return nil, ErrNilCloser
|
||||
}
|
||||
return &ConnWithCloser{conn: conn, closer: closer}, nil
|
||||
}
|
||||
|
||||
// CloseWithError closes the QUIC connection and then closes the underlying
|
||||
// [io.Closer]. If both operations return errors, the errors are joined so that
|
||||
// the closer error is no longer silently discarded.
|
||||
func (c *ConnWithCloser) CloseWithError(code quic.ApplicationErrorCode, reason string) error {
|
||||
connErr := c.conn.CloseWithError(code, reason)
|
||||
closerErr := c.closer.Close()
|
||||
|
||||
return errors.Join(connErr, closerErr)
|
||||
}
|
||||
|
||||
func (c *ConnWithCloser) AcceptStream(ctx context.Context) (quic.Stream, error) {
|
||||
return c.conn.AcceptStream(ctx)
|
||||
}
|
||||
|
||||
func (c *ConnWithCloser) OpenStream() (quic.Stream, error) {
|
||||
return c.conn.OpenStream()
|
||||
}
|
||||
|
||||
func (c *ConnWithCloser) OpenStreamSync(ctx context.Context) (quic.Stream, error) {
|
||||
return c.conn.OpenStreamSync(ctx)
|
||||
}
|
||||
|
||||
func (c *ConnWithCloser) Context() context.Context {
|
||||
return c.conn.Context()
|
||||
}
|
||||
|
||||
func (c *ConnWithCloser) SendDatagram(payload []byte) error {
|
||||
return c.conn.SendDatagram(payload)
|
||||
}
|
||||
|
||||
func (c *ConnWithCloser) ReceiveDatagram(ctx context.Context) ([]byte, error) {
|
||||
return c.conn.ReceiveDatagram(ctx)
|
||||
}
|
||||
|
||||
func (c *ConnWithCloser) LocalAddr() net.Addr {
|
||||
return c.conn.LocalAddr()
|
||||
}
|
||||
|
||||
func (c *ConnWithCloser) RemoteAddr() net.Addr {
|
||||
return c.conn.RemoteAddr()
|
||||
}
|
||||
|
||||
func (c *ConnWithCloser) ConnectionState() quic.ConnectionState {
|
||||
return c.conn.ConnectionState()
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package quic
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// mockCloser is an [io.Closer] that returns a configurable error.
|
||||
type mockCloser struct {
|
||||
closeErr error
|
||||
}
|
||||
|
||||
func (m *mockCloser) Close() error {
|
||||
return m.closeErr
|
||||
}
|
||||
|
||||
// mockQuicConnection is a minimal test double for [quic.Connection].
|
||||
type mockQuicConnection struct {
|
||||
quic.Connection
|
||||
closeWithErrorErr error
|
||||
}
|
||||
|
||||
func (m *mockQuicConnection) CloseWithError(_ quic.ApplicationErrorCode, _ string) error {
|
||||
return m.closeWithErrorErr
|
||||
}
|
||||
|
||||
func TestNewConnWithCloser_NilConn(t *testing.T) {
|
||||
t.Parallel()
|
||||
conn, err := NewQUICConnection(nil, &mockCloser{})
|
||||
require.ErrorIs(t, err, ErrNilQuicConnection)
|
||||
require.Nil(t, conn)
|
||||
}
|
||||
|
||||
func TestNewConnWithCloser_NilCloser(t *testing.T) {
|
||||
t.Parallel()
|
||||
conn, err := NewQUICConnection(&mockQuicConnection{}, nil)
|
||||
require.ErrorIs(t, err, ErrNilCloser)
|
||||
require.Nil(t, conn)
|
||||
}
|
||||
|
||||
func TestNewConnWithCloser_Success(t *testing.T) {
|
||||
t.Parallel()
|
||||
qc := &mockQuicConnection{}
|
||||
cl := &mockCloser{}
|
||||
conn, err := NewQUICConnection(qc, cl)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, conn)
|
||||
}
|
||||
|
||||
func TestConnWithCloser_CloseWithError_BothSucceed(t *testing.T) {
|
||||
t.Parallel()
|
||||
qc := &mockQuicConnection{}
|
||||
cl := &mockCloser{}
|
||||
conn, err := NewQUICConnection(qc, cl)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = conn.CloseWithError(0, "test")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestConnWithCloser_CloseWithError_QuicFails(t *testing.T) {
|
||||
t.Parallel()
|
||||
quicErr := errors.New("quic close failed")
|
||||
qc := &mockQuicConnection{closeWithErrorErr: quicErr}
|
||||
cl := &mockCloser{}
|
||||
conn, err := NewQUICConnection(qc, cl)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = conn.CloseWithError(0, "test")
|
||||
require.ErrorIs(t, err, quicErr)
|
||||
}
|
||||
|
||||
func TestConnWithCloser_CloseWithError_CloserFails(t *testing.T) {
|
||||
t.Parallel()
|
||||
closerErr := errors.New("closer failed")
|
||||
qc := &mockQuicConnection{}
|
||||
cl := &mockCloser{closeErr: closerErr}
|
||||
conn, err := NewQUICConnection(qc, cl)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = conn.CloseWithError(0, "test")
|
||||
require.ErrorIs(t, err, closerErr)
|
||||
}
|
||||
|
||||
func TestConnWithCloser_CloseWithError_BothFail(t *testing.T) {
|
||||
t.Parallel()
|
||||
quicErr := errors.New("quic close failed")
|
||||
closerErr := errors.New("closer failed")
|
||||
qc := &mockQuicConnection{closeWithErrorErr: quicErr}
|
||||
cl := &mockCloser{closeErr: closerErr}
|
||||
conn, err := NewQUICConnection(qc, cl)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = conn.CloseWithError(0, "test")
|
||||
require.ErrorIs(t, err, quicErr)
|
||||
require.ErrorIs(t, err, closerErr)
|
||||
}
|
||||
|
||||
// TestConnWithCloser_ImplementsInterface is a runtime assertion that
|
||||
// *ConnWithCloser satisfies QUICConnection. The compile-time assertion is in
|
||||
// quic_connection.go.
|
||||
func TestConnWithCloser_ImplementsInterface(t *testing.T) {
|
||||
t.Parallel()
|
||||
var _ QUICConnection = (*ConnWithCloser)(nil)
|
||||
}
|
||||
Reference in New Issue
Block a user