Skip to content

Commit 820a201

Browse files
committed
TUN-7135: Add cloudflared tail
1 parent 93acdaf commit 820a201

File tree

6 files changed

+283
-43
lines changed

6 files changed

+283
-43
lines changed

cmd/cloudflared/cliutil/logger.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package cliutil
2+
3+
import (
4+
"github.com/urfave/cli/v2"
5+
"github.com/urfave/cli/v2/altsrc"
6+
7+
"github.com/cloudflare/cloudflared/logger"
8+
)
9+
10+
var (
11+
debugLevelWarning = "At debug level cloudflared will log request URL, method, protocol, content length, as well as, all request and response headers. " +
12+
"This can expose sensitive information in your logs."
13+
)
14+
15+
func ConfigureLoggingFlags(shouldHide bool) []cli.Flag {
16+
return []cli.Flag{
17+
altsrc.NewStringFlag(&cli.StringFlag{
18+
Name: logger.LogLevelFlag,
19+
Value: "info",
20+
Usage: "Application logging level {debug, info, warn, error, fatal}. " + debugLevelWarning,
21+
EnvVars: []string{"TUNNEL_LOGLEVEL"},
22+
Hidden: shouldHide,
23+
}),
24+
altsrc.NewStringFlag(&cli.StringFlag{
25+
Name: logger.LogTransportLevelFlag,
26+
Aliases: []string{"proto-loglevel"}, // This flag used to be called proto-loglevel
27+
Value: "info",
28+
Usage: "Transport logging level(previously called protocol logging level) {debug, info, warn, error, fatal}",
29+
EnvVars: []string{"TUNNEL_PROTO_LOGLEVEL", "TUNNEL_TRANSPORT_LOGLEVEL"},
30+
Hidden: shouldHide,
31+
}),
32+
altsrc.NewStringFlag(&cli.StringFlag{
33+
Name: logger.LogFileFlag,
34+
Usage: "Save application log to this file for reporting issues.",
35+
EnvVars: []string{"TUNNEL_LOGFILE"},
36+
Hidden: shouldHide,
37+
}),
38+
altsrc.NewStringFlag(&cli.StringFlag{
39+
Name: logger.LogDirectoryFlag,
40+
Usage: "Save application log to this directory for reporting issues.",
41+
EnvVars: []string{"TUNNEL_LOGDIRECTORY"},
42+
Hidden: shouldHide,
43+
}),
44+
altsrc.NewStringFlag(&cli.StringFlag{
45+
Name: "trace-output",
46+
Usage: "Name of trace output file, generated when cloudflared stops.",
47+
EnvVars: []string{"TUNNEL_TRACE_OUTPUT"},
48+
Hidden: shouldHide,
49+
}),
50+
}
51+
}

cmd/cloudflared/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/cloudflare/cloudflared/cmd/cloudflared/access"
1616
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
1717
"github.com/cloudflare/cloudflared/cmd/cloudflared/proxydns"
18+
"github.com/cloudflare/cloudflared/cmd/cloudflared/tail"
1819
"github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
1920
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
2021
"github.com/cloudflare/cloudflared/config"
@@ -89,6 +90,7 @@ func main() {
8990
updater.Init(Version)
9091
tracing.Init(Version)
9192
token.Init(Version)
93+
tail.Init(Version)
9294
runApp(app, graceShutdownC)
9395
}
9496

