diff --git a/cmd/cloudflared/tunnel/cmd.go b/cmd/cloudflared/tunnel/cmd.go index 44f7f198..c98d5ae4 100644 --- a/cmd/cloudflared/tunnel/cmd.go +++ b/cmd/cloudflared/tunnel/cmd.go @@ -206,7 +206,7 @@ func runAdhocNamedTunnel(sc *subcommandContext, name, credentialsOutputPath stri // runClassicTunnel creates a "classic" non-named tunnel func runClassicTunnel(sc *subcommandContext) error { - return StartServer(sc.c, version, nil, sc.log, sc.isUIEnabled) + return StartServer(sc.c, version, nil, sc.log, sc.isUIEnabled, "") } func routeFromFlag(c *cli.Context) (route tunnelstore.Route, ok bool) { @@ -225,6 +225,7 @@ func StartServer( namedTunnel *connection.NamedTunnelConfig, log *zerolog.Logger, isUIEnabled bool, + quickTunnelHostname string, ) error { _ = raven.SetDSN(sentryDSN) var wg sync.WaitGroup @@ -324,15 +325,6 @@ func StartServer( observer := connection.NewObserver(log, logTransport, isUIEnabled) - // Send Quick Tunnel URL to UI if applicable - var quickTunnelURL string - if namedTunnel != nil { - quickTunnelURL = namedTunnel.QuickTunnelUrl - } - if quickTunnelURL != "" { - observer.SendURL(quickTunnelURL) - } - tunnelConfig, ingressRules, err := prepareTunnelConfig(c, buildInfo, version, log, logTransport, observer, namedTunnel) if err != nil { log.Err(err).Msg("Couldn't start tunnel") @@ -350,7 +342,7 @@ func StartServer( defer wg.Done() readinessServer := metrics.NewReadyServer(log) observer.RegisterSink(readinessServer) - errC <- metrics.ServeMetrics(metricsListener, ctx.Done(), readinessServer, quickTunnelURL, log) + errC <- metrics.ServeMetrics(metricsListener, ctx.Done(), readinessServer, quickTunnelHostname, log) }() if err := ingressRules.StartOrigins(&wg, log, ctx.Done(), errC); err != nil { @@ -634,7 +626,6 @@ func tunnelFlags(shouldHide bool) []cli.Flag { altsrc.NewStringFlag(&cli.StringFlag{ Name: "quick-service", Usage: "URL for a service which manages unauthenticated 'quick' tunnels.", - Value: "https://api.trycloudflare.com", Hidden: true, }), selectProtocolFlag, diff --git a/cmd/cloudflared/tunnel/configuration.go b/cmd/cloudflared/tunnel/configuration.go index 67e3b86f..5800a721 100644 --- a/cmd/cloudflared/tunnel/configuration.go +++ b/cmd/cloudflared/tunnel/configuration.go @@ -161,7 +161,7 @@ func prepareTunnelConfig( log.Err(err).Str(LogFieldHostname, configHostname).Msg("Invalid hostname") return nil, ingress.Ingress{}, errors.Wrap(err, "Invalid hostname") } - isQuickTunnel := hostname == "" + isFreeTunnel := hostname == "" clientID := c.String("id") if !c.IsSet("id") { clientID, err = generateRandomClientID(log) @@ -179,7 +179,7 @@ func prepareTunnelConfig( tags = append(tags, tunnelpogs.Tag{Name: "ID", Value: clientID}) var originCert []byte - if !isQuickTunnel { + if !isFreeTunnel { originCertPath := c.String("origincert") originCertLog := log.With(). Str(LogFieldOriginCertPath, originCertPath). @@ -278,6 +278,7 @@ func prepareTunnelConfig( HAConnections: c.Int("ha-connections"), IncidentLookup: origin.NewIncidentLookup(), IsAutoupdated: c.Bool("is-autoupdated"), + IsFreeTunnel: isFreeTunnel, LBPool: c.String("lb-pool"), Tags: tags, Log: log, diff --git a/cmd/cloudflared/tunnel/quick_tunnel.go b/cmd/cloudflared/tunnel/quick_tunnel.go index c81a4f32..db76d212 100644 --- a/cmd/cloudflared/tunnel/quick_tunnel.go +++ b/cmd/cloudflared/tunnel/quick_tunnel.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "net/http" - "strings" "time" "github.com/google/uuid" @@ -52,7 +51,7 @@ func RunQuickTunnel(sc *subcommandContext) error { TunnelName: data.Result.Name, } - for _, line := range AsciiBox([]string{ + for _, line := range connection.AsciiBox([]string{ "Your Quick Tunnel has been created! Visit it at:", data.Result.Hostname, }, 2) { @@ -62,9 +61,10 @@ func RunQuickTunnel(sc *subcommandContext) error { return StartServer( sc.c, version, - &connection.NamedTunnelConfig{Credentials: credentials, QuickTunnelUrl: data.Result.Hostname}, + &connection.NamedTunnelConfig{Credentials: credentials}, sc.log, sc.isUIEnabled, + data.Result.Hostname, ) } @@ -86,26 +86,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 -} diff --git a/cmd/cloudflared/tunnel/subcommand_context.go b/cmd/cloudflared/tunnel/subcommand_context.go index d5b42590..69d16478 100644 --- a/cmd/cloudflared/tunnel/subcommand_context.go +++ b/cmd/cloudflared/tunnel/subcommand_context.go @@ -286,6 +286,7 @@ func (sc *subcommandContext) run(tunnelID uuid.UUID) error { &connection.NamedTunnelConfig{Credentials: credentials}, sc.log, sc.isUIEnabled, + "", ) } diff --git a/component-tests/test_reconnect.py b/component-tests/test_reconnect.py index 78808a67..15af88ca 100644 --- a/component-tests/test_reconnect.py +++ b/component-tests/test_reconnect.py @@ -24,13 +24,12 @@ class TestReconnect: # Repeat the test multiple times because some issues only occur after multiple reconnects self.assert_reconnect(config, cloudflared, 5) - @pytest.mark.skipif(platform.system() == "Windows", reason=f"Currently buggy on Windows TUN-4699") def test_classic_reconnect(self, tmp_path, component_tests_config): extra_config = copy.copy(self.extra_config) extra_config["hello-world"] = True config = component_tests_config( additional_config=extra_config, named_tunnel=False) - with start_cloudflared(tmp_path, config, cfd_args=[], new_process=True, allow_input=True, capture_output=True) as cloudflared: + with start_cloudflared(tmp_path, config, cfd_args=[], new_process=True, allow_input=True, capture_output=False) as cloudflared: self.assert_reconnect(config, cloudflared, 1) def send_reconnect(self, cloudflared, secs): diff --git a/connection/connection.go b/connection/connection.go index dbe5ef1e..e68afb64 100644 --- a/connection/connection.go +++ b/connection/connection.go @@ -29,9 +29,8 @@ type Config struct { } type NamedTunnelConfig struct { - Credentials Credentials - Client pogs.ClientInfo - QuickTunnelUrl string + Credentials Credentials + Client pogs.ClientInfo } // Credentials are stored in the credentials file and contain all info needed to run a tunnel. @@ -56,6 +55,10 @@ type ClassicTunnelConfig struct { UseReconnectToken bool } +func (c *ClassicTunnelConfig) IsTrialZone() bool { + return c.Hostname == "" +} + // Type indicates the connection type of the connection. type Type int diff --git a/connection/event.go b/connection/event.go index ab6d0d33..6afde5bb 100644 --- a/connection/event.go +++ b/connection/event.go @@ -18,7 +18,7 @@ const ( Connected // Reconnecting means the connection to the edge is being re-established. Reconnecting - // SetURL means this connection's tunnel was given a URL by the edge. Used for quick tunnels. + // SetURL means this connection's tunnel was given a URL by the edge. Used for free tunnels. SetURL // RegisteringTunnel means the non-named tunnel is registering its connection. RegisteringTunnel diff --git a/connection/observer.go b/connection/observer.go index abf858d4..bb0887a5 100644 --- a/connection/observer.go +++ b/connection/observer.go @@ -1,7 +1,13 @@ package connection import ( + "fmt" + "net/url" + "strings" + "github.com/rs/zerolog" + + tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs" ) const ( @@ -48,6 +54,53 @@ func (o *Observer) logServerInfo(connIndex uint8, location, msg string) { o.metrics.registerServerLocation(uint8ToString(connIndex), location) } +func (o *Observer) logTrialHostname(registration *tunnelpogs.TunnelRegistration) error { + // Print out the user's trial zone URL in a nice box (if they requested and got one and UI flag is not set) + if !o.uiEnabled { + if registrationURL, err := url.Parse(registration.Url); err == nil { + for _, line := range AsciiBox(TrialZoneMsg(registrationURL.String()), 2) { + o.log.Info().Msg(line) + } + } else { + o.log.Error().Msg("Failed to connect tunnel, please try again.") + return fmt.Errorf("empty URL in response from Cloudflare edge") + } + } + return nil +} + +// 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 +} + +func TrialZoneMsg(url string) []string { + return []string{ + "Your free tunnel has started! Visit it:", + " " + url, + } +} + func (o *Observer) sendRegisteringEvent(connIndex uint8) { o.sendEvent(Event{Index: connIndex, EventType: RegisteringTunnel}) } @@ -56,7 +109,7 @@ func (o *Observer) sendConnectedEvent(connIndex uint8, location string) { o.sendEvent(Event{Index: connIndex, EventType: Connected, Location: location}) } -func (o *Observer) SendURL(url string) { +func (o *Observer) sendURL(url string) { o.sendEvent(Event{EventType: SetURL, URL: url}) } diff --git a/connection/rpc.go b/connection/rpc.go index e8eb6f4a..7ae55d40 100644 --- a/connection/rpc.go +++ b/connection/rpc.go @@ -159,6 +159,8 @@ func (h *h2muxConnection) registerTunnel(ctx context.Context, credentialSetter C return h.processRegisterTunnelError(registrationErr, register) } + // Send free tunnel URL to UI + h.observer.sendURL(registration.Url) credentialSetter.SetEventDigest(h.connIndex, registration.EventDigest) return h.processRegistrationSuccess(registration, register, credentialSetter, classicTunnel) } @@ -185,6 +187,14 @@ func (h *h2muxConnection) processRegistrationSuccess( h.observer.log.Info().Msgf("Each HA connection's tunnel IDs: %v", h.observer.metrics.tunnelsHA.String()) } + // Print out the user's trial zone URL in a nice box (if they requested and got one and UI flag is not set) + if classicTunnel.IsTrialZone() { + err := h.observer.logTrialHostname(registration) + if err != nil { + return err + } + } + credentialManager.SetConnDigest(h.connIndex, registration.ConnDigest) h.observer.metrics.userHostnamesCounts.WithLabelValues(registration.Url).Inc() diff --git a/metrics/readiness.go b/metrics/readiness.go index 030f71cb..62d9f8f4 100644 --- a/metrics/readiness.go +++ b/metrics/readiness.go @@ -36,6 +36,8 @@ func (rs *ReadyServer) OnTunnelEvent(c conn.Event) { rs.Lock() rs.isConnected[int(c.Index)] = false rs.Unlock() + case conn.SetURL: + break default: rs.log.Error().Msgf("Unknown connection event case %v", c) } diff --git a/origin/tunnel.go b/origin/tunnel.go index e1aa4a33..83a74302 100644 --- a/origin/tunnel.go +++ b/origin/tunnel.go @@ -46,6 +46,7 @@ type TunnelConfig struct { HAConnections int IncidentLookup IncidentLookup IsAutoupdated bool + IsFreeTunnel bool LBPool string Tags []tunnelpogs.Tag Log *zerolog.Logger