mirror of
https://github.com/cloudflare/cloudflared.git
synced 2026-06-23 04:10:20 +00:00
02eb75b56d
This adds back the quic-go bump.
731 lines
32 KiB
Go
731 lines
32 KiB
Go
package prechecks
|
||
|
||
import (
|
||
"errors"
|
||
"math"
|
||
"net"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/google/uuid"
|
||
"github.com/rs/zerolog"
|
||
"github.com/stretchr/testify/assert"
|
||
"github.com/stretchr/testify/require"
|
||
"go.uber.org/mock/gomock"
|
||
|
||
"github.com/cloudflare/cloudflared/connection"
|
||
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
|
||
"github.com/cloudflare/cloudflared/mocks"
|
||
)
|
||
|
||
const (
|
||
emptyCert = ""
|
||
)
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Helpers
|
||
// ---------------------------------------------------------------------------
|
||
|
||
// twoRegionAddrs returns a two-group [][]*EdgeAddr with one IPv4 address per
|
||
// region. Used by tests that only need to exercise the V4 path.
|
||
func twoRegionAddrs() [][]*allregions.EdgeAddr {
|
||
makeV4 := func(ip string) *allregions.EdgeAddr {
|
||
parsed := net.ParseIP(ip)
|
||
return &allregions.EdgeAddr{
|
||
TCP: &net.TCPAddr{IP: parsed, Port: 7844},
|
||
UDP: &net.UDPAddr{IP: parsed, Port: 7844},
|
||
IPVersion: allregions.V4,
|
||
}
|
||
}
|
||
return [][]*allregions.EdgeAddr{
|
||
{makeV4("1.2.3.4")},
|
||
{makeV4("5.6.7.8")},
|
||
}
|
||
}
|
||
|
||
// twoRegionAddrsBothFamilies returns a two-group [][]*EdgeAddr with one IPv4
|
||
// and one IPv6 address per region, used by per-family probe tests.
|
||
func twoRegionAddrsBothFamilies() [][]*allregions.EdgeAddr {
|
||
makeAddr := func(ip string, v allregions.EdgeIPVersion) *allregions.EdgeAddr {
|
||
parsed := net.ParseIP(ip)
|
||
return &allregions.EdgeAddr{
|
||
TCP: &net.TCPAddr{IP: parsed, Port: 7844},
|
||
UDP: &net.UDPAddr{IP: parsed, Port: 7844},
|
||
IPVersion: v,
|
||
}
|
||
}
|
||
return [][]*allregions.EdgeAddr{
|
||
{makeAddr("1.2.3.4", allregions.V4), makeAddr("2001:db8::1", allregions.V6)},
|
||
{makeAddr("5.6.7.8", allregions.V4), makeAddr("2001:db8::2", allregions.V6)},
|
||
}
|
||
}
|
||
|
||
// nopConn is a net.Conn whose Close() is a no-op, used as the success value
|
||
// for TCP and management dial mocks.
|
||
type nopConn struct{ net.Conn }
|
||
|
||
func (nopConn) Close() error { return nil }
|
||
|
||
// requireStatuses asserts the probe statuses in report.Results match
|
||
// expected (in order), failing immediately on length mismatch.
|
||
func requireStatuses(t *testing.T, report Report, expected ...Status) {
|
||
t.Helper()
|
||
require.Len(t, report.Results, len(expected))
|
||
for i, want := range expected {
|
||
got := report.Results[i].ProbeStatus
|
||
assert.Equalf(t, want, got,
|
||
"result[%d] (%s/%s): got %s, want %s",
|
||
i, report.Results[i].Component, report.Results[i].Target, got, want)
|
||
}
|
||
}
|
||
|
||
func nopLogger() *zerolog.Logger {
|
||
l := zerolog.Nop()
|
||
return &l
|
||
}
|
||
|
||
// newFakeQUICConn creates a mock QUIC connection with CloseWithError
|
||
// expectation pre-configured so gomock does not fail at runtime.
|
||
func newFakeQUICConn(ctrl *gomock.Controller) *mocks.MockQUICConnection {
|
||
conn := mocks.NewMockQUICConnection(ctrl)
|
||
conn.EXPECT().CloseWithError(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
|
||
return conn
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Tests
|
||
// ---------------------------------------------------------------------------
|
||
|
||
// TestRun_AllPass verifies that when all probes succeed the report contains
|
||
// 7 rows: 2 DNS + 2 QUIC (one per region) + 2 HTTP/2 (one per region) + 1 API.
|
||
func TestRun_AllPass(t *testing.T) {
|
||
t.Parallel()
|
||
ctrl := gomock.NewController(t)
|
||
|
||
dns := mocks.NewMockDNSResolver(ctrl)
|
||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||
fakeQUICConn := newFakeQUICConn(ctrl)
|
||
|
||
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrs(), nil)
|
||
// twoRegionAddrs has 2 regions × 1 V4 address each = 2 dials per transport.
|
||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nopConn{}, nil).Times(2)
|
||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(fakeQUICConn, nil).Times(2)
|
||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nopConn{}, nil)
|
||
|
||
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.Auto},
|
||
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||
|
||
// 2 DNS + 2 QUIC + 2 HTTP2 + 1 API = 7 results.
|
||
requireStatuses(t, report, Pass, Pass, Pass, Pass, Pass, Pass, Pass)
|
||
assert.NotEqual(t, uuid.Nil, report.RunID, "RunID must be set")
|
||
require.NotNil(t, report.SuggestedProtocol)
|
||
assert.Equal(t, connection.QUIC, *report.SuggestedProtocol)
|
||
assert.False(t, report.hasHardFail())
|
||
assert.False(t, report.hasWarn())
|
||
}
|
||
|
||
// TestRun_QUICBlocked verifies that when QUIC is blocked on all regions,
|
||
// the report is degraded (warn) and HTTP/2 is the suggested protocol.
|
||
func TestRun_QUICBlocked(t *testing.T) {
|
||
t.Parallel()
|
||
ctrl := gomock.NewController(t)
|
||
|
||
dns := mocks.NewMockDNSResolver(ctrl)
|
||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||
|
||
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrs(), nil)
|
||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nopConn{}, nil).AnyTimes()
|
||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nil, errors.New("connection refused")).AnyTimes()
|
||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nopConn{}, nil)
|
||
|
||
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.Auto},
|
||
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||
|
||
// 2 DNS Pass + 2 QUIC Fail + 2 HTTP2 Pass + 1 API Pass.
|
||
requireStatuses(t, report, Pass, Pass, Fail, Fail, Pass, Pass, Pass)
|
||
require.NotNil(t, report.SuggestedProtocol)
|
||
assert.Equal(t, connection.HTTP2, *report.SuggestedProtocol)
|
||
assert.False(t, report.hasHardFail())
|
||
assert.True(t, report.hasWarn())
|
||
}
|
||
|
||
// TestRun_HTTP2Blocked verifies that when HTTP/2 is blocked on all regions,
|
||
// the report is degraded (warn) and QUIC is the suggested protocol.
|
||
func TestRun_HTTP2Blocked(t *testing.T) {
|
||
t.Parallel()
|
||
ctrl := gomock.NewController(t)
|
||
|
||
fakeQUICConn := newFakeQUICConn(ctrl)
|
||
dns := mocks.NewMockDNSResolver(ctrl)
|
||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||
|
||
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrs(), nil)
|
||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nil, errors.New("connection refused")).AnyTimes()
|
||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(fakeQUICConn, nil).AnyTimes()
|
||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nopConn{}, nil)
|
||
|
||
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.Auto},
|
||
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||
|
||
// 2 DNS Pass + 2 QUIC Pass + 2 HTTP2 Fail + 1 API Pass.
|
||
requireStatuses(t, report, Pass, Pass, Pass, Pass, Fail, Fail, Pass)
|
||
require.NotNil(t, report.SuggestedProtocol)
|
||
assert.Equal(t, connection.QUIC, *report.SuggestedProtocol)
|
||
assert.False(t, report.hasHardFail())
|
||
assert.True(t, report.hasWarn())
|
||
}
|
||
|
||
// TestRun_BothTransportsBlocked verifies that when both QUIC and HTTP/2 are
|
||
// blocked on all regions it is a hard fail with no suggested protocol.
|
||
func TestRun_BothTransportsBlocked(t *testing.T) {
|
||
t.Parallel()
|
||
ctrl := gomock.NewController(t)
|
||
|
||
dns := mocks.NewMockDNSResolver(ctrl)
|
||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||
|
||
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrs(), nil)
|
||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nil, errors.New("blocked")).AnyTimes()
|
||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nil, errors.New("blocked")).AnyTimes()
|
||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nopConn{}, nil)
|
||
|
||
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.Auto},
|
||
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||
|
||
// 2 DNS Pass + 2 QUIC Fail + 2 HTTP2 Fail + 1 API Pass.
|
||
requireStatuses(t, report, Pass, Pass, Fail, Fail, Fail, Fail, Pass)
|
||
assert.Nil(t, report.SuggestedProtocol)
|
||
assert.True(t, report.hasHardFail())
|
||
}
|
||
|
||
// TestRun_PartialRegionQUICFail verifies "worst wins" semantics: when QUIC
|
||
// passes for region1 but fails for region2, QUIC is treated as failed and
|
||
// HTTP/2 becomes the suggested protocol.
|
||
func TestRun_PartialRegionQUICFail(t *testing.T) {
|
||
t.Parallel()
|
||
ctrl := gomock.NewController(t)
|
||
|
||
dns := mocks.NewMockDNSResolver(ctrl)
|
||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||
fakeQUICConn := newFakeQUICConn(ctrl)
|
||
|
||
// Two regions: 1.2.3.4 (region1) and 5.6.7.8 (region2).
|
||
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrs(), nil)
|
||
|
||
// TCP/HTTP2: both regions pass.
|
||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nopConn{}, nil).AnyTimes()
|
||
|
||
// QUIC: region1 (1.2.3.4) passes, region2 (5.6.7.8) fails.
|
||
region1Addr := &net.UDPAddr{IP: net.ParseIP("1.2.3.4"), Port: 7844}
|
||
region2Addr := &net.UDPAddr{IP: net.ParseIP("5.6.7.8"), Port: 7844}
|
||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), region1Addr.AddrPort(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(fakeQUICConn, nil).AnyTimes()
|
||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), region2Addr.AddrPort(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nil, errors.New("connection refused")).AnyTimes()
|
||
|
||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nopConn{}, nil)
|
||
|
||
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.Auto},
|
||
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||
|
||
// 2 DNS Pass + QUIC-region1 Pass + QUIC-region2 Fail + 2 HTTP2 Pass + 1 API Pass.
|
||
requireStatuses(t, report, Pass, Pass, Pass, Fail, Pass, Pass, Pass)
|
||
|
||
// Worst wins: region2 QUIC failed, so QUIC is treated as failed overall.
|
||
// HTTP/2 passes on all regions → HTTP/2 is the suggested protocol.
|
||
require.NotNil(t, report.SuggestedProtocol)
|
||
assert.Equal(t, connection.HTTP2, *report.SuggestedProtocol)
|
||
assert.False(t, report.hasHardFail())
|
||
assert.True(t, report.hasWarn())
|
||
}
|
||
|
||
// TestRun_DNSFail_SkipsTransports verifies that when DNS fails, transport rows
|
||
// are emitted as Skip (one per DNS region) and no transport dials are made.
|
||
func TestRun_DNSFail_SkipsTransports(t *testing.T) {
|
||
t.Parallel()
|
||
ctrl := gomock.NewController(t)
|
||
|
||
dns := mocks.NewMockDNSResolver(ctrl)
|
||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||
|
||
dns.EXPECT().Resolve(gomock.Any()).
|
||
Return(nil, errors.New("no such host")).AnyTimes()
|
||
// Transport dialers must NOT be called when DNS fails.
|
||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0)
|
||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0)
|
||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nopConn{}, nil)
|
||
|
||
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.Auto},
|
||
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||
|
||
// DNS failure emits 2 Fail rows (one per default region).
|
||
// Transport rows: one skip per DNS region for QUIC and HTTP/2 = 2 QUIC skips + 2 HTTP2 skips.
|
||
// 2 DNS Fail + 2 QUIC Skip + 2 HTTP2 Skip + 1 API Pass = 7 results.
|
||
require.Len(t, report.Results, 7)
|
||
assert.Equal(t, Fail, report.Results[0].ProbeStatus, "DNS region1")
|
||
assert.Equal(t, Fail, report.Results[1].ProbeStatus, "DNS region2")
|
||
assert.Equal(t, Skip, report.Results[2].ProbeStatus, "QUIC region1 must be skipped")
|
||
assert.Equal(t, Skip, report.Results[3].ProbeStatus, "QUIC region2 must be skipped")
|
||
assert.Equal(t, Skip, report.Results[4].ProbeStatus, "HTTP/2 region1 must be skipped")
|
||
assert.Equal(t, Skip, report.Results[5].ProbeStatus, "HTTP/2 region2 must be skipped")
|
||
assert.Equal(t, Pass, report.Results[6].ProbeStatus, "API still runs")
|
||
assert.True(t, report.hasHardFail())
|
||
}
|
||
|
||
// TestRun_ManagementAPIFail verifies that a Management API failure results
|
||
// in a warning (not a hard fail) and QUIC remains the suggested protocol.
|
||
func TestRun_ManagementAPIFail(t *testing.T) {
|
||
t.Parallel()
|
||
ctrl := gomock.NewController(t)
|
||
|
||
dns := mocks.NewMockDNSResolver(ctrl)
|
||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||
fakeQUICConn := newFakeQUICConn(ctrl)
|
||
|
||
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrs(), nil)
|
||
// twoRegionAddrs has 2 regions × 1 V4 address each; each succeeds on first try.
|
||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nopConn{}, nil).Times(2)
|
||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(fakeQUICConn, nil).Times(2)
|
||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nil, errors.New("connection refused")).AnyTimes()
|
||
|
||
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.Auto},
|
||
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||
|
||
// 2 DNS Pass + 2 QUIC Pass + 2 HTTP2 Pass + 1 API Fail.
|
||
requireStatuses(t, report, Pass, Pass, Pass, Pass, Pass, Pass, Fail)
|
||
require.NotNil(t, report.SuggestedProtocol)
|
||
assert.Equal(t, connection.QUIC, *report.SuggestedProtocol)
|
||
assert.False(t, report.hasHardFail())
|
||
assert.True(t, report.hasWarn())
|
||
}
|
||
|
||
// TestRun_RegionFlagForwardedToDNS verifies that the --region flag is passed
|
||
// verbatim to the DNS resolver and that regional hostnames appear in the results.
|
||
func TestRun_RegionFlagForwardedToDNS(t *testing.T) {
|
||
t.Parallel()
|
||
ctrl := gomock.NewController(t)
|
||
|
||
dns := mocks.NewMockDNSResolver(ctrl)
|
||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||
fakeQUICConn := newFakeQUICConn(ctrl)
|
||
|
||
// The region string must be forwarded verbatim to the DNS resolver.
|
||
dns.EXPECT().Resolve("us").Return(twoRegionAddrs(), nil)
|
||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nopConn{}, nil).Times(2)
|
||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(fakeQUICConn, nil).Times(2)
|
||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nopConn{}, nil)
|
||
|
||
report := Run(t.Context(), emptyCert, Config{Region: "us", Timeout: 2 * time.Second, IPVersion: allregions.Auto},
|
||
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||
|
||
// DNS rows carry regional hostnames (indices 0 and 1).
|
||
assert.Equal(t, "us-region1.v2.argotunnel.com", report.Results[0].Target, "DNS region1")
|
||
assert.Equal(t, "us-region2.v2.argotunnel.com", report.Results[1].Target, "DNS region2")
|
||
|
||
// Transport rows reuse the same regional hostnames (QUIC: 2,3 / HTTP2: 4,5).
|
||
assert.Equal(t, "us-region1.v2.argotunnel.com", report.Results[2].Target, "QUIC region1")
|
||
assert.Equal(t, "us-region2.v2.argotunnel.com", report.Results[3].Target, "QUIC region2")
|
||
assert.Equal(t, "us-region1.v2.argotunnel.com", report.Results[4].Target, "HTTP2 region1")
|
||
assert.Equal(t, "us-region2.v2.argotunnel.com", report.Results[5].Target, "HTTP2 region2")
|
||
}
|
||
|
||
// TestRun_QUICUsesProbeConnIndex verifies that the QUIC probe always uses the
|
||
// reserved sentinel connIndex (math.MaxUint8 = 255) to bypass port-reuse checks.
|
||
func TestRun_QUICUsesProbeConnIndex(t *testing.T) {
|
||
t.Parallel()
|
||
ctrl := gomock.NewController(t)
|
||
|
||
dns := mocks.NewMockDNSResolver(ctrl)
|
||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||
fakeQUICConn := newFakeQUICConn(ctrl)
|
||
|
||
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrs(), nil)
|
||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nopConn{}, nil).Times(2)
|
||
// connIndex must be the reserved sentinel (math.MaxUint8 = 255), never 0.
|
||
// twoRegionAddrs has 2 regions × 1 V4 address each → 2 calls.
|
||
quicD.EXPECT().DialQuic(
|
||
gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(),
|
||
gomock.Eq(uint8(math.MaxUint8)),
|
||
gomock.Any(), gomock.Any(),
|
||
).Return(fakeQUICConn, nil).Times(2)
|
||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nopConn{}, nil)
|
||
|
||
Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.Auto},
|
||
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||
}
|
||
|
||
// TestRun_BothFamiliesProbed verifies that when both V4 and V6 addresses are
|
||
// present in the DNS response, both are probed (2 regions × 2 families = 4 dials).
|
||
func TestRun_BothFamiliesProbed(t *testing.T) {
|
||
t.Parallel()
|
||
ctrl := gomock.NewController(t)
|
||
|
||
dns := mocks.NewMockDNSResolver(ctrl)
|
||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||
fakeQUICConn := newFakeQUICConn(ctrl)
|
||
|
||
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrsBothFamilies(), nil)
|
||
// 2 regions × 2 families = 4 dial calls each for QUIC and HTTP/2.
|
||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nopConn{}, nil).Times(4)
|
||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(fakeQUICConn, nil).Times(4)
|
||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nopConn{}, nil)
|
||
|
||
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: allregions.Auto},
|
||
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||
|
||
// 2 DNS + 2 QUIC + 2 HTTP2 + 1 API = 7 results, all passing.
|
||
requireStatuses(t, report, Pass, Pass, Pass, Pass, Pass, Pass, Pass)
|
||
require.NotNil(t, report.SuggestedProtocol)
|
||
assert.Equal(t, connection.QUIC, *report.SuggestedProtocol)
|
||
}
|
||
|
||
// TestRun_IPVersionRestriction verifies that when a single IP family is
|
||
// configured, only that family is probed (2 regions × 1 addr = 2 dials per
|
||
// transport) and the excluded family is never dialled.
|
||
func TestRun_IPVersionRestriction(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
tests := []struct {
|
||
name string
|
||
ipVersion allregions.ConfigIPVersion
|
||
}{
|
||
{"IPv4Only skips V6", allregions.IPv4Only},
|
||
{"IPv6Only skips V4", allregions.IPv6Only},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
t.Parallel()
|
||
ctrl := gomock.NewController(t)
|
||
|
||
dns := mocks.NewMockDNSResolver(ctrl)
|
||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||
fakeQUICConn := newFakeQUICConn(ctrl)
|
||
|
||
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrsBothFamilies(), nil)
|
||
// 2 regions × 1 addr per restricted family = 2 dials each.
|
||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nopConn{}, nil).Times(2)
|
||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(fakeQUICConn, nil).Times(2)
|
||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nopConn{}, nil)
|
||
|
||
report := Run(t.Context(), emptyCert, Config{Timeout: 2 * time.Second, IPVersion: tt.ipVersion},
|
||
nopLogger(), RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||
|
||
requireStatuses(t, report, Pass, Pass, Pass, Pass, Pass, Pass, Pass)
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestRun_EdgeAddrs_SingleAddr verifies that a single --edge addr bypasses DNS
|
||
// probing. The report contains one DNS Skip row, transport rows labeled with
|
||
// the raw addr string, and the Management API row.
|
||
func TestRun_EdgeAddrs_SingleAddr(t *testing.T) {
|
||
t.Parallel()
|
||
ctrl := gomock.NewController(t)
|
||
|
||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||
fakeQUICConn := newFakeQUICConn(ctrl)
|
||
|
||
// DNS resolver must NOT be called when EdgeAddrs is set.
|
||
dns := mocks.NewMockDNSResolver(ctrl)
|
||
dns.EXPECT().Resolve(gomock.Any()).Times(0)
|
||
|
||
// One addr resolves to one group → one dial per transport.
|
||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nopConn{}, nil).Times(1)
|
||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(fakeQUICConn, nil).Times(1)
|
||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nopConn{}, nil)
|
||
|
||
cfg := Config{
|
||
EdgeAddrs: []string{"127.0.0.1:7844"},
|
||
Timeout: 2 * time.Second,
|
||
IPVersion: allregions.Auto,
|
||
}
|
||
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
|
||
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||
|
||
// 1 DNS Skip + 1 QUIC + 1 HTTP2 + 1 API = 4 results.
|
||
requireStatuses(t, report, Pass, Pass, Pass, Pass)
|
||
assert.Equal(t, ProbeTypeDNS, report.Results[0].Type, "first row must be DNS skip")
|
||
assert.Equal(t, "127.0.0.1:7844", report.Results[1].Target, "QUIC target must be the raw --edge addr")
|
||
assert.Equal(t, "127.0.0.1:7844", report.Results[2].Target, "HTTP2 target must be the raw --edge addr")
|
||
require.NotNil(t, report.SuggestedProtocol)
|
||
assert.Equal(t, connection.QUIC, *report.SuggestedProtocol)
|
||
}
|
||
|
||
// TestRun_EdgeAddrs_MultipleAddrs verifies that multiple --edge addrs produce
|
||
// one transport row per addr, each labeled with its original addr string.
|
||
func TestRun_EdgeAddrs_MultipleAddrs(t *testing.T) {
|
||
t.Parallel()
|
||
ctrl := gomock.NewController(t)
|
||
|
||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||
fakeQUICConn := newFakeQUICConn(ctrl)
|
||
|
||
dns := mocks.NewMockDNSResolver(ctrl)
|
||
dns.EXPECT().Resolve(gomock.Any()).Times(0)
|
||
|
||
// Two addrs → two groups → two dials per transport.
|
||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nopConn{}, nil).Times(2)
|
||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(fakeQUICConn, nil).Times(2)
|
||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nopConn{}, nil)
|
||
|
||
cfg := Config{
|
||
EdgeAddrs: []string{"127.0.0.1:7844", "127.0.0.2:7844"},
|
||
Timeout: 2 * time.Second,
|
||
IPVersion: allregions.Auto,
|
||
}
|
||
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
|
||
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||
|
||
// 2 DNS Pass (one per addr) + 2 QUIC + 2 HTTP2 + 1 API = 7 results.
|
||
requireStatuses(t, report, Pass, Pass, Pass, Pass, Pass, Pass, Pass)
|
||
assert.Equal(t, ProbeTypeDNS, report.Results[0].Type, "first row must be DNS skip addr1")
|
||
assert.Equal(t, "127.0.0.1:7844", report.Results[0].Target, "DNS skip addr1 label")
|
||
assert.Equal(t, ProbeTypeDNS, report.Results[1].Type, "second row must be DNS skip addr2")
|
||
assert.Equal(t, "127.0.0.2:7844", report.Results[1].Target, "DNS skip addr2 label")
|
||
assert.Equal(t, "127.0.0.1:7844", report.Results[2].Target, "QUIC addr1")
|
||
assert.Equal(t, "127.0.0.2:7844", report.Results[3].Target, "QUIC addr2")
|
||
assert.Equal(t, "127.0.0.1:7844", report.Results[4].Target, "HTTP2 addr1")
|
||
assert.Equal(t, "127.0.0.2:7844", report.Results[5].Target, "HTTP2 addr2")
|
||
}
|
||
|
||
// TestRun_EdgeAddrs_UnresolvableAddr verifies that when all --edge addrs fail
|
||
// to resolve, the DNS resolver is not called and transport rows are skipped,
|
||
// mirroring the DNS skip row.
|
||
func TestRun_EdgeAddrs_UnresolvableAddr(t *testing.T) {
|
||
t.Parallel()
|
||
ctrl := gomock.NewController(t)
|
||
|
||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||
|
||
dns := mocks.NewMockDNSResolver(ctrl)
|
||
dns.EXPECT().Resolve(gomock.Any()).Times(0)
|
||
|
||
// Unresolvable addr → no groups → no transport dials.
|
||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0)
|
||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0)
|
||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nopConn{}, nil)
|
||
|
||
cfg := Config{
|
||
EdgeAddrs: []string{"not-a-valid-addr"},
|
||
Timeout: 2 * time.Second,
|
||
IPVersion: allregions.Auto,
|
||
}
|
||
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
|
||
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||
|
||
// 1 DNS Fail + 1 QUIC Skip + 1 HTTP2 Skip + 1 API = 4 results.
|
||
requireStatuses(t, report, Fail, Skip, Skip, Pass)
|
||
assert.Equal(t, ProbeTypeDNS, report.Results[0].Type)
|
||
assert.Equal(t, "not-a-valid-addr", report.Results[0].Target)
|
||
assert.Equal(t, ProbeTypeQUIC, report.Results[1].Type)
|
||
assert.Equal(t, ProbeTypeHTTP2, report.Results[2].Type)
|
||
assert.Nil(t, report.SuggestedProtocol)
|
||
assert.True(t, report.hasHardFail())
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Protocol override tests
|
||
// ---------------------------------------------------------------------------
|
||
|
||
// TestRun_ProtocolOverride_HTTP2_BothPass verifies that when --protocol http2
|
||
// is set and both transports are reachable, the summary reports HTTP/2 (not
|
||
// QUIC, which would otherwise win the heuristic).
|
||
func TestRun_ProtocolOverride_HTTP2_BothPass(t *testing.T) {
|
||
t.Parallel()
|
||
ctrl := gomock.NewController(t)
|
||
|
||
dns := mocks.NewMockDNSResolver(ctrl)
|
||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||
fakeQUICConn := newFakeQUICConn(ctrl)
|
||
|
||
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrs(), nil)
|
||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nopConn{}, nil).AnyTimes()
|
||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(fakeQUICConn, nil).AnyTimes()
|
||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nopConn{}, nil)
|
||
|
||
cfg := Config{
|
||
Timeout: 2 * time.Second,
|
||
IPVersion: allregions.Auto,
|
||
ProtocolOverride: "http2",
|
||
}
|
||
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
|
||
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||
|
||
// Both transports pass, but the override must win — HTTP/2 is reported.
|
||
require.NotNil(t, report.SuggestedProtocol)
|
||
assert.Equal(t, connection.HTTP2, *report.SuggestedProtocol,
|
||
"override http2 should be reported even though QUIC probes also passed")
|
||
assert.False(t, report.hasHardFail())
|
||
}
|
||
|
||
// TestRun_ProtocolOverride_QUIC_BothPass verifies that when --protocol quic is
|
||
// set and both transports are reachable, the summary reports QUIC (same as the
|
||
// heuristic would choose, but driven by the override).
|
||
func TestRun_ProtocolOverride_QUIC_BothPass(t *testing.T) {
|
||
t.Parallel()
|
||
ctrl := gomock.NewController(t)
|
||
|
||
dns := mocks.NewMockDNSResolver(ctrl)
|
||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||
fakeQUICConn := newFakeQUICConn(ctrl)
|
||
|
||
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrs(), nil)
|
||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nopConn{}, nil).AnyTimes()
|
||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(fakeQUICConn, nil).AnyTimes()
|
||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nopConn{}, nil)
|
||
|
||
cfg := Config{
|
||
Timeout: 2 * time.Second,
|
||
IPVersion: allregions.Auto,
|
||
ProtocolOverride: "quic",
|
||
}
|
||
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
|
||
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||
|
||
require.NotNil(t, report.SuggestedProtocol)
|
||
assert.Equal(t, connection.QUIC, *report.SuggestedProtocol)
|
||
assert.False(t, report.hasHardFail())
|
||
}
|
||
|
||
// TestRun_ProtocolOverride_HTTP2_QUICBlocked verifies that when --protocol http2
|
||
// is set and QUIC is blocked, we still report HTTP/2 (not a fallback to the
|
||
// heuristic, since the overridden transport is healthy).
|
||
func TestRun_ProtocolOverride_HTTP2_QUICBlocked(t *testing.T) {
|
||
t.Parallel()
|
||
ctrl := gomock.NewController(t)
|
||
|
||
dns := mocks.NewMockDNSResolver(ctrl)
|
||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||
|
||
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrs(), nil)
|
||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nopConn{}, nil).AnyTimes()
|
||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nil, errors.New("blocked")).AnyTimes()
|
||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nopConn{}, nil)
|
||
|
||
cfg := Config{
|
||
Timeout: 2 * time.Second,
|
||
IPVersion: allregions.Auto,
|
||
ProtocolOverride: "http2",
|
||
}
|
||
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
|
||
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||
|
||
require.NotNil(t, report.SuggestedProtocol)
|
||
assert.Equal(t, connection.HTTP2, *report.SuggestedProtocol)
|
||
assert.False(t, report.hasHardFail())
|
||
}
|
||
|
||
// TestRun_ProtocolOverride_HTTP2_BothBlocked verifies that when --protocol http2
|
||
// is set but the HTTP/2 transport itself also fails (hard fail), the override
|
||
// falls through to the heuristic which returns nil — there is no usable protocol.
|
||
func TestRun_ProtocolOverride_HTTP2_BothBlocked(t *testing.T) {
|
||
t.Parallel()
|
||
ctrl := gomock.NewController(t)
|
||
|
||
dns := mocks.NewMockDNSResolver(ctrl)
|
||
tcp := mocks.NewMockTCPDialer(ctrl)
|
||
quicD := mocks.NewMockQUICDialer(ctrl)
|
||
mgmt := mocks.NewMockManagementDialer(ctrl)
|
||
|
||
dns.EXPECT().Resolve(gomock.Any()).Return(twoRegionAddrs(), nil)
|
||
tcp.EXPECT().DialEdge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nil, errors.New("blocked")).AnyTimes()
|
||
quicD.EXPECT().DialQuic(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nil, errors.New("blocked")).AnyTimes()
|
||
mgmt.EXPECT().DialContext(gomock.Any(), gomock.Any(), gomock.Any()).
|
||
Return(nopConn{}, nil)
|
||
|
||
cfg := Config{
|
||
Timeout: 2 * time.Second,
|
||
IPVersion: allregions.Auto,
|
||
ProtocolOverride: "http2",
|
||
}
|
||
report := Run(t.Context(), emptyCert, cfg, nopLogger(),
|
||
RunDialers{DNSResolver: dns, TCPDialer: tcp, QUICDialer: quicD, ManagementDialer: mgmt})
|
||
|
||
// The overridden transport (HTTP/2) is blocked, so the override cannot be
|
||
// honoured and the hard-fail path reports no suggested protocol.
|
||
assert.Nil(t, report.SuggestedProtocol)
|
||
assert.True(t, report.hasHardFail())
|
||
}
|