@@ -138,6 +140,7 @@ To determine if an update happened in a script, check for error code 11.`,
138140
cmds = append(cmds, tunnel.Commands()...)
139141
cmds = append(cmds, proxydns.Command(false))
140142
cmds = append(cmds, access.Commands()...)
143+
cmds = append(cmds, tail.Command())
141144
return cmds
142145
}
143146

cmd/cloudflared/tail/cmd.go

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
package tail
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
"net/url"
8+
"os"
9+
"os/signal"
10+
"syscall"
11+
"time"
12+
13+
"github.com/mattn/go-colorable"
14+
"github.com/rs/zerolog"
15+
"github.com/urfave/cli/v2"
16+
"nhooyr.io/websocket"
17+
18+
"github.com/cloudflare/cloudflared/logger"
19+
"github.com/cloudflare/cloudflared/management"
20+
)
21+
22+
var (
23+
version string
24+
)
25+
26+
func Init(v string) {
27+
version = v
28+
}
29+
30+
func Command() *cli.Command {
31+
return &cli.Command{
32+
Name: "tail",
33+
Action: Run,
34+
Usage: "Stream logs from a remote cloudflared",
35+
Flags: []cli.Flag{
36+
&cli.StringFlag{
37+
Name: "connector-id",
38+
Usage: "Access a specific cloudflared instance by connector id (for when a tunnel has multiple cloudflared's)",
39+
Value: "",
40+
EnvVars: []string{"TUNNEL_MANAGEMENT_CONNECTOR"},
41+
},
42+
&cli.StringFlag{
43+
Name: "token",
44+
Usage: "Access token for a specific tunnel",
45+
Value: "",
46+
EnvVars: []string{"TUNNEL_MANAGEMENT_TOKEN"},
47+
},
48+
&cli.StringFlag{
49+
Name: "management-hostname",
50+
Usage: "Management hostname to signify incoming management requests",
51+
EnvVars: []string{"TUNNEL_MANAGEMENT_HOSTNAME"},
52+
Hidden: true,
53+
Value: "management.argotunnel.com",
54+
},
55+
&cli.StringFlag{
56+
Name: "trace",
57+
Usage: "Set a cf-trace-id for the request",
58+
Hidden: true,
59+
Value: "",
60+
},
61+
&cli.StringFlag{
62+
Name: logger.LogLevelFlag,
63+
Value: "info",
64+
Usage: "Application logging level {debug, info, warn, error, fatal}. ",
65+
EnvVars: []string{"TUNNEL_LOGLEVEL"},
66+
},
67+
},
68+
}
69+
}
70+
71+
// Middleware validation error struct for returning to the eyeball
72+
type managementError struct {
73+
Code int `json:"code,omitempty"`
74+
Message string `json:"message,omitempty"`
75+
}
76+
77+
// Middleware validation error HTTP response JSON for returning to the eyeball
78+
type managementErrorResponse struct {
79+
Success bool `json:"success,omitempty"`
80+
Errors []managementError `json:"errors,omitempty"`
81+
}
82+
83+
func handleValidationError(resp *http.Response, log *zerolog.Logger) {
84+
if resp.StatusCode == 530 {
85+
log.Error().Msgf("no cloudflared connector available or reachable via management request (a recent version of cloudflared is required to use streaming logs)")
86+
}
87+
var managementErr managementErrorResponse
88+
err := json.NewDecoder(resp.Body).Decode(&managementErr)
89+
if err != nil {
90+
log.Error().Msgf("unable to start management log streaming session: http response code returned %d", resp.StatusCode)
91+
return
92+
}
93+
if managementErr.Success || len(managementErr.Errors) == 0 {
94+
log.Error().Msgf("management tunnel validation returned success with invalid HTTP response code to convert to a WebSocket request")
95+
return
96+
}
97+
for _, e := range managementErr.Errors {
98+
log.Error().Msgf("management request failed validation: (%d) %s", e.Code, e.Message)
99+
}
100+
}
101+
102+
// logger will be created to emit only against the os.Stderr as to not obstruct with normal output from
103+
// management requests
104+
func createLogger(c *cli.Context) *zerolog.Logger {
105+
level, levelErr := zerolog.ParseLevel(c.String(logger.LogLevelFlag))
106+
if levelErr != nil {
107+
level = zerolog.InfoLevel
108+
}
109+
log := zerolog.New(zerolog.ConsoleWriter{
110+
Out: colorable.NewColorable(os.Stderr),
111+
TimeFormat: time.RFC3339,
112+
}).With().Timestamp().Logger().Level(level)
113+
return &log
114+
}
115+
116+
// Run implements a foreground runner
117+
func Run(c *cli.Context) error {
118+
log := createLogger(c)
119+
120+
signals := make(chan os.Signal, 10)
121+
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
122+
defer signal.Stop(signals)
123+
124+
managementHostname := c.String("management-hostname")
125+
token := c.String("token")
126+
u := url.URL{Scheme: "wss", Host: managementHostname, Path: "/logs", RawQuery: "access_token=" + token}
127+
128+
header := make(http.Header)
129+
header.Add("User-Agent", "cloudflared/"+version)
130+
trace := c.String("trace")
131+
if trace != "" {
132+
header["cf-trace-id"] = []string{trace}
133+
}
134+
ctx := c.Context
135+
conn, resp, err := websocket.Dial(ctx, u.String(), &websocket.DialOptions{
136+
HTTPHeader: header,
137+
})
138+
if err != nil {
139+
if resp != nil && resp.StatusCode != http.StatusSwitchingProtocols {
140+
handleValidationError(resp, log)
141+
return nil
142+
}
143+
log.Error().Err(err).Msgf("unable to start management log streaming session")
144+
return nil
145+
}
146+
defer conn.Close(websocket.StatusInternalError, "management connection was closed abruptly")
147+
148+
// Once connection is established, send start_streaming event to begin receiving logs
149+
err = management.WriteEvent(conn, ctx, &management.EventStartStreaming{
150+
ClientEvent: management.ClientEvent{Type: management.StartStreaming},
151+
})
152+
if err != nil {
153+
log.Error().Err(err).Msg("unable to request logs from management tunnel")
154+
return nil
155+
}
156+
157+
readerDone := make(chan struct{})
158+
159+
go func() {
160+
defer close(readerDone)
161+
for {
162+
select {
163+
case <-ctx.Done():
164+
return
165+
default:
166+
event, err := management.ReadServerEvent(conn, ctx)
167+
if err != nil {
168+
if closeErr := management.AsClosed(err); closeErr != nil {
169+
// If the client (or the server) already closed the connection, don't continue to
170+
// attempt to read from the client.
171+
if closeErr.Code == websocket.StatusNormalClosure {
172+
return
173+
}
174+
// Only log abnormal closures
175+
log.Error().Msgf("received remote closure: (%d) %s", closeErr.Code, closeErr.Reason)
176+
return
177+
}
178+
log.Err(err).Msg("unable to read event from server")
179+
return
180+
}
181+
switch event.Type {
182+
case management.Logs:
183+
logs, ok := management.IntoServerEvent(event, management.Logs)
184+
if !ok {
185+
log.Error().Msgf("invalid logs event")
186+
continue
187+
}
188+
// Output all the logs received to stdout
189+
for _, l := range logs.Logs {
190+
fmt.Printf("%s %s %s %s\n", l.Timestamp, l.Level, l.Event, l.Message)
191+
}
192+
case management.UnknownServerEventType:
193+
fallthrough
194+
default:
195+
log.Debug().Msgf("unexpected log event type: %s", event.Type)
196+
}
197+
}
198+
}
199+
}()
200+
201+
for {
202+
select {
203+
case <-ctx.Done():
204+
return nil
205+
case <-readerDone:
206+
return nil
207+
case <-signals:
208+
log.Debug().Msg("closing management connection")
209+
// Cleanly close the connection by sending a close message and then
210+
// waiting (with timeout) for the server to close the connection.
211+
conn.Close(websocket.StatusNormalClosure, "")
212+
select {
213+
case <-readerDone:
214+
case <-time.After(time.Second):
215+
}
216+
return nil
217+
}
218+
}
219+
}

cmd/cloudflared/tunnel/cmd.go

Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,6 @@ const (
8080
// uiFlag is to enable launching cloudflared in interactive UI mode
8181
uiFlag = "ui"
8282

83-
debugLevelWarning = "At debug level cloudflared will log request URL, method, protocol, content length, as well as, all request and response headers. " +
84-
"This can expose sensitive information in your logs."
85-
8683
LogFieldCommand = "command"
8784
LogFieldExpandedPath = "expandedPath"
8885
LogFieldPIDPathname = "pidPathname"
@@ -541,7 +538,7 @@ func addPortIfMissing(uri *url.URL, port int) string {
541538
func tunnelFlags(shouldHide bool) []cli.Flag {
542539
flags := configureCloudflaredFlags(shouldHide)
543540
flags = append(flags, configureProxyFlags(shouldHide)...)
544-
flags = append(flags, configureLoggingFlags(shouldHide)...)
541+
flags = append(flags, cliutil.ConfigureLoggingFlags(shouldHide)...)
545542
flags = append(flags, configureProxyDNSFlags(shouldHide)...)
546543
flags = append(flags, []cli.Flag{
547544
credentialsFileFlag,
@@ -1017,44 +1014,6 @@ func sshFlags(shouldHide bool) []cli.Flag {
10171014
}
10181015
}
10191016

1020-
func configureLoggingFlags(shouldHide bool) []cli.Flag {
1021-
return []cli.Flag{
1022-
altsrc.NewStringFlag(&cli.StringFlag{
1023-
Name: logger.LogLevelFlag,
1024-
Value: "info",
1025-
Usage: "Application logging level {debug, info, warn, error, fatal}. " + debugLevelWarning,
1026-
EnvVars: []string{"TUNNEL_LOGLEVEL"},
1027-
Hidden: shouldHide,
1028-
}),
1029-
altsrc.NewStringFlag(&cli.StringFlag{
1030-
Name: logger.LogTransportLevelFlag,
1031-
Aliases: []string{"proto-loglevel"}, // This flag used to be called proto-loglevel
1032-
Value: "info",
1033-
Usage: "Transport logging level(previously called protocol logging level) {debug, info, warn, error, fatal}",
1034-
EnvVars: []string{"TUNNEL_PROTO_LOGLEVEL", "TUNNEL_TRANSPORT_LOGLEVEL"},
1035-
Hidden: shouldHide,
1036-
}),
1037-
altsrc.NewStringFlag(&cli.StringFlag{
1038-
Name: logger.LogFileFlag,
1039-
Usage: "Save application log to this file for reporting issues.",
1040-
EnvVars: []string{"TUNNEL_LOGFILE"},
1041-
Hidden: shouldHide,
1042-
}),
1043-
altsrc.NewStringFlag(&cli.StringFlag{
1044-
Name: logger.LogDirectoryFlag,
1045-
Usage: "Save application log to this directory for reporting issues.",
1046-
EnvVars: []string{"TUNNEL_LOGDIRECTORY"},
1047-
Hidden: shouldHide,
1048-
}),
1049-
altsrc.NewStringFlag(&cli.StringFlag{
1050-
Name: "trace-output",
1051-
Usage: "Name of trace output file, generated when cloudflared stops.",
1052-
EnvVars: []string{"TUNNEL_TRACE_OUTPUT"},
1053-
Hidden: shouldHide,
1054-
}),
1055-
}
1056-
}
1057-
10581017
func configureProxyDNSFlags(shouldHide bool) []cli.Flag {
10591018
return []cli.Flag{
10601019
altsrc.NewBoolFlag(&cli.BoolFlag{

cmd/cloudflared/tunnel/subcommands.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -932,7 +932,7 @@ func commandHelpTemplate() string {
932932
for _, f := range configureCloudflaredFlags(false) {
933933
parentFlagsHelp += fmt.Sprintf(" %s\n\t", f)
934934
}
935-
for _, f := range configureLoggingFlags(false) {
935+
for _, f := range cliutil.ConfigureLoggingFlags(false) {
936936
parentFlagsHelp += fmt.Sprintf(" %s\n\t", f)
937937
}
938938
const template = `NAME:

management/events.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,3 +210,11 @@ func IsClosed(err error, log *zerolog.Logger) bool {
210210
}
211211
return false
212212
}
213+
214+
func AsClosed(err error) *websocket.CloseError {
215+
var closeErr websocket.CloseError
216+
if errors.As(err, &closeErr) {
217+
return &closeErr
218+
}
219+
return nil
220+
}

0 commit comments

Comments
 (0)