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-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"
2020-07-02 09:31:12 +00:00
"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-05-21 20:36:49 +00:00
"gopkg.in/yaml.v2"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
2020-04-29 20:51:32 +00:00
"github.com/cloudflare/cloudflared/logger"
2020-06-15 18:33:41 +00:00
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
2020-05-21 20:36:49 +00:00
"github.com/cloudflare/cloudflared/tunnelstore"
)
2020-07-02 09:31:12 +00:00
const (
credFileFlagAlias = "cred-file"
)
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" } ,
Usage : "List tunnels with the given name" ,
}
listExistedAtFlag = & cli . TimestampFlag {
Name : "when" ,
Aliases : [ ] string { "w" } ,
Usage : fmt . Sprintf ( "List tunnels that are active at the given time, expect format in RFC3339 (%s)" , time . Now ( ) . Format ( tunnelstore . TimeLayout ) ) ,
Layout : tunnelstore . TimeLayout ,
}
listIDFlag = & cli . StringFlag {
Name : "id" ,
Aliases : [ ] string { "i" } ,
Usage : "List tunnel by ID" ,
}
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" ,
}
2020-05-21 20:36:49 +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'" ,
2020-05-21 20:36:49 +00:00
}
2020-06-25 18:25:39 +00:00
forceFlag = & 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-07-02 09:31:12 +00:00
credentialsFileFlag = & cli . StringFlag {
Name : "credentials-file" ,
Aliases : [ ] string { credFileFlagAlias } ,
Usage : "File path of tunnel credentials" ,
}
2020-07-10 22:11:31 +00:00
forceDeleteFlag = & cli . BoolFlag {
Name : "force" ,
Aliases : [ ] string { "f" } ,
2020-07-06 08:01:48 +00:00
Usage : "Allows you to delete a tunnel, even if it has active connections." ,
2020-07-10 22:11:31 +00:00
}
2020-05-21 20:36:49 +00:00
)
const hideSubcommands = true
func buildCreateCommand ( ) * cli . Command {
return & cli . Command {
Name : "create" ,
2020-08-07 12:29:53 +00:00
Action : cliutil . ErrorHandler ( createCommand ) ,
2020-05-21 20:36:49 +00:00
Usage : "Create a new tunnel with given name" ,
ArgsUsage : "TUNNEL-NAME" ,
Hidden : hideSubcommands ,
Flags : [ ] cli . Flag { outputFormatFlag } ,
}
}
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
2020-08-07 12:29:53 +00:00
_ , err = sc . create ( name )
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
}
2020-07-06 08:01:48 +00:00
func writeTunnelCredentials ( tunnelID uuid . UUID , accountID , originCertPath string , tunnelSecret [ ] byte , logger logger . Service ) error {
2020-07-02 09:31:12 +00:00
originCertDir := filepath . Dir ( originCertPath )
filePath , err := tunnelFilePath ( tunnelID , originCertDir )
2020-06-15 18:33:41 +00:00
if err != nil {
return err
}
body , err := json . Marshal ( pogs . TunnelAuth {
AccountTag : accountID ,
TunnelSecret : tunnelSecret ,
} )
if err != nil {
return errors . Wrap ( err , "Unable to marshal tunnel credentials to JSON" )
}
2020-06-16 22:43:22 +00:00
logger . Infof ( "Writing tunnel credentials to %v. cloudflared chose this file based on where your origin certificate was found." , filePath )
logger . Infof ( "Keep this file secret. To revoke these credentials, delete the tunnel." )
return ioutil . WriteFile ( filePath , body , 400 )
}
2020-07-02 09:31:12 +00:00
func validFilePath ( path string ) bool {
fileStat , err := os . Stat ( path )
if err != nil {
return false
}
return ! fileStat . IsDir ( )
}
2020-05-21 20:36:49 +00:00
func buildListCommand ( ) * cli . Command {
return & cli . Command {
Name : "list" ,
2020-08-07 12:29:53 +00:00
Action : cliutil . ErrorHandler ( listCommand ) ,
2020-05-21 20:36:49 +00:00
Usage : "List existing tunnels" ,
ArgsUsage : " " ,
Hidden : hideSubcommands ,
2020-08-05 20:43:56 +00:00
Flags : [ ] cli . Flag { outputFormatFlag , showDeletedFlag , listNameFlag , listExistedAtFlag , listIDFlag , showRecentlyDisconnected } ,
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
}
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 )
}
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 )
}
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
}
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-08-05 20:43:56 +00:00
fmtAndPrintTunnelList ( 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" )
}
return nil
}
2020-08-07 12:29:53 +00:00
func fmtAndPrintTunnelList ( tunnels [ ] * tunnelstore . Tunnel , showRecentlyDisconnected bool ) {
2020-07-02 22:22:23 +00:00
const (
minWidth = 0
tabWidth = 8
padding = 1
padChar = ' '
flags = 0
)
writer := tabwriter . NewWriter ( os . Stdout , minWidth , tabWidth , padding , padChar , flags )
// Print column headers with tabbed columns
fmt . Fprintln ( writer , "ID\tNAME\tCREATED\tCONNECTIONS\t" )
// 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-07-02 22:22:23 +00:00
fmt . Fprintln ( writer , formattedStr )
}
// Write data buffered in tabwriter to output
writer . Flush ( )
}
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 , ", " )
}
2020-05-21 20:36:49 +00:00
func buildDeleteCommand ( ) * cli . Command {
return & cli . Command {
Name : "delete" ,
2020-08-07 12:29:53 +00:00
Action : cliutil . ErrorHandler ( deleteCommand ) ,
Usage : "Delete existing tunnel with given IDs" ,
2020-05-21 20:36:49 +00:00
ArgsUsage : "TUNNEL-ID" ,
Hidden : hideSubcommands ,
2020-07-10 22:11:31 +00:00
Flags : [ ] cli . Flag { credentialsFileFlag , forceDeleteFlag } ,
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
}
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 {
return & cli . Command {
Name : "run" ,
2020-08-07 12:29:53 +00:00
Action : cliutil . ErrorHandler ( runCommand ) ,
2020-06-16 22:43:22 +00:00
Usage : "Proxy a local web server by running the given tunnel" ,
ArgsUsage : "TUNNEL-ID" ,
Hidden : hideSubcommands ,
2020-07-02 09:31:12 +00:00
Flags : [ ] cli . Flag { forceFlag , credentialsFileFlag } ,
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-06-16 22:43:22 +00:00
if c . NArg ( ) != 1 {
2020-08-18 21:54:05 +00:00
return cliutil . UsageError ( ` "cloudflared tunnel run" requires exactly 1 argument, the ID or name of the tunnel to run. ` )
2020-06-16 22:43:22 +00:00
}
2020-07-06 08:01:48 +00:00
tunnelID , err := uuid . Parse ( c . Args ( ) . First ( ) )
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-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 {
Name : "cleanup" ,
2020-08-07 12:29:53 +00:00
Action : cliutil . ErrorHandler ( cleanupCommand ) ,
2020-07-03 08:55:11 +00:00
Usage : "Cleanup connections for the tunnel with given IDs" ,
ArgsUsage : "TUNNEL-IDS" ,
Hidden : hideSubcommands ,
}
}
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 {
Name : "route" ,
2020-08-07 12:29:53 +00:00
Action : cliutil . ErrorHandler ( routeCommand ) ,
2020-07-06 08:01:48 +00:00
Usage : "Define what hostname or load balancer can route to this tunnel" ,
Description : ` The route defines what hostname or load balancer can route to this tunnel .
To route a hostname : cloudflared tunnel route dns < tunnel ID > < hostname >
To route a load balancer : cloudflared tunnel route lb < tunnel ID > < load balancer name > < load balancer pool >
If you don ' t specify a load balancer pool , we will create a new pool called tunnel : < tunnel ID > ` ,
ArgsUsage : "dns|lb TUNNEL-ID HOSTNAME [LB-POOL]" ,
Hidden : hideSubcommands ,
}
}
func dnsRouteFromArg ( c * cli . Context , tunnelID uuid . UUID ) ( tunnelstore . Route , error ) {
const (
userHostnameIndex = 2
expectArgs = 3
)
if c . NArg ( ) != expectArgs {
return nil , cliutil . UsageError ( "Expect %d arguments, got %d" , expectArgs , c . NArg ( ) )
}
userHostname := c . Args ( ) . Get ( userHostnameIndex )
if userHostname == "" {
return nil , cliutil . UsageError ( "The third argument should be the hostname" )
}
return tunnelstore . NewDNSRoute ( userHostname ) , nil
}
2020-08-07 12:29:53 +00:00
func lbRouteFromArg ( c * cli . Context , tunnelID uuid . UUID ) ( tunnelstore . Route , error ) {
2020-07-06 08:01:48 +00:00
const (
lbNameIndex = 2
lbPoolIndex = 3
expectMinArgs = 3
)
if c . NArg ( ) < expectMinArgs {
return nil , cliutil . UsageError ( "Expect at least %d arguments, got %d" , expectMinArgs , c . NArg ( ) )
}
lbName := c . Args ( ) . Get ( lbNameIndex )
if lbName == "" {
return nil , cliutil . UsageError ( "The third argument should be the load balancer name" )
}
lbPool := c . Args ( ) . Get ( lbPoolIndex )
if lbPool == "" {
2020-08-07 12:29:53 +00:00
lbPool = defaultPoolName ( tunnelID )
2020-07-06 08:01:48 +00:00
}
return tunnelstore . NewLBRoute ( lbName , lbPool ) , nil
}
2020-08-07 12:29:53 +00:00
func routeCommand ( c * cli . Context ) error {
if c . NArg ( ) < 2 {
2020-08-18 21:54:05 +00:00
return cliutil . UsageError ( ` "cloudflared tunnel route" requires the first argument to be the route type(dns or lb), followed by the ID or name of the tunnel ` )
2020-08-07 12:29:53 +00:00
}
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
}
2020-08-18 21:54:05 +00:00
const tunnelIDIndex = 1
tunnelID , err := sc . findID ( c . Args ( ) . Get ( tunnelIDIndex ) )
2020-08-07 12:29:53 +00:00
if err != nil {
return err
}
routeType := c . Args ( ) . First ( )
var r tunnelstore . Route
switch routeType {
case "dns" :
r , err = dnsRouteFromArg ( c , tunnelID )
if err != nil {
return err
}
case "lb" :
r , err = lbRouteFromArg ( c , tunnelID )
if err != nil {
return err
}
default :
return cliutil . UsageError ( "%s is not a recognized route type. Supported route types are dns and lb" , routeType )
}
if err := sc . route ( tunnelID , r ) ; err != nil {
return err
}
sc . logger . Infof ( r . SuccessSummary ( ) )
return nil
}
func defaultPoolName ( tunnelID uuid . UUID ) string {
return fmt . Sprintf ( "tunnel:%v" , tunnelID )
}