2020-05-21 20:36:49 +00:00
package tunnel
import (
2020-06-15 18:33:41 +00:00
"crypto/rand"
2020-05-21 20:36:49 +00:00
"encoding/json"
"fmt"
2020-06-16 22:43:22 +00:00
"io/ioutil"
2020-05-21 20:36:49 +00:00
"os"
2020-06-15 18:33:41 +00:00
"path/filepath"
2020-09-14 21:41:02 +00:00
"regexp"
2020-06-11 23:44:39 +00:00
"sort"
"strings"
2020-07-02 22:22:23 +00:00
"text/tabwriter"
2020-05-21 20:36:49 +00:00
"time"
2020-06-25 18:25:39 +00:00
"github.com/google/uuid"
2021-03-23 14:30:43 +00:00
homedir "github.com/mitchellh/go-homedir"
2020-05-21 20:36:49 +00:00
"github.com/pkg/errors"
2020-08-05 10:49:53 +00:00
"github.com/urfave/cli/v2"
2020-10-09 17:07:08 +00:00
"github.com/urfave/cli/v2/altsrc"
2020-09-15 13:36:36 +00:00
"golang.org/x/net/idna"
2021-03-23 14:30:43 +00:00
yaml "gopkg.in/yaml.v2"
2020-05-21 20:36:49 +00:00
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
2021-03-08 16:46:23 +00:00
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
"github.com/cloudflare/cloudflared/config"
2020-10-14 13:42:00 +00:00
"github.com/cloudflare/cloudflared/connection"
2020-05-21 20:36:49 +00:00
"github.com/cloudflare/cloudflared/tunnelstore"
)
2020-07-02 09:31:12 +00:00
const (
2021-05-19 17:38:09 +00:00
allSortByOptions = "name, id, createdAt, deletedAt, numConnections"
connsSortByOptions = "id, startedAt, numConnections, version"
CredFileFlagAlias = "cred-file"
CredFileFlag = "credentials-file"
2021-09-03 15:01:45 +00:00
CredContentsFlag = "credentials-contents"
2021-05-19 17:38:09 +00:00
overwriteDNSFlagName = "overwrite-dns"
2020-12-28 18:10:01 +00:00
LogFieldTunnelID = "tunnelID"
2020-07-02 09:31:12 +00:00
)
2020-05-21 20:36:49 +00:00
var (
2020-06-16 22:55:33 +00:00
showDeletedFlag = & cli . BoolFlag {
Name : "show-deleted" ,
Aliases : [ ] string { "d" } ,
Usage : "Include deleted tunnels in the list" ,
}
2020-08-05 10:49:53 +00:00
listNameFlag = & cli . StringFlag {
Name : "name" ,
Aliases : [ ] string { "n" } ,
2020-09-15 13:36:36 +00:00
Usage : "List tunnels with the given `NAME`" ,
2020-08-05 10:49:53 +00:00
}
2021-07-30 11:00:26 +00:00
listNamePrefixFlag = & cli . StringFlag {
Name : "name-prefix" ,
Aliases : [ ] string { "np" } ,
Usage : "List tunnels that start with the give `NAME` prefix" ,
}
listExcludeNamePrefixFlag = & cli . StringFlag {
Name : "exclude-name-prefix" ,
Aliases : [ ] string { "enp" } ,
Usage : "List tunnels whose `NAME` does not start with the given prefix" ,
}
2020-08-05 10:49:53 +00:00
listExistedAtFlag = & cli . TimestampFlag {
2020-09-16 12:15:49 +00:00
Name : "when" ,
Aliases : [ ] string { "w" } ,
Usage : "List tunnels that are active at the given `TIME` in RFC3339 format" ,
Layout : tunnelstore . TimeLayout ,
2020-09-15 13:36:36 +00:00
DefaultText : fmt . Sprintf ( "current time, %s" , time . Now ( ) . Format ( tunnelstore . TimeLayout ) ) ,
2020-08-05 10:49:53 +00:00
}
listIDFlag = & cli . StringFlag {
Name : "id" ,
Aliases : [ ] string { "i" } ,
2020-09-15 13:36:36 +00:00
Usage : "List tunnel by `ID`" ,
2020-08-05 10:49:53 +00:00
}
2020-08-05 20:43:56 +00:00
showRecentlyDisconnected = & cli . BoolFlag {
Name : "show-recently-disconnected" ,
Aliases : [ ] string { "rd" } ,
Usage : "Include connections that have recently disconnected in the list" ,
}
2021-03-15 18:30:17 +00:00
outputFormatFlag = & cli . StringFlag {
2020-04-29 20:51:32 +00:00
Name : "output" ,
2020-05-21 20:36:49 +00:00
Aliases : [ ] string { "o" } ,
2020-04-29 20:51:32 +00:00
Usage : "Render output using given `FORMAT`. Valid options are 'json' or 'yaml'" ,
2021-03-15 18:30:17 +00:00
}
2021-02-03 18:22:57 +00:00
sortByFlag = & cli . StringFlag {
Name : "sort-by" ,
Value : "name" ,
Usage : fmt . Sprintf ( "Sorts the list of tunnels by the given field. Valid options are {%s}" , allSortByOptions ) ,
EnvVars : [ ] string { "TUNNEL_LIST_SORT_BY" } ,
}
invertSortFlag = & cli . BoolFlag {
Name : "invert-sort" ,
Usage : "Inverts the sort order of the tunnel list." ,
EnvVars : [ ] string { "TUNNEL_LIST_INVERT_SORT" } ,
}
2020-10-09 17:07:08 +00:00
forceFlag = altsrc . NewBoolFlag ( & cli . BoolFlag {
2020-06-16 22:43:22 +00:00
Name : "force" ,
Aliases : [ ] string { "f" } ,
Usage : "By default, if a tunnel is currently being run from a cloudflared, you can't " +
"simultaneously rerun it again from a second cloudflared. The --force flag lets you " +
"overwrite the previous tunnel. If you want to use a single hostname with multiple " +
"tunnels, you can do so with Cloudflare's Load Balancer product." ,
2020-10-09 17:07:08 +00:00
} )
2021-03-02 22:16:38 +00:00
featuresFlag = altsrc . NewStringSliceFlag ( & cli . StringSliceFlag {
Name : "features" ,
Aliases : [ ] string { "F" } ,
Usage : "Opt into various features that are still being developed or tested." ,
} )
2021-03-24 02:52:14 +00:00
credentialsFileFlagCLIOnly = & cli . StringFlag {
2020-11-23 21:36:16 +00:00
Name : CredFileFlag ,
Aliases : [ ] string { CredFileFlagAlias } ,
2021-02-04 17:57:50 +00:00
Usage : "Filepath at which to read/write the tunnel credentials" ,
2020-10-08 10:12:26 +00:00
EnvVars : [ ] string { "TUNNEL_CRED_FILE" } ,
2021-03-24 02:52:14 +00:00
}
2021-09-03 15:01:45 +00:00
credentialsFileFlag = altsrc . NewStringFlag ( credentialsFileFlagCLIOnly )
credentialsContentsFlag = altsrc . NewStringFlag ( & cli . StringFlag {
Name : CredContentsFlag ,
Usage : "Contents of the tunnel credentials JSON file to use. When provided along with credentials-file, this will take precedence." ,
EnvVars : [ ] string { "TUNNEL_CRED_CONTENTS" } ,
} )
forceDeleteFlag = & cli . BoolFlag {
2020-07-10 22:11:31 +00:00
Name : "force" ,
Aliases : [ ] string { "f" } ,
2021-02-11 21:22:51 +00:00
Usage : "Cleans up any stale connections before the tunnel is deleted. cloudflared will not " +
"delete a tunnel with connections without this flag." ,
2020-10-08 10:12:26 +00:00
EnvVars : [ ] string { "TUNNEL_RUN_FORCE_OVERWRITE" } ,
2020-07-10 22:11:31 +00:00
}
2020-10-14 10:28:07 +00:00
selectProtocolFlag = altsrc . NewStringFlag ( & cli . StringFlag {
2020-10-08 09:48:10 +00:00
Name : "protocol" ,
2021-05-17 14:48:07 +00:00
Value : "auto" ,
2020-10-08 09:48:10 +00:00
Aliases : [ ] string { "p" } ,
2020-10-14 13:42:00 +00:00
Usage : fmt . Sprintf ( "Protocol implementation to connect with Cloudflare's edge network. %s" , connection . AvailableProtocolFlagMessage ) ,
2020-10-08 10:12:26 +00:00
EnvVars : [ ] string { "TUNNEL_TRANSPORT_PROTOCOL" } ,
2020-10-08 09:48:10 +00:00
Hidden : true ,
2020-10-14 10:28:07 +00:00
} )
2021-03-15 18:30:17 +00:00
sortInfoByFlag = & cli . StringFlag {
Name : "sort-by" ,
Value : "createdAt" ,
Usage : fmt . Sprintf ( "Sorts the list of connections of a tunnel by the given field. Valid options are {%s}" , connsSortByOptions ) ,
EnvVars : [ ] string { "TUNNEL_INFO_SORT_BY" } ,
}
invertInfoSortFlag = & cli . BoolFlag {
Name : "invert-sort" ,
Usage : "Inverts the sort order of the tunnel info." ,
EnvVars : [ ] string { "TUNNEL_INFO_INVERT_SORT" } ,
}
2021-03-19 23:26:51 +00:00
cleanupClientFlag = & cli . StringFlag {
Name : "connector-id" ,
Aliases : [ ] string { "c" } ,
Usage : ` Constraints the cleanup to stop the connections of a single Connector (by its ID). You can find the various Connectors (and their IDs) currently connected to your tunnel via 'cloudflared tunnel info <name>'. ` ,
EnvVars : [ ] string { "TUNNEL_CLEANUP_CONNECTOR" } ,
}
2021-05-19 17:38:09 +00:00
overwriteDNSFlag = & cli . BoolFlag {
Name : overwriteDNSFlagName ,
Aliases : [ ] string { "f" } ,
Usage : ` Overwrites existing DNS records with this hostname ` ,
EnvVars : [ ] string { "TUNNEL_FORCE_PROVISIONING_DNS" } ,
}
2020-05-21 20:36:49 +00:00
)
func buildCreateCommand ( ) * cli . Command {
return & cli . Command {
2020-10-08 10:12:26 +00:00
Name : "create" ,
2021-03-16 22:36:46 +00:00
Action : cliutil . ConfiguredAction ( createCommand ) ,
2020-10-08 10:12:26 +00:00
Usage : "Create a new tunnel with given name" ,
UsageText : "cloudflared tunnel [tunnel command options] create [subcommand options] NAME" ,
Description : ` Creates a tunnel , registers it with Cloudflare edge and generates credential file used to run this tunnel .
2020-11-20 22:22:32 +00:00
Use "cloudflared tunnel route" subcommand to map a DNS name to this tunnel and "cloudflared tunnel run" to start the connection .
2020-10-09 17:07:08 +00:00
For example , to create a tunnel named ' my - tunnel ' run :
$ cloudflared tunnel create my - tunnel ` ,
2021-03-24 02:52:14 +00:00
Flags : [ ] cli . Flag { outputFormatFlag , credentialsFileFlagCLIOnly } ,
2020-09-29 09:47:50 +00:00
CustomHelpTemplate : commandHelpTemplate ( ) ,
2020-05-21 20:36:49 +00:00
}
}
2020-06-15 18:33:41 +00:00
// generateTunnelSecret as an array of 32 bytes using secure random number generator
func generateTunnelSecret ( ) ( [ ] byte , error ) {
randomBytes := make ( [ ] byte , 32 )
_ , err := rand . Read ( randomBytes )
return randomBytes , err
}
2020-08-07 12:29:53 +00:00
func createCommand ( c * cli . Context ) error {
sc , err := newSubcommandContext ( c )
2020-04-29 20:51:32 +00:00
if err != nil {
return errors . Wrap ( err , "error setting up logger" )
}
2020-08-07 12:29:53 +00:00
if c . NArg ( ) != 1 {
return cliutil . UsageError ( ` "cloudflared tunnel create" requires exactly 1 argument, the name of tunnel to create. ` )
2020-05-21 20:36:49 +00:00
}
2020-08-07 12:29:53 +00:00
name := c . Args ( ) . First ( )
2020-05-21 20:36:49 +00:00
2021-02-28 23:24:38 +00:00
warningChecker := updater . StartWarningCheck ( c )
defer warningChecker . LogWarningIfAny ( sc . log )
2021-02-04 17:57:50 +00:00
_ , err = sc . create ( name , c . String ( CredFileFlag ) )
2020-08-07 12:29:53 +00:00
return errors . Wrap ( err , "failed to create tunnel" )
2020-05-21 20:36:49 +00:00
}
2020-07-06 08:01:48 +00:00
func tunnelFilePath ( tunnelID uuid . UUID , directory string ) ( string , error ) {
2020-06-15 18:33:41 +00:00
fileName := fmt . Sprintf ( "%v.json" , tunnelID )
2020-07-02 09:31:12 +00:00
filePath := filepath . Clean ( fmt . Sprintf ( "%s/%s" , directory , fileName ) )
return homedir . Expand ( filePath )
2020-06-15 18:33:41 +00:00
}
2021-03-24 03:32:40 +00:00
// writeTunnelCredentials saves `credentials` as a JSON into `filePath`, only if
// the file does not exist already
func writeTunnelCredentials ( filePath string , credentials * connection . Credentials ) error {
if _ , err := os . Stat ( filePath ) ; ! os . IsNotExist ( err ) {
if err == nil {
return fmt . Errorf ( "%s already exists" , filePath )
}
return err
2021-02-04 17:57:50 +00:00
}
2020-11-23 21:36:16 +00:00
body , err := json . Marshal ( credentials )
2020-07-02 09:31:12 +00:00
if err != nil {
2021-03-24 03:32:40 +00:00
return errors . Wrap ( err , "Unable to marshal tunnel credentials to JSON" )
2020-07-02 09:31:12 +00:00
}
2021-03-24 03:32:40 +00:00
return ioutil . WriteFile ( filePath , body , 400 )
2020-07-02 09:31:12 +00:00
}
2020-05-21 20:36:49 +00:00
func buildListCommand ( ) * cli . Command {
return & cli . Command {
2021-02-03 18:22:57 +00:00
Name : "list" ,
2021-03-16 22:36:46 +00:00
Action : cliutil . ConfiguredAction ( listCommand ) ,
2021-02-03 18:22:57 +00:00
Usage : "List existing tunnels" ,
UsageText : "cloudflared tunnel [tunnel command options] list [subcommand options]" ,
Description : "cloudflared tunnel list will display all active tunnels, their created time and associated connections. Use -d flag to include deleted tunnels. See the list of options to filter the list" ,
Flags : [ ] cli . Flag {
outputFormatFlag ,
showDeletedFlag ,
listNameFlag ,
2021-07-30 11:00:26 +00:00
listNamePrefixFlag ,
listExcludeNamePrefixFlag ,
2021-02-03 18:22:57 +00:00
listExistedAtFlag ,
listIDFlag ,
showRecentlyDisconnected ,
sortByFlag ,
invertSortFlag ,
} ,
2020-09-29 09:47:50 +00:00
CustomHelpTemplate : commandHelpTemplate ( ) ,
2020-05-21 20:36:49 +00:00
}
}
2020-08-07 12:29:53 +00:00
func listCommand ( c * cli . Context ) error {
sc , err := newSubcommandContext ( c )
2020-05-21 20:36:49 +00:00
if err != nil {
2020-08-05 10:49:53 +00:00
return err
2020-05-21 20:36:49 +00:00
}
2021-03-15 18:30:17 +00:00
warningChecker := updater . StartWarningCheck ( c )
defer warningChecker . LogWarningIfAny ( sc . log )
2020-08-05 10:49:53 +00:00
filter := tunnelstore . NewFilter ( )
if ! c . Bool ( "show-deleted" ) {
2020-08-07 12:29:53 +00:00
filter . NoDeleted ( )
2020-08-05 10:49:53 +00:00
}
if name := c . String ( "name" ) ; name != "" {
filter . ByName ( name )
}
2021-07-30 11:00:26 +00:00
if namePrefix := c . String ( "name-prefix" ) ; namePrefix != "" {
filter . ByNamePrefix ( namePrefix )
}
if excludePrefix := c . String ( "exclude-name-prefix" ) ; excludePrefix != "" {
filter . ExcludeNameWithPrefix ( excludePrefix )
}
2020-08-05 10:49:53 +00:00
if existedAt := c . Timestamp ( "time" ) ; existedAt != nil {
filter . ByExistedAt ( * existedAt )
}
if id := c . String ( "id" ) ; id != "" {
tunnelID , err := uuid . Parse ( id )
if err != nil {
return errors . Wrapf ( err , "%s is not a valid tunnel ID" , id )
2020-06-16 22:55:33 +00:00
}
2020-08-05 10:49:53 +00:00
filter . ByTunnelID ( tunnelID )
}
2021-10-19 14:01:23 +00:00
if maxFetch := c . Int ( "max-fetch-size" ) ; maxFetch > 0 {
filter . MaxFetchSize ( uint ( maxFetch ) )
2021-10-18 09:32:33 +00:00
}
2020-08-05 10:49:53 +00:00
2020-08-07 12:29:53 +00:00
tunnels , err := sc . list ( filter )
2020-08-05 10:49:53 +00:00
if err != nil {
2020-08-07 12:29:53 +00:00
return err
2020-06-16 22:55:33 +00:00
}
2021-02-03 18:22:57 +00:00
// Sort the tunnels
sortBy := c . String ( "sort-by" )
invalidSortField := false
sort . Slice ( tunnels , func ( i , j int ) bool {
cmp := func ( ) bool {
switch sortBy {
case "name" :
return tunnels [ i ] . Name < tunnels [ j ] . Name
case "id" :
return tunnels [ i ] . ID . String ( ) < tunnels [ j ] . ID . String ( )
case "createdAt" :
return tunnels [ i ] . CreatedAt . Unix ( ) < tunnels [ j ] . CreatedAt . Unix ( )
case "deletedAt" :
return tunnels [ i ] . DeletedAt . Unix ( ) < tunnels [ j ] . DeletedAt . Unix ( )
case "numConnections" :
return len ( tunnels [ i ] . Connections ) < len ( tunnels [ j ] . Connections )
default :
invalidSortField = true
return tunnels [ i ] . Name < tunnels [ j ] . Name
}
} ( )
if c . Bool ( "invert-sort" ) {
return ! cmp
}
return cmp
} )
if invalidSortField {
sc . log . Error ( ) . Msgf ( "%s is not a valid sort field. Valid sort fields are %s. Defaulting to 'name'." , sortBy , allSortByOptions )
}
2020-05-21 20:36:49 +00:00
if outputFormat := c . String ( outputFormatFlag . Name ) ; outputFormat != "" {
return renderOutput ( outputFormat , tunnels )
}
2020-07-02 22:22:23 +00:00
2020-05-21 20:36:49 +00:00
if len ( tunnels ) > 0 {
2020-12-22 02:06:46 +00:00
formatAndPrintTunnelList ( tunnels , c . Bool ( "show-recently-disconnected" ) )
2020-05-21 20:36:49 +00:00
} else {
fmt . Println ( "You have no tunnels, use 'cloudflared tunnel create' to define a new tunnel" )
}
2021-02-28 23:24:38 +00:00
2020-05-21 20:36:49 +00:00
return nil
}
2020-12-22 02:06:46 +00:00
func formatAndPrintTunnelList ( tunnels [ ] * tunnelstore . Tunnel , showRecentlyDisconnected bool ) {
2021-03-15 18:30:17 +00:00
writer := tabWriter ( )
2020-09-14 22:23:23 +00:00
defer writer . Flush ( )
2020-07-02 22:22:23 +00:00
2021-03-15 18:30:17 +00:00
_ , _ = fmt . Fprintln ( writer , "You can obtain more detailed information for each tunnel with `cloudflared tunnel info <name/uuid>`" )
2020-07-02 22:22:23 +00:00
// Print column headers with tabbed columns
2020-11-25 06:55:13 +00:00
_ , _ = fmt . Fprintln ( writer , "ID\tNAME\tCREATED\tCONNECTIONS\t" )
2020-07-02 22:22:23 +00:00
// Loop through tunnels, create formatted string for each, and print using tabwriter
for _ , t := range tunnels {
2020-08-05 20:43:56 +00:00
formattedStr := fmt . Sprintf (
"%s\t%s\t%s\t%s\t" ,
t . ID ,
t . Name ,
t . CreatedAt . Format ( time . RFC3339 ) ,
fmtConnections ( t . Connections , showRecentlyDisconnected ) ,
)
2020-11-25 06:55:13 +00:00
_ , _ = fmt . Fprintln ( writer , formattedStr )
2020-07-02 22:22:23 +00:00
}
}
2020-08-05 20:43:56 +00:00
func fmtConnections ( connections [ ] tunnelstore . Connection , showRecentlyDisconnected bool ) string {
2020-06-11 23:44:39 +00:00
// Count connections per colo
numConnsPerColo := make ( map [ string ] uint , len ( connections ) )
for _ , connection := range connections {
2020-08-05 20:43:56 +00:00
if ! connection . IsPendingReconnect || showRecentlyDisconnected {
numConnsPerColo [ connection . ColoName ] ++
}
2020-06-11 23:44:39 +00:00
}
// Get sorted list of colos
sortedColos := [ ] string { }
for coloName := range numConnsPerColo {
sortedColos = append ( sortedColos , coloName )
}
sort . Strings ( sortedColos )
// Map each colo to its frequency, combine into output string.
var output [ ] string
for _ , coloName := range sortedColos {
output = append ( output , fmt . Sprintf ( "%dx%s" , numConnsPerColo [ coloName ] , coloName ) )
}
return strings . Join ( output , ", " )
}
2021-03-15 18:30:17 +00:00
func buildInfoCommand ( ) * cli . Command {
return & cli . Command {
Name : "info" ,
Action : cliutil . ConfiguredAction ( tunnelInfo ) ,
Usage : "List details about the active connectors for a tunnel" ,
UsageText : "cloudflared tunnel [tunnel command options] info [subcommand options] [TUNNEL]" ,
Description : "cloudflared tunnel info displays details about the active connectors for a given tunnel (identified by name or uuid)." ,
Flags : [ ] cli . Flag {
outputFormatFlag ,
showRecentlyDisconnected ,
sortInfoByFlag ,
invertInfoSortFlag ,
} ,
CustomHelpTemplate : commandHelpTemplate ( ) ,
}
}
func tunnelInfo ( c * cli . Context ) error {
sc , err := newSubcommandContext ( c )
if err != nil {
return err
}
warningChecker := updater . StartWarningCheck ( c )
defer warningChecker . LogWarningIfAny ( sc . log )
2021-03-26 19:45:35 +00:00
if c . NArg ( ) != 1 {
return cliutil . UsageError ( ` "cloudflared tunnel info" accepts exactly one argument, the ID or name of the tunnel to get info about. ` )
2021-03-15 18:30:17 +00:00
}
tunnelID , err := sc . findID ( c . Args ( ) . First ( ) )
if err != nil {
return errors . Wrap ( err , "error parsing tunnel ID" )
}
client , err := sc . client ( )
if err != nil {
return err
}
clients , err := client . ListActiveClients ( tunnelID )
if err != nil {
return err
}
sortBy := c . String ( "sort-by" )
invalidSortField := false
sort . Slice ( clients , func ( i , j int ) bool {
cmp := func ( ) bool {
switch sortBy {
case "id" :
return clients [ i ] . ID . String ( ) < clients [ j ] . ID . String ( )
case "createdAt" :
return clients [ i ] . RunAt . Unix ( ) < clients [ j ] . RunAt . Unix ( )
case "numConnections" :
return len ( clients [ i ] . Connections ) < len ( clients [ j ] . Connections )
case "version" :
return clients [ i ] . Version < clients [ j ] . Version
default :
invalidSortField = true
return clients [ i ] . RunAt . Unix ( ) < clients [ j ] . RunAt . Unix ( )
}
} ( )
if c . Bool ( "invert-sort" ) {
return ! cmp
}
return cmp
} )
if invalidSortField {
sc . log . Error ( ) . Msgf ( "%s is not a valid sort field. Valid sort fields are %s. Defaulting to 'name'." , sortBy , connsSortByOptions )
}
tunnel , err := getTunnel ( sc , tunnelID )
if err != nil {
return err
}
info := Info {
tunnel . ID ,
tunnel . Name ,
tunnel . CreatedAt ,
clients ,
}
if outputFormat := c . String ( outputFormatFlag . Name ) ; outputFormat != "" {
return renderOutput ( outputFormat , info )
}
if len ( clients ) > 0 {
formatAndPrintConnectionsList ( info , c . Bool ( "show-recently-disconnected" ) )
} else {
fmt . Printf ( "Your tunnel %s does not have any active connection.\n" , tunnelID )
}
return nil
}
func getTunnel ( sc * subcommandContext , tunnelID uuid . UUID ) ( * tunnelstore . Tunnel , error ) {
filter := tunnelstore . NewFilter ( )
filter . ByTunnelID ( tunnelID )
tunnels , err := sc . list ( filter )
if err != nil {
return nil , err
}
if len ( tunnels ) != 1 {
return nil , errors . Errorf ( "Expected to find a single tunnel with uuid %v but found %d tunnels." , tunnelID , len ( tunnels ) )
}
return tunnels [ 0 ] , nil
}
func formatAndPrintConnectionsList ( tunnelInfo Info , showRecentlyDisconnected bool ) {
writer := tabWriter ( )
defer writer . Flush ( )
2021-03-30 19:14:22 +00:00
// Print the general tunnel info table
2021-03-15 18:30:17 +00:00
_ , _ = fmt . Fprintf ( writer , "NAME: %s\nID: %s\nCREATED: %s\n\n" , tunnelInfo . Name , tunnelInfo . ID , tunnelInfo . CreatedAt )
2021-03-30 19:14:22 +00:00
// Determine whether to print the connector table
shouldDisplayTable := false
for _ , c := range tunnelInfo . Connectors {
conns := fmtConnections ( c . Connections , showRecentlyDisconnected )
if len ( conns ) > 0 {
shouldDisplayTable = true
}
}
if ! shouldDisplayTable {
fmt . Println ( "This tunnel has no active connectors." )
return
}
// Print the connector table
2021-03-15 18:30:17 +00:00
_ , _ = fmt . Fprintln ( writer , "CONNECTOR ID\tCREATED\tARCHITECTURE\tVERSION\tORIGIN IP\tEDGE\t" )
for _ , c := range tunnelInfo . Connectors {
2021-03-30 19:14:22 +00:00
conns := fmtConnections ( c . Connections , showRecentlyDisconnected )
if len ( conns ) == 0 {
continue
2021-03-15 18:30:17 +00:00
}
2021-03-30 19:14:22 +00:00
originIp := c . Connections [ 0 ] . OriginIP . String ( )
2021-03-15 18:30:17 +00:00
formattedStr := fmt . Sprintf (
"%s\t%s\t%s\t%s\t%s\t%s\t" ,
c . ID ,
c . RunAt . Format ( time . RFC3339 ) ,
c . Arch ,
c . Version ,
originIp ,
2021-03-30 19:14:22 +00:00
conns ,
2021-03-15 18:30:17 +00:00
)
_ , _ = fmt . Fprintln ( writer , formattedStr )
}
}
func tabWriter ( ) * tabwriter . Writer {
const (
minWidth = 0
tabWidth = 8
padding = 1
padChar = ' '
flags = 0
)
writer := tabwriter . NewWriter ( os . Stdout , minWidth , tabWidth , padding , padChar , flags )
return writer
}
2020-05-21 20:36:49 +00:00
func buildDeleteCommand ( ) * cli . Command {
return & cli . Command {
2020-09-29 09:47:50 +00:00
Name : "delete" ,
2021-03-16 22:36:46 +00:00
Action : cliutil . ConfiguredAction ( deleteCommand ) ,
2020-09-29 09:47:50 +00:00
Usage : "Delete existing tunnel by UUID or name" ,
2020-10-09 17:07:08 +00:00
UsageText : "cloudflared tunnel [tunnel command options] delete [subcommand options] TUNNEL" ,
2020-09-29 09:47:50 +00:00
Description : "cloudflared tunnel delete will delete tunnels with the given tunnel UUIDs or names. A tunnel cannot be deleted if it has active connections. To delete the tunnel unconditionally, use -f flag." ,
2021-03-24 02:52:14 +00:00
Flags : [ ] cli . Flag { credentialsFileFlagCLIOnly , forceDeleteFlag } ,
2020-09-29 09:47:50 +00:00
CustomHelpTemplate : commandHelpTemplate ( ) ,
2020-05-21 20:36:49 +00:00
}
}
2020-08-07 12:29:53 +00:00
func deleteCommand ( c * cli . Context ) error {
sc , err := newSubcommandContext ( c )
2020-07-06 08:01:48 +00:00
if err != nil {
2020-08-07 12:29:53 +00:00
return err
2020-07-06 08:01:48 +00:00
}
2020-05-21 20:36:49 +00:00
2020-08-07 12:29:53 +00:00
if c . NArg ( ) < 1 {
2020-08-18 21:54:05 +00:00
return cliutil . UsageError ( ` "cloudflared tunnel delete" requires at least 1 argument, the ID or name of the tunnel to delete. ` )
2020-04-29 20:51:32 +00:00
}
2021-02-28 23:24:38 +00:00
warningChecker := updater . StartWarningCheck ( c )
defer warningChecker . LogWarningIfAny ( sc . log )
2020-08-18 21:54:05 +00:00
tunnelIDs , err := sc . findIDs ( c . Args ( ) . Slice ( ) )
2020-08-05 10:49:53 +00:00
if err != nil {
return err
}
2020-05-21 20:36:49 +00:00
2020-08-07 12:29:53 +00:00
return sc . delete ( tunnelIDs )
}
2020-07-06 08:01:48 +00:00
2020-05-21 20:36:49 +00:00
func renderOutput ( format string , v interface { } ) error {
switch format {
case "json" :
encoder := json . NewEncoder ( os . Stdout )
encoder . SetIndent ( "" , " " )
return encoder . Encode ( v )
case "yaml" :
return yaml . NewEncoder ( os . Stdout ) . Encode ( v )
default :
return errors . Errorf ( "Unknown output format '%s'" , format )
}
}
2020-06-16 22:43:22 +00:00
func buildRunCommand ( ) * cli . Command {
2020-09-16 12:15:49 +00:00
flags := [ ] cli . Flag {
forceFlag ,
credentialsFileFlag ,
2021-09-03 15:01:45 +00:00
credentialsContentsFlag ,
2020-10-08 09:48:10 +00:00
selectProtocolFlag ,
2021-03-02 22:16:38 +00:00
featuresFlag ,
2020-09-16 12:15:49 +00:00
}
2020-09-29 09:47:50 +00:00
flags = append ( flags , configureProxyFlags ( false ) ... )
2020-06-16 22:43:22 +00:00
return & cli . Command {
Name : "run" ,
2021-03-16 22:36:46 +00:00
Action : cliutil . ConfiguredAction ( runCommand ) ,
2020-06-16 22:43:22 +00:00
Usage : "Proxy a local web server by running the given tunnel" ,
2020-10-15 20:08:57 +00:00
UsageText : "cloudflared tunnel [tunnel command options] run [subcommand options] [TUNNEL]" ,
2020-11-20 22:22:32 +00:00
Description : ` Runs the tunnel identified by name or UUUD , creating highly available connections
2020-10-15 20:08:57 +00:00
between your server and the Cloudflare edge . You can provide name or UUID of tunnel to run either as the
last command line argument or in the configuration file using "tunnel: TUNNEL" .
2020-09-14 22:23:23 +00:00
2020-11-20 22:22:32 +00:00
This command requires the tunnel credentials file created when "cloudflared tunnel create" was run ,
2020-10-09 17:07:08 +00:00
however it does not need access to cert . pem from "cloudflared login" if you identify the tunnel by UUID .
2020-11-20 22:22:32 +00:00
If you experience other problems running the tunnel , "cloudflared tunnel cleanup" may help by removing
2020-10-09 17:07:08 +00:00
any old connection records .
2020-09-16 12:15:49 +00:00
` ,
Flags : flags ,
2020-09-29 09:47:50 +00:00
CustomHelpTemplate : commandHelpTemplate ( ) ,
2020-06-16 22:43:22 +00:00
}
}
2020-08-07 12:29:53 +00:00
func runCommand ( c * cli . Context ) error {
sc , err := newSubcommandContext ( c )
if err != nil {
return err
}
2020-10-15 20:08:57 +00:00
if c . NArg ( ) > 1 {
return cliutil . UsageError ( ` "cloudflared tunnel run" accepts only one argument, the ID or name of the tunnel to run. ` )
}
tunnelRef := c . Args ( ) . First ( )
if tunnelRef == "" {
2020-10-19 22:33:40 +00:00
// see if tunnel id was in the config file
tunnelRef = config . GetConfiguration ( ) . TunnelID
2020-10-15 20:08:57 +00:00
if tunnelRef == "" {
return cliutil . UsageError ( ` "cloudflared tunnel run" requires the ID or name of the tunnel to run as the last command line argument or in the configuration file. ` )
}
2020-06-16 22:43:22 +00:00
}
2020-10-15 20:08:57 +00:00
2021-05-10 15:17:21 +00:00
if c . String ( "hostname" ) != "" {
sc . log . Warn ( ) . Msg ( "The property `hostname` in your configuration is ignored because you configured a Named Tunnel " +
"in the property `tunnel` to run. Make sure to provision the routing (e.g. via `cloudflared tunnel route dns/lb`) or else " +
"your origin will not be reachable. You should remove the `hostname` property to avoid this warning." )
}
2020-10-15 20:08:57 +00:00
return runNamedTunnel ( sc , tunnelRef )
}
func runNamedTunnel ( sc * subcommandContext , tunnelRef string ) error {
tunnelID , err := sc . findID ( tunnelRef )
2020-06-25 18:25:39 +00:00
if err != nil {
return errors . Wrap ( err , "error parsing tunnel ID" )
}
2020-06-16 22:43:22 +00:00
2020-12-28 18:10:01 +00:00
sc . log . Info ( ) . Str ( LogFieldTunnelID , tunnelID . String ( ) ) . Msg ( "Starting tunnel" )
2020-10-15 20:08:57 +00:00
2020-08-07 12:29:53 +00:00
return sc . run ( tunnelID )
2020-06-16 22:43:22 +00:00
}
2020-07-03 08:55:11 +00:00
func buildCleanupCommand ( ) * cli . Command {
return & cli . Command {
2020-09-29 09:47:50 +00:00
Name : "cleanup" ,
2021-03-16 22:36:46 +00:00
Action : cliutil . ConfiguredAction ( cleanupCommand ) ,
2020-09-29 09:47:50 +00:00
Usage : "Cleanup tunnel connections" ,
2020-10-09 17:07:08 +00:00
UsageText : "cloudflared tunnel [tunnel command options] cleanup [subcommand options] TUNNEL" ,
2020-09-29 09:47:50 +00:00
Description : "Delete connections for tunnels with the given UUIDs or names." ,
2021-03-19 23:26:51 +00:00
Flags : [ ] cli . Flag { cleanupClientFlag } ,
2020-09-29 09:47:50 +00:00
CustomHelpTemplate : commandHelpTemplate ( ) ,
2020-07-03 08:55:11 +00:00
}
}
2020-08-07 12:29:53 +00:00
func cleanupCommand ( c * cli . Context ) error {
2020-07-03 08:55:11 +00:00
if c . NArg ( ) < 1 {
return cliutil . UsageError ( ` "cloudflared tunnel cleanup" requires at least 1 argument, the IDs of the tunnels to cleanup connections. ` )
}
2020-08-07 12:29:53 +00:00
sc , err := newSubcommandContext ( c )
2020-07-03 08:55:11 +00:00
if err != nil {
return err
}
2020-08-07 12:29:53 +00:00
2020-08-18 21:54:05 +00:00
tunnelIDs , err := sc . findIDs ( c . Args ( ) . Slice ( ) )
2020-08-05 10:49:53 +00:00
if err != nil {
return err
}
2020-07-03 08:55:11 +00:00
2020-08-07 12:29:53 +00:00
return sc . cleanupConnections ( tunnelIDs )
2020-07-03 08:55:11 +00:00
}
2020-07-06 08:01:48 +00:00
func buildRouteCommand ( ) * cli . Command {
return & cli . Command {
2020-09-29 09:47:50 +00:00
Name : "route" ,
2021-02-26 09:50:19 +00:00
Usage : "Define which traffic routed from Cloudflare edge to this tunnel: requests to a DNS hostname, to a Cloudflare Load Balancer, or traffic originating from Cloudflare WARP clients" ,
UsageText : "cloudflared tunnel [tunnel command options] route [subcommand options] [dns TUNNEL HOSTNAME]|[lb TUNNEL HOSTNAME LB-POOL]|[ip NETWORK TUNNEL]" ,
Description : ` The route command defines how Cloudflare will proxy requests to this tunnel .
To route a hostname by creating a DNS CNAME record to a tunnel :
cloudflared tunnel route dns < tunnel ID or name > < hostname >
You can read more at : https : //developers.cloudflare.com/cloudflare-one/connections/connect-apps/routing-to-tunnel/dns
To use this tunnel as a load balancer origin , creating pool and load balancer if necessary :
cloudflared tunnel route lb < tunnel ID or name > < hostname > < load balancer pool >
You can read more at : https : //developers.cloudflare.com/cloudflare-one/connections/connect-apps/routing-to-tunnel/lb
For Cloudflare WARP traffic to be routed to your private network , reachable from this tunnel as origins , use :
cloudflared tunnel route ip < network CIDR > < tunnel ID or name >
Further information about managing Cloudflare WARP traffic to your tunnel is available at :
cloudflared tunnel route ip -- help
` ,
2020-09-29 09:47:50 +00:00
CustomHelpTemplate : commandHelpTemplate ( ) ,
2020-12-22 02:06:46 +00:00
Subcommands : [ ] * cli . Command {
2021-06-07 08:20:11 +00:00
{
Name : "dns" ,
Action : cliutil . ConfiguredAction ( routeDnsCommand ) ,
Usage : "Route a hostname by creating a DNS CNAME record to a tunnel" ,
UsageText : "cloudflared tunnel route dns [TUNNEL] [HOSTNAME]" ,
Description : ` Creates a DNS CNAME record hostname that points to the tunnel. ` ,
Flags : [ ] cli . Flag { overwriteDNSFlag } ,
} ,
{
Name : "lb" ,
Action : cliutil . ConfiguredAction ( routeLbCommand ) ,
Usage : "Use this tunnel as a load balancer origin, creating pool and load balancer if necessary" ,
UsageText : "cloudflared tunnel route dns [TUNNEL] [HOSTNAME] [LB-POOL]" ,
Description : ` Creates Load Balancer with an origin pool that points to the tunnel. ` ,
} ,
2020-12-22 02:06:46 +00:00
buildRouteIPSubcommand ( ) ,
} ,
2020-07-06 08:01:48 +00:00
}
}
2021-05-19 17:38:09 +00:00
func dnsRouteFromArg ( c * cli . Context , overwriteExisting bool ) ( tunnelstore . Route , error ) {
2020-07-06 08:01:48 +00:00
const (
2021-06-07 08:20:11 +00:00
userHostnameIndex = 1
expectedNArgs = 2
2020-07-06 08:01:48 +00:00
)
2020-09-14 21:41:02 +00:00
if c . NArg ( ) != expectedNArgs {
return nil , cliutil . UsageError ( "Expected %d arguments, got %d" , expectedNArgs , c . NArg ( ) )
2020-07-06 08:01:48 +00:00
}
userHostname := c . Args ( ) . Get ( userHostnameIndex )
if userHostname == "" {
return nil , cliutil . UsageError ( "The third argument should be the hostname" )
2020-11-20 22:22:32 +00:00
} else if ! validateHostname ( userHostname , true ) {
2020-09-14 21:41:02 +00:00
return nil , errors . Errorf ( "%s is not a valid hostname" , userHostname )
2020-07-06 08:01:48 +00:00
}
2021-05-19 17:38:09 +00:00
return tunnelstore . NewDNSRoute ( userHostname , overwriteExisting ) , nil
2020-07-06 08:01:48 +00:00
}
2020-09-14 21:41:02 +00:00
func lbRouteFromArg ( c * cli . Context ) ( tunnelstore . Route , error ) {
2020-07-06 08:01:48 +00:00
const (
2021-06-07 08:20:11 +00:00
lbNameIndex = 1
lbPoolIndex = 2
expectedNArgs = 3
2020-07-06 08:01:48 +00:00
)
2020-09-14 21:41:02 +00:00
if c . NArg ( ) != expectedNArgs {
return nil , cliutil . UsageError ( "Expected %d arguments, got %d" , expectedNArgs , c . NArg ( ) )
2020-07-06 08:01:48 +00:00
}
lbName := c . Args ( ) . Get ( lbNameIndex )
if lbName == "" {
return nil , cliutil . UsageError ( "The third argument should be the load balancer name" )
2020-11-20 22:22:32 +00:00
} else if ! validateHostname ( lbName , true ) {
2020-09-14 21:41:02 +00:00
return nil , errors . Errorf ( "%s is not a valid load balancer name" , lbName )
2020-07-06 08:01:48 +00:00
}
2020-09-14 21:41:02 +00:00
2020-07-06 08:01:48 +00:00
lbPool := c . Args ( ) . Get ( lbPoolIndex )
if lbPool == "" {
2020-09-14 21:41:02 +00:00
return nil , cliutil . UsageError ( "The fourth argument should be the pool name" )
2020-11-20 22:22:32 +00:00
} else if ! validateName ( lbPool , false ) {
2020-09-14 21:41:02 +00:00
return nil , errors . Errorf ( "%s is not a valid pool name" , lbPool )
2020-07-06 08:01:48 +00:00
}
return tunnelstore . NewLBRoute ( lbName , lbPool ) , nil
}
2020-08-07 12:29:53 +00:00
2020-09-14 21:41:02 +00:00
var nameRegex = regexp . MustCompile ( "^[_a-zA-Z0-9][-_.a-zA-Z0-9]*$" )
2020-11-20 22:22:32 +00:00
var hostNameRegex = regexp . MustCompile ( "^[*_a-zA-Z0-9][-_.a-zA-Z0-9]*$" )
2020-09-14 21:41:02 +00:00
2020-11-20 22:22:32 +00:00
func validateName ( s string , allowWildcardSubdomain bool ) bool {
if allowWildcardSubdomain {
return hostNameRegex . MatchString ( s )
}
2020-09-14 21:41:02 +00:00
return nameRegex . MatchString ( s )
}
2020-11-20 22:22:32 +00:00
func validateHostname ( s string , allowWildcardSubdomain bool ) bool {
2020-09-15 13:36:36 +00:00
// Slightly stricter than PunyCodeProfile
idnaProfile := idna . New (
idna . ValidateLabels ( true ) ,
idna . VerifyDNSLength ( true ) )
puny , err := idnaProfile . ToASCII ( s )
2020-11-20 22:22:32 +00:00
return err == nil && validateName ( puny , allowWildcardSubdomain )
2020-09-15 13:36:36 +00:00
}
2021-06-07 08:20:11 +00:00
func routeDnsCommand ( c * cli . Context ) error {
if c . NArg ( ) != 2 {
return cliutil . UsageError ( ` This command expects the format "cloudflared tunnel route dns <tunnel name/id> <hostname>" ` )
2020-08-07 12:29:53 +00:00
}
2021-06-07 08:20:11 +00:00
return routeCommand ( c , "dns" )
}
func routeLbCommand ( c * cli . Context ) error {
if c . NArg ( ) != 3 {
return cliutil . UsageError ( ` This command expects the format "cloudflared tunnel route lb <tunnel name/id> <hostname> <load balancer pool>" ` )
}
return routeCommand ( c , "lb" )
}
func routeCommand ( c * cli . Context , routeType string ) error {
2020-08-18 21:54:05 +00:00
sc , err := newSubcommandContext ( c )
2020-08-07 12:29:53 +00:00
if err != nil {
2020-08-18 21:54:05 +00:00
return err
2020-08-07 12:29:53 +00:00
}
2021-06-07 08:20:11 +00:00
tunnelID , err := sc . findID ( c . Args ( ) . Get ( 0 ) )
if err != nil {
return err
}
2020-09-17 20:19:47 +00:00
var route tunnelstore . Route
2020-08-07 12:29:53 +00:00
switch routeType {
case "dns" :
2021-05-19 17:38:09 +00:00
route , err = dnsRouteFromArg ( c , c . Bool ( overwriteDNSFlagName ) )
2020-08-07 12:29:53 +00:00
case "lb" :
2020-09-17 20:19:47 +00:00
route , err = lbRouteFromArg ( c )
2021-06-07 08:20:11 +00:00
}
if err != nil {
return err
2020-08-07 12:29:53 +00:00
}
2020-09-17 20:19:47 +00:00
res , err := sc . route ( tunnelID , route )
if err != nil {
2020-08-07 12:29:53 +00:00
return err
}
2020-09-17 20:19:47 +00:00
2020-12-28 18:10:01 +00:00
sc . log . Info ( ) . Str ( LogFieldTunnelID , tunnelID . String ( ) ) . Msg ( res . SuccessSummary ( ) )
2020-08-07 12:29:53 +00:00
return nil
}
2020-09-29 09:47:50 +00:00
func commandHelpTemplate ( ) string {
var parentFlagsHelp string
for _ , f := range configureCloudflaredFlags ( false ) {
parentFlagsHelp += fmt . Sprintf ( " %s\n\t" , f )
}
for _ , f := range configureLoggingFlags ( false ) {
parentFlagsHelp += fmt . Sprintf ( " %s\n\t" , f )
}
const template = ` NAME :
{ { . HelpName } } - { { . Usage } }
2020-11-20 22:22:32 +00:00
2020-09-29 09:47:50 +00:00
USAGE :
{ { . UsageText } }
2020-11-20 22:22:32 +00:00
2020-09-29 09:47:50 +00:00
DESCRIPTION :
{ { . Description } }
2020-11-20 22:22:32 +00:00
2020-09-29 09:47:50 +00:00
TUNNEL COMMAND OPTIONS :
% s
2020-10-09 17:07:08 +00:00
SUBCOMMAND OPTIONS :
2020-09-29 09:47:50 +00:00
{ { range . VisibleFlags } } { { . } }
{ { end } }
`
return fmt . Sprintf ( template , parentFlagsHelp )
}