TUN-1099: Bring back changes in 2018.10.1

This commit is contained in:
Areg Harutyunyan
2018-10-08 14:20:28 -05:00
parent 995e773096
commit ca9902a8d1
105 changed files with 13902 additions and 997 deletions

View File

@@ -0,0 +1,199 @@
package access
import (
"errors"
"fmt"
"net/url"
"os"
"github.com/cloudflare/cloudflared/cmd/cloudflared/shell"
"golang.org/x/net/idna"
"github.com/cloudflare/cloudflared/log"
raven "github.com/getsentry/raven-go"
cli "gopkg.in/urfave/cli.v2"
)
const sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b@sentry.io/189878"
// Flags return the global flags for Access related commands (hopefully none)
func Flags() []cli.Flag {
return []cli.Flag{} // no flags yet.
}
// Commands returns all the Access related subcommands
func Commands() []*cli.Command {
return []*cli.Command{
{
Name: "access",
Category: "Access (BETA)",
Usage: "access <subcommand>",
Description: `(BETA) Cloudflare Access protects internal resources by securing, authenticating and monitoring access
per-user and by application. With Cloudflare Access, only authenticated users with the required permissions are
able to reach sensitive resources. The commands provided here allow you to interact with Access protected
applications from the command line. This feature is considered beta. Your feedback is greatly appreciated!
https://cfl.re/CLIAuthBeta`,
Subcommands: []*cli.Command{
{
Name: "login",
Action: login,
Usage: "login <url of access application>",
Description: `The login subcommand initiates an authentication flow with your identity provider.
The subcommand will launch a browser. For headless systems, a url is provided.
Once authenticated with your identity provider, the login command will generate a JSON Web Token (JWT)
scoped to your identity, the application you intend to reach, and valid for a session duration set by your
administrator. cloudflared stores the token in local storage.`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "url",
Hidden: true,
},
},
},
{
Name: "curl",
Action: curl,
Usage: "curl <args>",
Description: `The curl subcommand wraps curl and automatically injects the JWT into a cf-access-token
header when using curl to reach an application behind Access.`,
ArgsUsage: "allow-request will allow the curl request to continue even if the jwt is not present.",
SkipFlagParsing: true,
},
{
Name: "token",
Action: token,
Usage: "token -app=<url of access application>",
ArgsUsage: "url of Access application",
Description: `The token subcommand produces a JWT which can be used to authenticate requests.`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "app",
},
},
},
},
},
}
}
// login pops up the browser window to do the actual login and JWT generation
func login(c *cli.Context) error {
raven.SetDSN(sentryDSN)
logger := log.CreateLogger()
args := c.Args()
appURL, err := url.Parse(args.First())
if args.Len() < 1 || err != nil {
logger.Errorf("Please provide the url of the Access application\n")
return err
}
if _, err := fetchToken(c, appURL); err != nil {
logger.Errorf("Failed to fetch token: %s\n", err)
return err
}
return nil
}
// curl provides a wrapper around curl, passing Access JWT along in request
func curl(c *cli.Context) error {
raven.SetDSN(sentryDSN)
logger := log.CreateLogger()
args := c.Args()
if args.Len() < 1 {
logger.Error("Please provide the access app and command you wish to run.")
return errors.New("incorrect args")
}
cmdArgs, appURL, allowRequest, err := buildCurlCmdArgs(args.Slice())
if err != nil {
return err
}
token, err := getTokenIfExists(appURL)
if err != nil || token == "" {
if allowRequest {
logger.Warn("You don't have an Access token set. Please run access token <access application> to fetch one.")
return shell.Run("curl", cmdArgs...)
}
token, err = fetchToken(c, appURL)
if err != nil {
logger.Error("Failed to refresh token: ", err)
return err
}
}
cmdArgs = append(cmdArgs, "-H")
cmdArgs = append(cmdArgs, fmt.Sprintf("cf-access-token: %s", token))
return shell.Run("curl", cmdArgs...)
}
// token dumps provided token to stdout
func token(c *cli.Context) error {
raven.SetDSN(sentryDSN)
appURL, err := url.Parse(c.String("app"))
if err != nil || c.NumFlags() < 1 {
fmt.Fprintln(os.Stderr, "Please provide a url.")
return err
}
token, err := getTokenIfExists(appURL)
if err != nil || token == "" {
fmt.Fprintln(os.Stderr, "Unable to find token for provided application. Please run token command to generate token.")
return err
}
if _, err := fmt.Fprint(os.Stdout, token); err != nil {
fmt.Fprintln(os.Stderr, "Failed to write token to stdout.")
return err
}
return nil
}
// processURL will preprocess the string (parse to a url, convert to punycode, etc).
func processURL(s string) (*url.URL, error) {
u, err := url.ParseRequestURI(s)
if err != nil {
return nil, err
}
host, err := idna.ToASCII(u.Hostname())
if err != nil { // we fail to convert to punycode, just return the url we parsed.
return u, nil
}
if u.Port() != "" {
u.Host = fmt.Sprintf("%s:%s", host, u.Port())
} else {
u.Host = host
}
return u, nil
}
// buildCurlCmdArgs will build the curl cmd args
func buildCurlCmdArgs(cmdArgs []string) ([]string, *url.URL, bool, error) {
allowRequest, iAllowRequest := false, 0
var appURL *url.URL
for i, arg := range cmdArgs {
if arg == "-allow-request" || arg == "-ar" {
iAllowRequest = i
allowRequest = true
}
u, err := processURL(arg)
if err == nil {
appURL = u
cmdArgs[i] = appURL.String()
}
}
if appURL == nil {
logger.Error("Please provide a valid URL.")
return cmdArgs, appURL, allowRequest, errors.New("invalid url")
}
if allowRequest {
// remove from cmdArgs
cmdArgs[iAllowRequest] = cmdArgs[len(cmdArgs)-1]
cmdArgs = cmdArgs[:len(cmdArgs)-1]
}
return cmdArgs, appURL, allowRequest, nil
}

View File

@@ -0,0 +1,90 @@
package access
import (
"fmt"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/cmd/cloudflared/transfer"
"github.com/cloudflare/cloudflared/log"
"github.com/coreos/go-oidc/jose"
"github.com/coreos/go-oidc/oidc"
homedir "github.com/mitchellh/go-homedir"
cli "gopkg.in/urfave/cli.v2"
)
var logger = log.CreateLogger()
// fetchToken will either load a stored token or generate a new one
func fetchToken(c *cli.Context, appURL *url.URL) (string, error) {
if token, err := getTokenIfExists(appURL); token != "" && err == nil {
fmt.Fprintf(os.Stdout, "You have an existing token:\n\n%s\n\n", token)
return token, nil
}
path, err := generateFilePathForTokenURL(appURL)
if err != nil {
return "", err
}
// this weird parameter is the resource name (token) and the key/value
// we want to send to the transfer service. the key is token and the value
// is blank (basically just the id generated in the transfer service)
const resourceName, key, value = "token", "token", ""
token, err := transfer.Run(c, appURL, resourceName, key, value, path, true)
if err != nil {
return "", err
}
fmt.Fprintf(os.Stdout, "Successfully fetched your token:\n\n%s\n\n", string(token))
return string(token), nil
}
// getTokenIfExists will return the token from local storage if it exists
func getTokenIfExists(url *url.URL) (string, error) {
path, err := generateFilePathForTokenURL(url)
if err != nil {
return "", err
}
content, err := ioutil.ReadFile(path)
if err != nil {
return "", err
}
token, err := jose.ParseJWT(string(content))
if err != nil {
return "", err
}
claims, err := token.Claims()
if err != nil {
return "", err
}
ident, err := oidc.IdentityFromClaims(claims)
if err == nil && ident.ExpiresAt.After(time.Now()) {
return token.Encode(), nil
}
return "", err
}
// generateFilePathForTokenURL will return a filepath for given access application url
func generateFilePathForTokenURL(url *url.URL) (string, error) {
configPath, err := homedir.Expand(config.DefaultConfigDirs[0])
if err != nil {
return "", err
}
ok, err := config.FileExists(configPath)
if !ok && err == nil {
// create config directory if doesn't already exist
err = os.Mkdir(configPath, 0700)
}
if err != nil {
return "", err
}
name := strings.Replace(fmt.Sprintf("%s%s-token", url.Hostname(), url.EscapedPath()), "/", "-", -1)
return filepath.Join(configPath, name), nil
}

View File

@@ -0,0 +1,62 @@
package config
import (
"os"
"path/filepath"
homedir "github.com/mitchellh/go-homedir"
"gopkg.in/urfave/cli.v2"
"gopkg.in/urfave/cli.v2/altsrc"
)
var (
// File names from which we attempt to read configuration.
DefaultConfigFiles = []string{"config.yml", "config.yaml"}
// Launchd doesn't set root env variables, so there is default
// Windows default config dir was ~/cloudflare-warp in documentation; let's keep it compatible
DefaultConfigDirs = []string{"~/.cloudflared", "~/.cloudflare-warp", "~/cloudflare-warp", "/usr/local/etc/cloudflared", "/etc/cloudflared"}
)
const DefaultCredentialFile = "cert.pem"
// FileExists checks to see if a file exist at the provided path.
func FileExists(path string) (bool, error) {
f, err := os.Open(path)
if err != nil {
if os.IsNotExist(err) {
// ignore missing files
return false, nil
}
return false, err
}
f.Close()
return true, nil
}
// FindInputSourceContext pulls the input source from the config flag.
func FindInputSourceContext(context *cli.Context) (altsrc.InputSourceContext, error) {
if context.String("config") != "" {
return altsrc.NewYamlSourceFromFile(context.String("config"))
}
return nil, nil
}
// FindDefaultConfigPath returns the first path that contains a config file.
// If none of the combination of DefaultConfigDirs and DefaultConfigFiles
// contains a config file, return empty string.
func FindDefaultConfigPath() string {
for _, configDir := range DefaultConfigDirs {
for _, configFile := range DefaultConfigFiles {
dirPath, err := homedir.Expand(configDir)
if err != nil {
continue
}
path := filepath.Join(dirPath, configFile)
if ok, _ := FileExists(path); ok {
return path
}
}
}
return ""
}

View File

@@ -0,0 +1,176 @@
// Package encrypter is suitable for encrypting messages you would like to securely share between two points.
// Useful for providing end to end encryption (E2EE). It uses Box (NaCl) for encrypting the messages.
// tldr is it uses Elliptic Curves (Curve25519) for the keys, XSalsa20 and Poly1305 for encryption.
// You can read more here https://godoc.org/golang.org/x/crypto/nacl/box.
//
// msg := []byte("super safe message.")
// alice, err := New("alice_priv_key.pem", "alice_pub_key.pem")
// if err != nil {
// log.Fatal(err)
// }
//
// bob, err := New("bob_priv_key.pem", "bob_pub_key.pem")
// if err != nil {
// log.Fatal(err)
// }
// encrypted, err := alice.Encrypt(msg, bob.PublicKey())
// if err != nil {
// log.Fatal(err)
// }
//
// data, err := bob.Decrypt(encrypted, alice.PublicKey())
// if err != nil {
// log.Fatal(err)
// }
// fmt.Println(string(data))
package encrypter
import (
"bytes"
"crypto/rand"
"encoding/base64"
"encoding/pem"
"errors"
"io"
"os"
"golang.org/x/crypto/nacl/box"
)
// Encrypter represents a keypair value with auxiliary functions to make
// doing encryption and decryption easier
type Encrypter struct {
privateKey *[32]byte
publicKey *[32]byte
}
// New returns a new encrypter with initialized keypair
func New(privateKey, publicKey string) (*Encrypter, error) {
e := &Encrypter{}
pubKey, key, err := e.fetchOrGenerateKeys(privateKey, publicKey)
if err != nil {
return nil, err
}
e.privateKey, e.publicKey = key, pubKey
return e, nil
}
// PublicKey returns a base64 encoded public key. Useful for transport (like in HTTP requests)
func (e *Encrypter) PublicKey() string {
return base64.URLEncoding.EncodeToString(e.publicKey[:])
}
// Decrypt data that was encrypted using our publicKey. It will use our privateKey and the sender's publicKey to decrypt
// data is an encrypted buffer of data, mostly like from the Encrypt function. Messages contain the nonce data on the front
// of the message.
// senderPublicKey is a base64 encoded version of the sender's public key (most likely from the PublicKey function).
// The return value is the decrypted buffer or an error.
func (e *Encrypter) Decrypt(data []byte, senderPublicKey string) ([]byte, error) {
var decryptNonce [24]byte
copy(decryptNonce[:], data[:24]) // we pull the nonce from the front of the actual message.
pubKey, err := e.decodePublicKey(senderPublicKey)
if err != nil {
return nil, err
}
decrypted, ok := box.Open(nil, data[24:], &decryptNonce, pubKey, e.privateKey)
if !ok {
return nil, errors.New("failed to decrypt message")
}
return decrypted, nil
}
// Encrypt data using our privateKey and the recipient publicKey
// data is a buffer of data that we would like to encrypt. Messages will have the nonce added to front
// as they have to unique for each message shared.
// recipientPublicKey is a base64 encoded version of the sender's public key (most likely from the PublicKey function).
// The return value is the encrypted buffer or an error.
func (e *Encrypter) Encrypt(data []byte, recipientPublicKey string) ([]byte, error) {
var nonce [24]byte
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
return nil, err
}
pubKey, err := e.decodePublicKey(recipientPublicKey)
if err != nil {
return nil, err
}
// This encrypts msg and adds the nonce to the front of the message, since the nonce has to be
// the same for encrypting and decrypting
return box.Seal(nonce[:], data, &nonce, pubKey, e.privateKey), nil
}
// WriteKeys keys will take the currently initialized keypair and write them to provided filenames
func (e *Encrypter) WriteKeys(privateKey, publicKey string) error {
if err := e.writeKey(e.privateKey[:], "BOX PRIVATE KEY", privateKey); err != nil {
return err
}
return e.writeKey(e.publicKey[:], "PUBLIC KEY", publicKey)
}
// fetchOrGenerateKeys will either load or create a keypair if it doesn't exist
func (e *Encrypter) fetchOrGenerateKeys(privateKey, publicKey string) (*[32]byte, *[32]byte, error) {
key, err := e.fetchKey(privateKey)
if os.IsNotExist(err) {
return box.GenerateKey(rand.Reader)
} else if err != nil {
return nil, nil, err
}
pub, err := e.fetchKey(publicKey)
if os.IsNotExist(err) {
return box.GenerateKey(rand.Reader)
} else if err != nil {
return nil, nil, err
}
return pub, key, nil
}
// writeKey will write a key to disk in DER format (it's a standard pem key)
func (e *Encrypter) writeKey(key []byte, pemType, filename string) error {
data := pem.EncodeToMemory(&pem.Block{
Type: pemType,
Bytes: key,
})
f, err := os.Create(filename)
if err != nil {
return err
}
_, err = f.Write(data)
if err != nil {
return err
}
return nil
}
// fetchKey will load a a DER formatted key from disk
func (e *Encrypter) fetchKey(filename string) (*[32]byte, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
buf := new(bytes.Buffer)
io.Copy(buf, f)
p, _ := pem.Decode(buf.Bytes())
if p == nil {
return nil, errors.New("Failed to decode key")
}
var newKey [32]byte
copy(newKey[:], p.Bytes)
return &newKey, nil
}
// decodePublicKey will base64 decode the provided key to the box representation
func (e *Encrypter) decodePublicKey(key string) (*[32]byte, error) {
pub, err := base64.URLEncoding.DecodeString(key)
if err != nil {
return nil, err
}
var newKey [32]byte
copy(newKey[:], pub)
return &newKey, nil
}

View File

@@ -7,6 +7,7 @@ import (
"os"
"path/filepath"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
cli "gopkg.in/urfave/cli.v2"
)
@@ -30,7 +31,13 @@ func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
app.Run(os.Args)
}
const serviceConfigDir = "/etc/cloudflared"
// The directory and files that are used by the service.
// These are hard-coded in the templates below.
const (
serviceConfigDir = "/etc/cloudflared"
serviceConfigFile = "config.yml"
serviceCredentialFile = "cert.pem"
)
var systemdTemplates = []ServiceTemplate{
{
@@ -174,6 +181,23 @@ func isSystemd() bool {
return false
}
func copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile string) error {
if err := ensureConfigDirExists(serviceConfigDir); err != nil {
return err
}
srcCredentialPath := filepath.Join(userConfigDir, userCredentialFile)
destCredentialPath := filepath.Join(serviceConfigDir, serviceCredentialFile)
if err := copyCredential(srcCredentialPath, destCredentialPath); err != nil {
return err
}
srcConfigPath := filepath.Join(userConfigDir, userConfigFile)
destConfigPath := filepath.Join(serviceConfigDir, serviceConfigFile)
if err := copyConfig(srcConfigPath, destConfigPath); err != nil {
return err
}
return nil
}
func installLinuxService(c *cli.Context) error {
etPath, err := os.Executable()
if err != nil {
@@ -181,11 +205,12 @@ func installLinuxService(c *cli.Context) error {
}
templateArgs := ServiceTemplateArgs{Path: etPath}
defaultConfigDir := filepath.Dir(c.String("config"))
defaultConfigFile := filepath.Base(c.String("config"))
if err = copyCredentials(serviceConfigDir, defaultConfigDir, defaultConfigFile, defaultCredentialFile); err != nil {
userConfigDir := filepath.Dir(c.String("config"))
userConfigFile := filepath.Base(c.String("config"))
userCredentialFile := config.DefaultCredentialFile
if err = copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile); err != nil {
logger.WithError(err).Infof("Failed to copy user configuration. Before running the service, ensure that %s contains two files, %s and %s",
serviceConfigDir, defaultCredentialFile, defaultConfigFiles[0])
serviceConfigDir, serviceCredentialFile, serviceConfigFile)
return err
}

View File

@@ -1,194 +0,0 @@
package main
import (
"crypto/rand"
"encoding/base32"
"fmt"
"io"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"runtime"
"syscall"
"time"
homedir "github.com/mitchellh/go-homedir"
cli "gopkg.in/urfave/cli.v2"
)
const baseLoginURL = "https://dash.cloudflare.com/warp"
const baseCertStoreURL = "https://login.cloudflarewarp.com"
const clientTimeout = time.Minute * 20
func login(c *cli.Context) error {
configPath, err := homedir.Expand(defaultConfigDirs[0])
if err != nil {
return err
}
ok, err := fileExists(configPath)
if !ok && err == nil {
// create config directory if doesn't already exist
err = os.Mkdir(configPath, 0700)
}
if err != nil {
return err
}
path := filepath.Join(configPath, defaultCredentialFile)
fileInfo, err := os.Stat(path)
if err == nil && fileInfo.Size() > 0 {
fmt.Fprintf(os.Stderr, `You have an existing certificate at %s which login would overwrite.
If this is intentional, please move or delete that file then run this command again.
`, path)
return nil
}
if err != nil && err.(*os.PathError).Err != syscall.ENOENT {
return err
}
// for local debugging
baseURL := baseCertStoreURL
if c.IsSet("url") {
baseURL = c.String("url")
}
// Generate a random post URL
certURL := baseURL + generateRandomPath()
loginURL, err := url.Parse(baseLoginURL)
if err != nil {
// shouldn't happen, URL is hardcoded
return err
}
loginURL.RawQuery = "callback=" + url.QueryEscape(certURL)
err = open(loginURL.String())
if err != nil {
fmt.Fprintf(os.Stderr, `Please open the following URL and log in with your Cloudflare account:
%s
Leave cloudflared running to install the certificate automatically.
`, loginURL.String())
} else {
fmt.Fprintf(os.Stderr, `A browser window should have opened at the following URL:
%s
If the browser failed to open, open it yourself and visit the URL above.
`, loginURL.String())
}
if download(certURL, path) {
fmt.Fprintf(os.Stderr, `You have successfully logged in.
If you wish to copy your credentials to a server, they have been saved to:
%s
`, path)
} else {
fmt.Fprintf(os.Stderr, `Failed to write the certificate due to the following error:
%v
Your browser will download the certificate instead. You will have to manually
copy it to the following path:
%s
`, err, path)
}
return nil
}
// generateRandomPath generates a random URL to associate with the certificate.
func generateRandomPath() string {
randomBytes := make([]byte, 40)
_, err := rand.Read(randomBytes)
if err != nil {
panic(err)
}
return "/" + base32.StdEncoding.EncodeToString(randomBytes)
}
// open opens the specified URL in the default browser of the user.
func open(url string) error {
var cmd string
var args []string
switch runtime.GOOS {
case "windows":
cmd = "cmd"
args = []string{"/c", "start"}
case "darwin":
cmd = "open"
default: // "linux", "freebsd", "openbsd", "netbsd"
cmd = "xdg-open"
}
args = append(args, url)
return exec.Command(cmd, args...).Start()
}
func download(certURL, filePath string) bool {
client := &http.Client{Timeout: clientTimeout}
// attempt a (long-running) certificate get
for i := 0; i < 20; i++ {
ok, err := tryDownload(client, certURL, filePath)
if ok {
putSuccess(client, certURL)
return true
}
if err != nil {
logger.WithError(err).Error("Error fetching certificate")
return false
}
}
return false
}
func tryDownload(client *http.Client, certURL, filePath string) (ok bool, err error) {
resp, err := client.Get(certURL)
if err != nil {
return false, err
}
defer resp.Body.Close()
if resp.StatusCode == 404 {
return false, nil
}
if resp.StatusCode != 200 {
return false, fmt.Errorf("Unexpected HTTP error code %d", resp.StatusCode)
}
if resp.Header.Get("Content-Type") != "application/x-pem-file" {
return false, fmt.Errorf("Unexpected content type %s", resp.Header.Get("Content-Type"))
}
// write response
file, err := os.Create(filePath)
if err != nil {
return false, err
}
defer file.Close()
written, err := io.Copy(file, resp.Body)
switch {
case err != nil:
return false, err
case resp.ContentLength != written && resp.ContentLength != -1:
return false, fmt.Errorf("Short read (%d bytes) from server while writing certificate", written)
default:
return true, nil
}
}
func putSuccess(client *http.Client, certURL string) {
// indicate success to the relay server
req, err := http.NewRequest("PUT", certURL+"/ok", nil)
if err != nil {
logger.WithError(err).Error("HTTP request error")
return
}
resp, err := client.Do(req)
if err != nil {
logger.WithError(err).Error("HTTP error")
return
}
resp.Body.Close()
if resp.StatusCode != 200 {
logger.Errorf("Unexpected HTTP error code %d", resp.StatusCode)
}
}

View File

@@ -2,48 +2,37 @@ package main
import (
"fmt"
"io/ioutil"
"os"
"runtime/trace"
"sync"
"syscall"
"time"
"github.com/cloudflare/cloudflared/cmd/sqlgateway"
"github.com/cloudflare/cloudflared/hello"
"github.com/cloudflare/cloudflared/metrics"
"github.com/cloudflare/cloudflared/origin"
"github.com/cloudflare/cloudflared/tunneldns"
"golang.org/x/crypto/ssh/terminal"
"github.com/cloudflare/cloudflared/cmd/cloudflared/access"
"github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
"github.com/cloudflare/cloudflared/log"
"github.com/cloudflare/cloudflared/metrics"
"github.com/getsentry/raven-go"
"github.com/mitchellh/go-homedir"
"gopkg.in/urfave/cli.v2"
"gopkg.in/urfave/cli.v2/altsrc"
"github.com/coreos/go-systemd/daemon"
"github.com/facebookgo/grace/gracenet"
"github.com/pkg/errors"
)
const (
sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b:3e8827f6f9f740738eb11138f7bebb68@sentry.io/189878"
developerPortal = "https://developers.cloudflare.com/argo-tunnel"
quickStartUrl = developerPortal + "/quickstart/quickstart/"
serviceUrl = developerPortal + "/reference/service/"
argumentsUrl = developerPortal + "/reference/arguments/"
licenseUrl = developerPortal + "/licence/"
)
var (
Version = "DEV"
BuildTime = "unknown"
logger = log.CreateLogger()
)
func main() {
metrics.RegisterBuildInfo(BuildTime, Version)
raven.SetDSN(sentryDSN)
raven.SetRelease(Version)
// Force shutdown channel used by the app. When closed, app must terminate.
@@ -55,588 +44,63 @@ func main() {
app := &cli.App{}
app.Name = "cloudflared"
app.Usage = "Cloudflare's command-line tool and agent"
app.ArgsUsage = "origin-url"
app.Copyright = fmt.Sprintf(`(c) %d Cloudflare Inc.
Use is subject to the license agreement at %s`, time.Now().Year(), licenseUrl)
app.Usage = "Cloudflare reverse tunnelling proxy agent"
app.ArgsUsage = "origin-url"
app.Version = fmt.Sprintf("%s (built %s)", Version, BuildTime)
app.Description = `A reverse tunnel proxy agent that connects to Cloudflare's infrastructure.
Upon connecting, you are assigned a unique subdomain on cftunnel.com.
You need to specify a hostname on a zone you control.
A DNS record will be created to CNAME your hostname to the unique subdomain on cftunnel.com.
app.Description = `cloudflared connects your machine or user identity to Cloudflare's global network.
You can use it to authenticate a session to reach an API behind Access, route web traffic to this machine,
and configure access control.`
app.Flags = flags()
app.Action = action(Version, shutdownC, graceShutdownC)
app.Before = tunnel.Before
app.Commands = commands()
Requests made to Cloudflare's servers for your hostname will be proxied
through the tunnel to your local webserver.`
app.Flags = []cli.Flag{
&cli.StringFlag{
Name: "config",
Usage: "Specifies a config file in YAML format.",
Value: findDefaultConfigPath(),
tunnel.Init(Version, shutdownC, graceShutdownC) // we need this to support the tunnel sub command...
runApp(app, shutdownC, graceShutdownC)
}
func commands() []*cli.Command {
cmds := []*cli.Command{
{
Name: "update",
Action: updater.Update,
Usage: "Update the agent if a new version exists",
ArgsUsage: " ",
Description: `Looks for a new version on the official download server.
If a new version exists, updates the agent binary and quits.
Otherwise, does nothing.
To determine if an update happened in a script, check for error code 64.`,
},
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: "autoupdate-freq",
Usage: "Autoupdate frequency. Default is 24h.",
Value: time.Hour * 24,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "no-autoupdate",
Usage: "Disable periodic check for updates, restarting the server with the new version.",
Value: false,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "is-autoupdated",
Usage: "Signal the new process that Argo Tunnel client has been autoupdated",
Value: false,
Hidden: true,
}),
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
Name: "edge",
Usage: "Address of the Cloudflare tunnel server.",
EnvVars: []string{"TUNNEL_EDGE"},
Hidden: true,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "cacert",
Usage: "Certificate Authority authenticating the Cloudflare tunnel connection.",
EnvVars: []string{"TUNNEL_CACERT"},
Hidden: true,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "no-tls-verify",
Usage: "Disables TLS verification of the certificate presented by your origin. Will allow any certificate from the origin to be accepted. Note: The connection from your machine to Cloudflare's Edge is still encrypted.",
EnvVars: []string{"NO_TLS_VERIFY"},
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "origincert",
Usage: "Path to the certificate generated for your origin when you run cloudflared login.",
EnvVars: []string{"TUNNEL_ORIGIN_CERT"},
Value: findDefaultOriginCertPath(),
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "origin-ca-pool",
Usage: "Path to the CA for the certificate of your origin. This option should be used only if your certificate is not signed by Cloudflare.",
EnvVars: []string{"TUNNEL_ORIGIN_CA_POOL"},
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "url",
Value: "https://localhost:8080",
Usage: "Connect to the local webserver at `URL`.",
EnvVars: []string{"TUNNEL_URL"},
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "hostname",
Usage: "Set a hostname on a Cloudflare zone to route traffic through this tunnel.",
EnvVars: []string{"TUNNEL_HOSTNAME"},
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "origin-server-name",
Usage: "Hostname on the origin server certificate.",
EnvVars: []string{"TUNNEL_ORIGIN_SERVER_NAME"},
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "id",
Usage: "A unique identifier used to tie connections to this tunnel instance.",
EnvVars: []string{"TUNNEL_ID"},
Hidden: true,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "lb-pool",
Usage: "The name of a (new/existing) load balancing pool to add this origin to.",
EnvVars: []string{"TUNNEL_LB_POOL"},
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "api-key",
Usage: "This parameter has been deprecated since version 2017.10.1.",
EnvVars: []string{"TUNNEL_API_KEY"},
Hidden: true,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "api-email",
Usage: "This parameter has been deprecated since version 2017.10.1.",
EnvVars: []string{"TUNNEL_API_EMAIL"},
Hidden: true,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "api-ca-key",
Usage: "This parameter has been deprecated since version 2017.10.1.",
EnvVars: []string{"TUNNEL_API_CA_KEY"},
Hidden: true,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "metrics",
Value: "localhost:",
Usage: "Listen address for metrics reporting.",
EnvVars: []string{"TUNNEL_METRICS"},
}),
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: "metrics-update-freq",
Usage: "Frequency to update tunnel metrics",
Value: time.Second * 5,
EnvVars: []string{"TUNNEL_METRICS_UPDATE_FREQ"},
}),
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
Name: "tag",
Usage: "Custom tags used to identify this tunnel, in format `KEY=VALUE`. Multiple tags may be specified",
EnvVars: []string{"TUNNEL_TAG"},
}),
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: "heartbeat-interval",
Usage: "Minimum idle time before sending a heartbeat.",
Value: time.Second * 5,
Hidden: true,
}),
altsrc.NewUint64Flag(&cli.Uint64Flag{
Name: "heartbeat-count",
Usage: "Minimum number of unacked heartbeats to send before closing the connection.",
Value: 5,
Hidden: true,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "loglevel",
Value: "info",
Usage: "Application logging level {panic, fatal, error, warn, info, debug}",
EnvVars: []string{"TUNNEL_LOGLEVEL"},
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "proto-loglevel",
Value: "warn",
Usage: "Protocol logging level {panic, fatal, error, warn, info, debug}",
EnvVars: []string{"TUNNEL_PROTO_LOGLEVEL"},
}),
altsrc.NewUintFlag(&cli.UintFlag{
Name: "retries",
Value: 5,
Usage: "Maximum number of retries for connection/protocol errors.",
EnvVars: []string{"TUNNEL_RETRIES"},
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "hello-world",
Value: false,
Usage: "Run Hello World Server",
EnvVars: []string{"TUNNEL_HELLO_WORLD"},
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "pidfile",
Usage: "Write the application's PID to this file after first successful connection.",
EnvVars: []string{"TUNNEL_PIDFILE"},
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "logfile",
Usage: "Save application log to this file for reporting issues.",
EnvVars: []string{"TUNNEL_LOGFILE"},
}),
altsrc.NewIntFlag(&cli.IntFlag{
Name: "ha-connections",
Value: 4,
Hidden: true,
}),
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: "proxy-connect-timeout",
Usage: "HTTP proxy timeout for establishing a new connection",
Value: time.Second * 30,
}),
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: "proxy-tls-timeout",
Usage: "HTTP proxy timeout for completing a TLS handshake",
Value: time.Second * 10,
}),
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: "proxy-tcp-keepalive",
Usage: "HTTP proxy TCP keepalive duration",
Value: time.Second * 30,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "proxy-no-happy-eyeballs",
Usage: "HTTP proxy should disable \"happy eyeballs\" for IPv4/v6 fallback",
}),
altsrc.NewIntFlag(&cli.IntFlag{
Name: "proxy-keepalive-connections",
Usage: "HTTP proxy maximum keepalive connection pool size",
Value: 100,
}),
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: "proxy-keepalive-timeout",
Usage: "HTTP proxy timeout for closing an idle connection",
Value: time.Second * 90,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "proxy-dns",
Usage: "Run a DNS over HTTPS proxy server.",
EnvVars: []string{"TUNNEL_DNS"},
}),
altsrc.NewIntFlag(&cli.IntFlag{
Name: "proxy-dns-port",
Value: 53,
Usage: "Listen on given port for the DNS over HTTPS proxy server.",
EnvVars: []string{"TUNNEL_DNS_PORT"},
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "proxy-dns-address",
Usage: "Listen address for the DNS over HTTPS proxy server.",
Value: "localhost",
EnvVars: []string{"TUNNEL_DNS_ADDRESS"},
}),
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
Name: "proxy-dns-upstream",
Usage: "Upstream endpoint URL, you can specify multiple endpoints for redundancy.",
Value: cli.NewStringSlice("https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query"),
EnvVars: []string{"TUNNEL_DNS_UPSTREAM"},
}),
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: "grace-period",
Usage: "Duration to accept new requests after cloudflared receives first SIGINT/SIGTERM. A second SIGINT/SIGTERM will force cloudflared to shutdown immediately.",
Value: time.Second * 30,
EnvVars: []string{"TUNNEL_GRACE_PERIOD"},
Hidden: true,
}),
altsrc.NewUintFlag(&cli.UintFlag{
Name: "compression-quality",
Value: 0,
Usage: "Use cross-stream compression instead HTTP compression. 0-off, 1-low, 2-medium, >=3-high",
EnvVars: []string{"TUNNEL_COMPRESSION_LEVEL"},
Hidden: true,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "no-chunked-encoding",
Usage: "Disables chunked transfer encoding; useful if you are running a WSGI server.",
EnvVars: []string{"TUNNEL_NO_CHUNKED_ENCODING"},
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "trace-output",
Usage: "Name of trace output file, generated when cloudflared stops.",
EnvVars: []string{"TUNNEL_TRACE_OUTPUT"},
}),
}
app.Action = func(c *cli.Context) (err error) {
cmds = append(cmds, tunnel.Commands()...)
cmds = append(cmds, access.Commands()...)
return cmds
}
func flags() []cli.Flag {
flags := tunnel.Flags()
return append(flags, access.Flags()...)
}
func action(version string, shutdownC, graceShutdownC chan struct{}) cli.ActionFunc {
return func(c *cli.Context) (err error) {
if isRunningFromTerminal() {
logger.Error("Use of cloudflared without commands is deprecated.")
cli.ShowAppHelp(c)
return nil
}
tags := make(map[string]string)
tags["hostname"] = c.String("hostname")
raven.SetTagsContext(tags)
raven.CapturePanic(func() { err = startServer(c, shutdownC, graceShutdownC) }, nil)
raven.CapturePanic(func() { err = tunnel.StartServer(c, version, shutdownC, graceShutdownC) }, nil)
if err != nil {
raven.CaptureError(err, nil)
}
return err
}
app.Before = func(context *cli.Context) error {
if context.String("config") == "" {
logger.Warnf("Cannot determine default configuration path. No file %v in %v", defaultConfigFiles, defaultConfigDirs)
}
inputSource, err := findInputSourceContext(context)
if err != nil {
logger.WithError(err).Infof("Cannot load configuration from %s", context.String("config"))
return err
} else if inputSource != nil {
err := altsrc.ApplyInputSourceValues(context, inputSource, app.Flags)
if err != nil {
logger.WithError(err).Infof("Cannot apply configuration from %s", context.String("config"))
return err
}
logger.Infof("Applied configuration from %s", context.String("config"))
}
return nil
}
app.Commands = []*cli.Command{
{
Name: "update",
Action: update,
Usage: "Update the agent if a new version exists",
ArgsUsage: " ",
Description: `Looks for a new version on the offical download server.
If a new version exists, updates the agent binary and quits.
Otherwise, does nothing.
To determine if an update happened in a script, check for error code 64.`,
},
{
Name: "login",
Action: login,
Usage: "Generate a configuration file with your login details",
ArgsUsage: " ",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "url",
Hidden: true,
},
},
},
{
Name: "hello",
Action: helloWorld,
Usage: "Run a simple \"Hello World\" server for testing Argo Tunnel.",
Flags: []cli.Flag{
&cli.IntFlag{
Name: "port",
Usage: "Listen on the selected port.",
Value: 8080,
},
},
ArgsUsage: " ", // can't be the empty string or we get the default output
},
{
Name: "proxy-dns",
Action: tunneldns.Run,
Usage: "Run a DNS over HTTPS proxy server.",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "metrics",
Value: "localhost:",
Usage: "Listen address for metrics reporting.",
EnvVars: []string{"TUNNEL_METRICS"},
},
&cli.StringFlag{
Name: "address",
Usage: "Listen address for the DNS over HTTPS proxy server.",
Value: "localhost",
EnvVars: []string{"TUNNEL_DNS_ADDRESS"},
},
&cli.IntFlag{
Name: "port",
Usage: "Listen on given port for the DNS over HTTPS proxy server.",
Value: 53,
EnvVars: []string{"TUNNEL_DNS_PORT"},
},
&cli.StringSliceFlag{
Name: "upstream",
Usage: "Upstream endpoint URL, you can specify multiple endpoints for redundancy.",
Value: cli.NewStringSlice("https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query"),
EnvVars: []string{"TUNNEL_DNS_UPSTREAM"},
},
},
ArgsUsage: " ", // can't be the empty string or we get the default output
},
{
Name: "db",
Action: func(c *cli.Context) error {
tags := make(map[string]string)
tags["hostname"] = c.String("hostname")
raven.SetTagsContext(tags)
fmt.Printf("\nSQL Database Password: ")
pass, err := terminal.ReadPassword(int(syscall.Stdin))
if err != nil {
logger.Error(err)
}
go sqlgateway.StartProxy(c, logger, string(pass))
raven.CapturePanic(func() { err = startServer(c, shutdownC, graceShutdownC) }, nil)
if err != nil {
raven.CaptureError(err, nil)
}
return err
},
Before: func(c *cli.Context) error {
if c.String("config") == "" {
logger.Warnf("Cannot determine default configuration path. No file %v in %v", defaultConfigFiles, defaultConfigDirs)
}
inputSource, err := findInputSourceContext(c)
if err != nil {
logger.WithError(err).Infof("Cannot load configuration from %s", c.String("config"))
return err
} else if inputSource != nil {
err := altsrc.ApplyInputSourceValues(c, inputSource, app.Flags)
if err != nil {
logger.WithError(err).Infof("Cannot apply configuration from %s", c.String("config"))
return err
}
logger.Infof("Applied configuration from %s", c.String("config"))
}
return nil
},
Usage: "SQL Gateway is an SQL over HTTP reverse proxy",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "db",
Value: true,
Usage: "Enable the SQL Gateway Proxy",
},
&cli.StringFlag{
Name: "address",
Value: "",
Usage: "Database connection string: db://user:pass",
},
},
},
}
runApp(app, shutdownC, graceShutdownC)
}
func startServer(c *cli.Context, shutdownC, graceShutdownC chan struct{}) error {
var wg sync.WaitGroup
listeners := gracenet.Net{}
errC := make(chan error)
connectedSignal := make(chan struct{})
dnsReadySignal := make(chan struct{})
// check whether client provides enough flags or env variables. If not, print help.
if ok := enoughOptionsSet(c); !ok {
return nil
}
if err := configMainLogger(c); err != nil {
return errors.Wrap(err, "Error configuring logger")
}
protoLogger, err := configProtoLogger(c)
if err != nil {
return errors.Wrap(err, "Error configuring protocol logger")
}
if c.IsSet("trace-output") {
tmpTraceFile, err := ioutil.TempFile("", "trace")
if err != nil {
logger.WithError(err).Error("Failed to create new temporary file to save trace output")
}
defer func() {
if err := tmpTraceFile.Close(); err != nil {
logger.WithError(err).Error("Failed to close trace output file %s", tmpTraceFile.Name())
}
if err := os.Rename(tmpTraceFile.Name(), c.String("trace-output")); err != nil {
logger.WithError(err).Errorf("Failed to rename temporary trace output file %s to %s", tmpTraceFile.Name(), c.String("trace-output"))
} else {
os.Remove(tmpTraceFile.Name())
}
}()
if err := trace.Start(tmpTraceFile); err != nil {
logger.WithError(err).Error("Failed to start trace")
return errors.Wrap(err, "Error starting tracing")
}
defer trace.Stop()
}
if c.String("logfile") != "" {
if err := initLogFile(c, logger, protoLogger); err != nil {
logger.Error(err)
}
}
if err := handleDeprecatedOptions(c); err != nil {
return err
}
buildInfo := origin.GetBuildInfo()
logger.Infof("Build info: %+v", *buildInfo)
logger.Infof("Version %s", Version)
logClientOptions(c)
if c.IsSet("proxy-dns") {
wg.Add(1)
go func() {
defer wg.Done()
errC <- runDNSProxyServer(c, dnsReadySignal, shutdownC)
}()
} else {
close(dnsReadySignal)
}
// Wait for proxy-dns to come up (if used)
<-dnsReadySignal
// update needs to be after DNS proxy is up to resolve equinox server address
if isAutoupdateEnabled(c) {
logger.Infof("Autoupdate frequency is set to %v", c.Duration("autoupdate-freq"))
wg.Add(1)
go func() {
defer wg.Done()
errC <- autoupdate(c.Duration("autoupdate-freq"), &listeners, shutdownC)
}()
}
metricsListener, err := listeners.Listen("tcp", c.String("metrics"))
if err != nil {
logger.WithError(err).Error("Error opening metrics server listener")
return errors.Wrap(err, "Error opening metrics server listener")
}
defer metricsListener.Close()
wg.Add(1)
go func() {
defer wg.Done()
errC <- metrics.ServeMetrics(metricsListener, shutdownC, logger)
}()
go notifySystemd(connectedSignal)
if c.IsSet("pidfile") {
go writePidFile(connectedSignal, c.String("pidfile"))
}
// Serve DNS proxy stand-alone if no hostname or tag or app is going to run
if dnsProxyStandAlone(c) {
close(connectedSignal)
// no grace period, handle SIGINT/SIGTERM immediately
return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, 0)
}
if c.IsSet("hello-world") {
helloListener, err := hello.CreateTLSListener("127.0.0.1:")
if err != nil {
logger.WithError(err).Error("Cannot start Hello World Server")
return errors.Wrap(err, "Cannot start Hello World Server")
}
defer helloListener.Close()
wg.Add(1)
go func() {
defer wg.Done()
hello.StartHelloWorldServer(logger, helloListener, shutdownC)
}()
c.Set("url", "https://"+helloListener.Addr().String())
}
tunnelConfig, err := prepareTunnelConfig(c, buildInfo, logger, protoLogger)
if err != nil {
return err
}
wg.Add(1)
go func() {
defer wg.Done()
errC <- origin.StartTunnelDaemon(tunnelConfig, graceShutdownC, connectedSignal)
}()
return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, c.Duration("grace-period"))
}
func waitToShutdown(wg *sync.WaitGroup,
errC chan error,
shutdownC, graceShutdownC chan struct{},
gracePeriod time.Duration,
) error {
var err error
if gracePeriod > 0 {
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceShutdownC, gracePeriod)
} else {
err = waitForSignal(errC, shutdownC)
close(graceShutdownC)
}
if err != nil {
logger.WithError(err).Error("Quitting due to error")
} else {
logger.Info("Quitting...")
}
// Wait for clean exit, discarding all errors
go func() {
for range errC {
}
}()
wg.Wait()
return err
}
func notifySystemd(waitForSignal chan struct{}) {
<-waitForSignal
daemon.SdNotify(false, "READY=1")
}
func writePidFile(waitForSignal chan struct{}, pidFile string) {
<-waitForSignal
file, err := os.Create(pidFile)
if err != nil {
logger.WithError(err).Errorf("Unable to write pid to %s", pidFile)
}
defer file.Close()
fmt.Fprintf(file, "%d", os.Getpid())
}
func userHomeDir() (string, error) {
@@ -651,3 +115,7 @@ func userHomeDir() (string, error) {
}
return homeDir, nil
}
func isRunningFromTerminal() bool {
return terminal.IsTerminal(int(os.Stdout.Fd()))
}

View File

@@ -8,9 +8,9 @@ import (
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"text/template"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/mitchellh/go-homedir"
)
@@ -94,7 +94,7 @@ func runCommand(command string, args ...string) error {
}
func ensureConfigDirExists(configDir string) error {
ok, err := fileExists(configDir)
ok, err := config.FileExists(configDir)
if !ok && err == nil {
err = os.Mkdir(configDir, 0700)
}
@@ -119,8 +119,7 @@ func openFile(path string, create bool) (file *os.File, exists bool, err error)
return file, false, err
}
func copyCertificate(srcConfigDir, destConfigDir, credentialFile string) error {
destCredentialPath := filepath.Join(destConfigDir, credentialFile)
func copyCredential(srcCredentialPath, destCredentialPath string) error {
destFile, exists, err := openFile(destCredentialPath, true)
if err != nil {
return err
@@ -130,7 +129,6 @@ func copyCertificate(srcConfigDir, destConfigDir, credentialFile string) error {
}
defer destFile.Close()
srcCredentialPath := filepath.Join(srcConfigDir, credentialFile)
srcFile, _, err := openFile(srcCredentialPath, false)
if err != nil {
return err
@@ -146,17 +144,8 @@ func copyCertificate(srcConfigDir, destConfigDir, credentialFile string) error {
return nil
}
func copyCredentials(serviceConfigDir, defaultConfigDir, defaultConfigFile, defaultCredentialFile string) error {
if err := ensureConfigDirExists(serviceConfigDir); err != nil {
return err
}
if err := copyCertificate(defaultConfigDir, serviceConfigDir, defaultCredentialFile); err != nil {
return err
}
func copyConfig(srcConfigPath, destConfigPath string) error {
// Copy or create config
destConfigPath := filepath.Join(serviceConfigDir, defaultConfigFile)
destFile, exists, err := openFile(destConfigPath, true)
if err != nil {
logger.WithError(err).Infof("cannot open %s", destConfigPath)
@@ -167,7 +156,6 @@ func copyCredentials(serviceConfigDir, defaultConfigDir, defaultConfigFile, defa
}
defer destFile.Close()
srcConfigPath := filepath.Join(defaultConfigDir, defaultConfigFile)
srcFile, _, err := openFile(srcConfigPath, false)
if err != nil {
fmt.Println("Your service needs a config file that at least specifies the hostname option.")

View File

@@ -0,0 +1,47 @@
package shell
import (
"io"
"os"
"os/exec"
"runtime"
)
// OpenBrowser opens the specified URL in the default browser of the user
func OpenBrowser(url string) error {
var cmd string
var args []string
switch runtime.GOOS {
case "windows":
cmd = "cmd"
args = []string{"/c", "start"}
case "darwin":
cmd = "open"
default: // "linux", "freebsd", "openbsd", "netbsd"
cmd = "xdg-open"
}
args = append(args, url)
return exec.Command(cmd, args...).Start()
}
// Run will kick off a shell task and pipe the results to the respective std pipes
func Run(cmd string, args ...string) error {
c := exec.Command(cmd, args...)
stderr, err := c.StderrPipe()
if err != nil {
return err
}
go func() {
io.Copy(os.Stderr, stderr)
}()
stdout, err := c.StdoutPipe()
if err != nil {
return err
}
go func() {
io.Copy(os.Stdout, stdout)
}()
return c.Run()
}

View File

@@ -0,0 +1,171 @@
package transfer
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"path/filepath"
"time"
"github.com/cloudflare/cloudflared/cmd/cloudflared/encrypter"
"github.com/cloudflare/cloudflared/cmd/cloudflared/shell"
"github.com/cloudflare/cloudflared/log"
cli "gopkg.in/urfave/cli.v2"
)
const (
baseStoreURL = "https://login.cloudflarewarp.com/"
clientTimeout = time.Second * 60
)
var logger = log.CreateLogger()
// Run does the transfer "dance" with the end result downloading the supported resource.
// The expanded description is run is encapsulation of shared business logic needed
// to request a resource (token/cert/etc) from the transfer service (loginhelper).
// The "dance" we refer to is building a HTTP request, opening that in a browser waiting for
// the user to complete an action, while it long polls in the background waiting for an
// action to be completed to download the resource.
func Run(c *cli.Context, transferURL *url.URL, resourceName, key, value, path string, shouldEncrypt bool) ([]byte, error) {
encrypterClient, err := encrypter.New("cloudflared_priv.pem", "cloudflared_pub.pem")
if err != nil {
return nil, err
}
requestURL, err := buildRequestURL(transferURL, key, value+encrypterClient.PublicKey(), shouldEncrypt)
if err != nil {
return nil, err
}
err = shell.OpenBrowser(requestURL)
if err != nil {
fmt.Fprintf(os.Stdout, "Please open the following URL and log in with your Cloudflare account:\n\n%s\n\nLeave cloudflared running to download the %s automatically.\n", resourceName, requestURL)
} else {
fmt.Fprintf(os.Stdout, "A browser window should have opened at the following URL:\n\n%s\n\nIf the browser failed to open, open it yourself and visit the URL above.\n", requestURL)
}
// for local debugging
baseURL := baseStoreURL
if c.IsSet("url") {
baseURL = c.String("url")
}
var resourceData []byte
if shouldEncrypt {
buf, key, err := transferRequest(baseURL + filepath.Join("transfer", encrypterClient.PublicKey()))
if err != nil {
return nil, err
}
decodedBuf, err := base64.StdEncoding.DecodeString(string(buf))
if err != nil {
return nil, err
}
decrypted, err := encrypterClient.Decrypt(decodedBuf, key)
if err != nil {
return nil, err
}
resourceData = decrypted
} else {
buf, _, err := transferRequest(baseURL + filepath.Join(encrypterClient.PublicKey()))
if err != nil {
return nil, err
}
resourceData = buf
}
if err := ioutil.WriteFile(path, resourceData, 0600); err != nil {
return nil, err
}
return resourceData, nil
}
// BuildRequestURL creates a request suitable for a resource transfer.
// it will return a constructed url based off the base url and query key/value provided.
// follow will follow redirects.
func buildRequestURL(baseURL *url.URL, key, value string, follow bool) (string, error) {
q := baseURL.Query()
q.Set(key, value)
baseURL.RawQuery = q.Encode()
if !follow {
return baseURL.String(), nil
}
response, err := http.Get(baseURL.String())
if err != nil {
return "", err
}
return response.Request.URL.String(), nil
}
// transferRequest downloads the requested resource from the request URL
func transferRequest(requestURL string) ([]byte, string, error) {
client := &http.Client{Timeout: clientTimeout}
const pollAttempts = 10
// we do "long polling" on the endpoint to get the resource.
for i := 0; i < pollAttempts; i++ {
buf, key, err := poll(client, requestURL)
if err != nil {
return nil, "", err
} else if len(buf) > 0 {
if err := putSuccess(client, requestURL); err != nil {
logger.WithError(err).Error("Failed to update resource success")
}
return buf, key, nil
}
}
return nil, "", errors.New("Failed to fetch resource")
}
// poll the endpoint for the request resource, waiting for the user interaction
func poll(client *http.Client, requestURL string) ([]byte, string, error) {
resp, err := client.Get(requestURL)
if err != nil {
return nil, "", err
}
defer resp.Body.Close()
// ignore everything other than server errors as the resource
// may not exist until the user does the interaction
if resp.StatusCode >= 500 {
return nil, "", fmt.Errorf("error on request %d", resp.StatusCode)
}
if resp.StatusCode != 200 {
logger.Info("Waiting for login...")
return nil, "", nil
}
buf := new(bytes.Buffer)
if _, err := io.Copy(buf, resp.Body); err != nil {
return nil, "", err
}
return buf.Bytes(), resp.Header.Get("service-public-key"), nil
}
// putSuccess tells the server we successfully downloaded the resource
func putSuccess(client *http.Client, requestURL string) error {
req, err := http.NewRequest("PUT", requestURL+"/ok", nil)
if err != nil {
return err
}
resp, err := client.Do(req)
if err != nil {
return err
}
resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("HTTP Response Status Code: %d", resp.StatusCode)
}
return nil
}

View File

@@ -0,0 +1,658 @@
package tunnel
import (
"fmt"
"io/ioutil"
"os"
"runtime/trace"
"sync"
"syscall"
"time"
"github.com/getsentry/raven-go"
"golang.org/x/crypto/ssh/terminal"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
"github.com/cloudflare/cloudflared/cmd/sqlgateway"
"github.com/cloudflare/cloudflared/hello"
"github.com/cloudflare/cloudflared/metrics"
"github.com/cloudflare/cloudflared/origin"
"github.com/cloudflare/cloudflared/tunneldns"
"github.com/coreos/go-systemd/daemon"
"github.com/facebookgo/grace/gracenet"
"github.com/pkg/errors"
cli "gopkg.in/urfave/cli.v2"
"gopkg.in/urfave/cli.v2/altsrc"
)
const sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b:3e8827f6f9f740738eb11138f7bebb68@sentry.io/189878"
var (
shutdownC chan struct{}
graceShutdownC chan struct{}
version string
)
func Flags() []cli.Flag {
return tunnelFlags(true)
}
func Commands() []*cli.Command {
cmds := []*cli.Command{
{
Name: "login",
Action: login,
Usage: "Generate a configuration file with your login details",
ArgsUsage: " ",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "url",
Hidden: true,
},
},
Hidden: true,
},
{
Name: "hello",
Action: helloWorld,
Usage: "Run a simple \"Hello World\" server for testing Argo Tunnel.",
Flags: []cli.Flag{
&cli.IntFlag{
Name: "port",
Usage: "Listen on the selected port.",
Value: 8080,
},
},
ArgsUsage: " ", // can't be the empty string or we get the default output
Hidden: true,
},
{
Name: "proxy-dns",
Action: tunneldns.Run,
Usage: "Run a DNS over HTTPS proxy server.",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "metrics",
Value: "localhost:",
Usage: "Listen address for metrics reporting.",
EnvVars: []string{"TUNNEL_METRICS"},
},
&cli.StringFlag{
Name: "address",
Usage: "Listen address for the DNS over HTTPS proxy server.",
Value: "localhost",
EnvVars: []string{"TUNNEL_DNS_ADDRESS"},
},
&cli.IntFlag{
Name: "port",
Usage: "Listen on given port for the DNS over HTTPS proxy server.",
Value: 53,
EnvVars: []string{"TUNNEL_DNS_PORT"},
},
&cli.StringSliceFlag{
Name: "upstream",
Usage: "Upstream endpoint URL, you can specify multiple endpoints for redundancy.",
Value: cli.NewStringSlice("https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query"),
EnvVars: []string{"TUNNEL_DNS_UPSTREAM"},
},
},
ArgsUsage: " ", // can't be the empty string or we get the default output
Hidden: false,
},
{
Name: "db",
Action: func(c *cli.Context) error {
tags := make(map[string]string)
tags["hostname"] = c.String("hostname")
raven.SetTagsContext(tags)
fmt.Printf("\nSQL Database Password: ")
pass, err := terminal.ReadPassword(int(syscall.Stdin))
if err != nil {
logger.Error(err)
}
go sqlgateway.StartProxy(c, logger, string(pass))
raven.CapturePanic(func() { err = tunnel(c) }, nil)
if err != nil {
raven.CaptureError(err, nil)
}
return err
},
Before: Before,
Usage: "SQL Gateway is an SQL over HTTP reverse proxy",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "db",
Value: true,
Usage: "Enable the SQL Gateway Proxy",
},
&cli.StringFlag{
Name: "address",
Value: "",
Usage: "Database connection string: db://user:pass",
},
},
Hidden: true,
},
}
var subcommands []*cli.Command
for _, cmd := range cmds {
c := *cmd
c.Hidden = false
subcommands = append(subcommands, &c)
}
cmds = append(cmds, &cli.Command{
Name: "tunnel",
Action: tunnel,
Before: Before,
Category: "Tunnel",
Usage: "Make a locally-running web service accessible over the internet using Argo Tunnel.",
ArgsUsage: "[origin-url]",
Description: `Argo Tunnel asks you to specify a hostname on a Cloudflare-powered
domain you control and a local address. Traffic from that hostname is routed
(optionally via a Cloudflare Load Balancer) to this machine and appears on the
specified port where it can be served.
This feature requires your Cloudflare account be subscribed to the Argo Smart Routing feature.
To use, begin by calling login to download a certificate:
cloudflared tunnel login
With your certificate installed you can then launch your first tunnel,
replacing my.site.com with a subdomain of your site:
cloudflared tunnel --hostname my.site.com --url http://localhost:8080
If you have a web server running on port 8080 (in this example), it will be available on
the internet!`,
Subcommands: subcommands,
Flags: tunnelFlags(false),
})
return cmds
}
func tunnel(c *cli.Context) error {
return StartServer(c, version, shutdownC, graceShutdownC)
}
func Init(v string, s, g chan struct{}) {
version, shutdownC, graceShutdownC = v, s, g
}
func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan struct{}) error {
raven.SetDSN(sentryDSN)
var wg sync.WaitGroup
listeners := gracenet.Net{}
errC := make(chan error)
connectedSignal := make(chan struct{})
dnsReadySignal := make(chan struct{})
if c.String("config") == "" {
logger.Warnf("Cannot determine default configuration path. No file %v in %v", config.DefaultConfigFiles, config.DefaultConfigDirs)
}
if err := configMainLogger(c); err != nil {
return errors.Wrap(err, "Error configuring logger")
}
protoLogger, err := configProtoLogger(c)
if err != nil {
return errors.Wrap(err, "Error configuring protocol logger")
}
if c.IsSet("trace-output") {
tmpTraceFile, err := ioutil.TempFile("", "trace")
if err != nil {
logger.WithError(err).Error("Failed to create new temporary file to save trace output")
}
defer func() {
if err := tmpTraceFile.Close(); err != nil {
logger.WithError(err).Errorf("Failed to close trace output file %s", tmpTraceFile.Name())
}
if err := os.Rename(tmpTraceFile.Name(), c.String("trace-output")); err != nil {
logger.WithError(err).Errorf("Failed to rename temporary trace output file %s to %s", tmpTraceFile.Name(), c.String("trace-output"))
} else {
os.Remove(tmpTraceFile.Name())
}
}()
if err := trace.Start(tmpTraceFile); err != nil {
logger.WithError(err).Error("Failed to start trace")
return errors.Wrap(err, "Error starting tracing")
}
defer trace.Stop()
}
if c.String("logfile") != "" {
if err := initLogFile(c, logger, protoLogger); err != nil {
logger.Error(err)
}
}
if err := handleDeprecatedOptions(c); err != nil {
return err
}
buildInfo := origin.GetBuildInfo()
logger.Infof("Build info: %+v", *buildInfo)
logger.Infof("Version %s", version)
logClientOptions(c)
if c.IsSet("proxy-dns") {
wg.Add(1)
go func() {
defer wg.Done()
errC <- runDNSProxyServer(c, dnsReadySignal, shutdownC)
}()
} else {
close(dnsReadySignal)
}
// Wait for proxy-dns to come up (if used)
<-dnsReadySignal
// update needs to be after DNS proxy is up to resolve equinox server address
if updater.IsAutoupdateEnabled(c) {
logger.Infof("Autoupdate frequency is set to %v", c.Duration("autoupdate-freq"))
wg.Add(1)
go func() {
defer wg.Done()
errC <- updater.Autoupdate(c.Duration("autoupdate-freq"), &listeners, shutdownC)
}()
}
metricsListener, err := listeners.Listen("tcp", c.String("metrics"))
if err != nil {
logger.WithError(err).Error("Error opening metrics server listener")
return errors.Wrap(err, "Error opening metrics server listener")
}
defer metricsListener.Close()
wg.Add(1)
go func() {
defer wg.Done()
errC <- metrics.ServeMetrics(metricsListener, shutdownC, logger)
}()
go notifySystemd(connectedSignal)
if c.IsSet("pidfile") {
go writePidFile(connectedSignal, c.String("pidfile"))
}
// Serve DNS proxy stand-alone if no hostname or tag or app is going to run
if dnsProxyStandAlone(c) {
close(connectedSignal)
// no grace period, handle SIGINT/SIGTERM immediately
return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, 0)
}
if c.IsSet("hello-world") {
helloListener, err := hello.CreateTLSListener("127.0.0.1:")
if err != nil {
logger.WithError(err).Error("Cannot start Hello World Server")
return errors.Wrap(err, "Cannot start Hello World Server")
}
defer helloListener.Close()
wg.Add(1)
go func() {
defer wg.Done()
hello.StartHelloWorldServer(logger, helloListener, shutdownC)
}()
c.Set("url", "https://"+helloListener.Addr().String())
}
tunnelConfig, err := prepareTunnelConfig(c, buildInfo, version, logger, protoLogger)
if err != nil {
return err
}
wg.Add(1)
go func() {
defer wg.Done()
errC <- origin.StartTunnelDaemon(tunnelConfig, graceShutdownC, connectedSignal)
}()
return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, c.Duration("grace-period"))
}
func Before(c *cli.Context) error {
if c.String("config") == "" {
logger.Warnf("Cannot determine default configuration path. No file %v in %v", config.DefaultConfigFiles, config.DefaultConfigDirs)
}
inputSource, err := config.FindInputSourceContext(c)
if err != nil {
logger.WithError(err).Infof("Cannot load configuration from %s", c.String("config"))
return err
} else if inputSource != nil {
err := altsrc.ApplyInputSourceValues(c, inputSource, c.App.Flags)
if err != nil {
logger.WithError(err).Infof("Cannot apply configuration from %s", c.String("config"))
return err
}
logger.Infof("Applied configuration from %s", c.String("config"))
}
return nil
}
func waitToShutdown(wg *sync.WaitGroup,
errC chan error,
shutdownC, graceShutdownC chan struct{},
gracePeriod time.Duration,
) error {
var err error
if gracePeriod > 0 {
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceShutdownC, gracePeriod)
} else {
err = waitForSignal(errC, shutdownC)
close(graceShutdownC)
}
if err != nil {
logger.WithError(err).Error("Quitting due to error")
} else {
logger.Info("Quitting...")
}
// Wait for clean exit, discarding all errors
go func() {
for range errC {
}
}()
wg.Wait()
return err
}
func notifySystemd(waitForSignal chan struct{}) {
<-waitForSignal
daemon.SdNotify(false, "READY=1")
}
func writePidFile(waitForSignal chan struct{}, pidFile string) {
<-waitForSignal
file, err := os.Create(pidFile)
if err != nil {
logger.WithError(err).Errorf("Unable to write pid to %s", pidFile)
}
defer file.Close()
fmt.Fprintf(file, "%d", os.Getpid())
}
func tunnelFlags(shouldHide bool) []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "config",
Usage: "Specifies a config file in YAML format.",
Value: config.FindDefaultConfigPath(),
Hidden: shouldHide,
},
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: "autoupdate-freq",
Usage: "Autoupdate frequency. Default is 24h.",
Value: time.Hour * 24,
Hidden: shouldHide,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "no-autoupdate",
Usage: "Disable periodic check for updates, restarting the server with the new version.",
Value: false,
Hidden: shouldHide,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "is-autoupdated",
Usage: "Signal the new process that Argo Tunnel client has been autoupdated",
Value: false,
Hidden: true,
}),
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
Name: "edge",
Usage: "Address of the Cloudflare tunnel server.",
EnvVars: []string{"TUNNEL_EDGE"},
Hidden: true,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "cacert",
Usage: "Certificate Authority authenticating the Cloudflare tunnel connection.",
EnvVars: []string{"TUNNEL_CACERT"},
Hidden: true,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "no-tls-verify",
Usage: "Disables TLS verification of the certificate presented by your origin. Will allow any certificate from the origin to be accepted. Note: The connection from your machine to Cloudflare's Edge is still encrypted.",
EnvVars: []string{"NO_TLS_VERIFY"},
Hidden: shouldHide,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "origincert",
Usage: "Path to the certificate generated for your origin when you run cloudflared login.",
EnvVars: []string{"TUNNEL_ORIGIN_CERT"},
Value: findDefaultOriginCertPath(),
Hidden: shouldHide,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "origin-ca-pool",
Usage: "Path to the CA for the certificate of your origin. This option should be used only if your certificate is not signed by Cloudflare.",
EnvVars: []string{"TUNNEL_ORIGIN_CA_POOL"},
Hidden: shouldHide,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "url",
Value: "http://localhost:8080",
Usage: "Connect to the local webserver at `URL`.",
EnvVars: []string{"TUNNEL_URL"},
Hidden: shouldHide,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "hostname",
Usage: "Set a hostname on a Cloudflare zone to route traffic through this tunnel.",
EnvVars: []string{"TUNNEL_HOSTNAME"},
Hidden: shouldHide,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "origin-server-name",
Usage: "Hostname on the origin server certificate.",
EnvVars: []string{"TUNNEL_ORIGIN_SERVER_NAME"},
Hidden: shouldHide,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "id",
Usage: "A unique identifier used to tie connections to this tunnel instance.",
EnvVars: []string{"TUNNEL_ID"},
Hidden: true,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "lb-pool",
Usage: "The name of a (new/existing) load balancing pool to add this origin to.",
EnvVars: []string{"TUNNEL_LB_POOL"},
Hidden: shouldHide,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "api-key",
Usage: "This parameter has been deprecated since version 2017.10.1.",
EnvVars: []string{"TUNNEL_API_KEY"},
Hidden: true,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "api-email",
Usage: "This parameter has been deprecated since version 2017.10.1.",
EnvVars: []string{"TUNNEL_API_EMAIL"},
Hidden: true,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "api-ca-key",
Usage: "This parameter has been deprecated since version 2017.10.1.",
EnvVars: []string{"TUNNEL_API_CA_KEY"},
Hidden: true,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "metrics",
Value: "localhost:",
Usage: "Listen address for metrics reporting.",
EnvVars: []string{"TUNNEL_METRICS"},
Hidden: shouldHide,
}),
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: "metrics-update-freq",
Usage: "Frequency to update tunnel metrics",
Value: time.Second * 5,
EnvVars: []string{"TUNNEL_METRICS_UPDATE_FREQ"},
Hidden: shouldHide,
}),
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
Name: "tag",
Usage: "Custom tags used to identify this tunnel, in format `KEY=VALUE`. Multiple tags may be specified",
EnvVars: []string{"TUNNEL_TAG"},
Hidden: shouldHide,
}),
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: "heartbeat-interval",
Usage: "Minimum idle time before sending a heartbeat.",
Value: time.Second * 5,
Hidden: true,
}),
altsrc.NewUint64Flag(&cli.Uint64Flag{
Name: "heartbeat-count",
Usage: "Minimum number of unacked heartbeats to send before closing the connection.",
Value: 5,
Hidden: true,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "loglevel",
Value: "info",
Usage: "Application logging level {panic, fatal, error, warn, info, debug}",
EnvVars: []string{"TUNNEL_LOGLEVEL"},
Hidden: shouldHide,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "proto-loglevel",
Value: "warn",
Usage: "Protocol logging level {panic, fatal, error, warn, info, debug}",
EnvVars: []string{"TUNNEL_PROTO_LOGLEVEL"},
Hidden: shouldHide,
}),
altsrc.NewUintFlag(&cli.UintFlag{
Name: "retries",
Value: 5,
Usage: "Maximum number of retries for connection/protocol errors.",
EnvVars: []string{"TUNNEL_RETRIES"},
Hidden: shouldHide,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "hello-world",
Value: false,
Usage: "Run Hello World Server",
EnvVars: []string{"TUNNEL_HELLO_WORLD"},
Hidden: shouldHide,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "pidfile",
Usage: "Write the application's PID to this file after first successful connection.",
EnvVars: []string{"TUNNEL_PIDFILE"},
Hidden: shouldHide,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "logfile",
Usage: "Save application log to this file for reporting issues.",
EnvVars: []string{"TUNNEL_LOGFILE"},
Hidden: shouldHide,
}),
altsrc.NewIntFlag(&cli.IntFlag{
Name: "ha-connections",
Value: 4,
Hidden: true,
}),
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: "proxy-connect-timeout",
Usage: "HTTP proxy timeout for establishing a new connection",
Value: time.Second * 30,
Hidden: shouldHide,
}),
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: "proxy-tls-timeout",
Usage: "HTTP proxy timeout for completing a TLS handshake",
Value: time.Second * 10,
Hidden: shouldHide,
}),
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: "proxy-tcp-keepalive",
Usage: "HTTP proxy TCP keepalive duration",
Value: time.Second * 30,
Hidden: shouldHide,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "proxy-no-happy-eyeballs",
Usage: "HTTP proxy should disable \"happy eyeballs\" for IPv4/v6 fallback",
Hidden: shouldHide,
}),
altsrc.NewIntFlag(&cli.IntFlag{
Name: "proxy-keepalive-connections",
Usage: "HTTP proxy maximum keepalive connection pool size",
Value: 100,
Hidden: shouldHide,
}),
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: "proxy-keepalive-timeout",
Usage: "HTTP proxy timeout for closing an idle connection",
Value: time.Second * 90,
Hidden: shouldHide,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "proxy-dns",
Usage: "Run a DNS over HTTPS proxy server.",
EnvVars: []string{"TUNNEL_DNS"},
Hidden: shouldHide,
}),
altsrc.NewIntFlag(&cli.IntFlag{
Name: "proxy-dns-port",
Value: 53,
Usage: "Listen on given port for the DNS over HTTPS proxy server.",
EnvVars: []string{"TUNNEL_DNS_PORT"},
Hidden: shouldHide,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "proxy-dns-address",
Usage: "Listen address for the DNS over HTTPS proxy server.",
Value: "localhost",
EnvVars: []string{"TUNNEL_DNS_ADDRESS"},
Hidden: shouldHide,
}),
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
Name: "proxy-dns-upstream",
Usage: "Upstream endpoint URL, you can specify multiple endpoints for redundancy.",
Value: cli.NewStringSlice("https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query"),
EnvVars: []string{"TUNNEL_DNS_UPSTREAM"},
Hidden: shouldHide,
}),
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: "grace-period",
Usage: "Duration to accept new requests after cloudflared receives first SIGINT/SIGTERM. A second SIGINT/SIGTERM will force cloudflared to shutdown immediately.",
Value: time.Second * 30,
EnvVars: []string{"TUNNEL_GRACE_PERIOD"},
Hidden: true,
}),
altsrc.NewUintFlag(&cli.UintFlag{
Name: "compression-quality",
Value: 0,
Usage: "(beta) Use cross-stream compression instead HTTP compression. 0-off, 1-low, 2-medium, >=3-high.",
EnvVars: []string{"TUNNEL_COMPRESSION_LEVEL"},
Hidden: shouldHide,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "no-chunked-encoding",
Usage: "Disables chunked transfer encoding; useful if you are running a WSGI server.",
EnvVars: []string{"TUNNEL_NO_CHUNKED_ENCODING"},
Hidden: shouldHide,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "trace-output",
Usage: "Name of trace output file, generated when cloudflared stops.",
EnvVars: []string{"TUNNEL_TRACE_OUTPUT"},
Hidden: shouldHide,
}),
}
}

View File

@@ -1,4 +1,4 @@
package main
package tunnel
import (
"crypto/tls"
@@ -15,80 +15,39 @@ import (
"strings"
"time"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/origin"
"github.com/cloudflare/cloudflared/tlsconfig"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/cloudflare/cloudflared/validation"
"golang.org/x/crypto/ssh/terminal"
"github.com/sirupsen/logrus"
"gopkg.in/urfave/cli.v2"
"gopkg.in/urfave/cli.v2/altsrc"
"github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
)
var (
defaultConfigFiles = []string{"config.yml", "config.yaml"}
// Launchd doesn't set root env variables, so there is default
// Windows default config dir was ~/cloudflare-warp in documentation; let's keep it compatible
defaultConfigDirs = []string{"~/.cloudflared", "~/.cloudflare-warp", "~/cloudflare-warp", "/usr/local/etc/cloudflared", "/etc/cloudflared"}
developerPortal = "https://developers.cloudflare.com/argo-tunnel"
quickStartUrl = developerPortal + "/quickstart/quickstart/"
serviceUrl = developerPortal + "/reference/service/"
argumentsUrl = developerPortal + "/reference/arguments/"
)
const defaultCredentialFile = "cert.pem"
func fileExists(path string) (bool, error) {
f, err := os.Open(path)
if err != nil {
if os.IsNotExist(err) {
// ignore missing files
return false, nil
}
return false, err
}
f.Close()
return true, nil
}
// returns the first path that contains a cert.pem file. If none of the defaultConfigDirs
// (differs by OS for legacy reasons) contains a cert.pem file, return empty string
// returns the first path that contains a cert.pem file. If none of the DefaultConfigDirs
// contains a cert.pem file, return empty string
func findDefaultOriginCertPath() string {
for _, defaultConfigDir := range defaultConfigDirs {
originCertPath, _ := homedir.Expand(filepath.Join(defaultConfigDir, defaultCredentialFile))
if ok, _ := fileExists(originCertPath); ok {
for _, defaultConfigDir := range config.DefaultConfigDirs {
originCertPath, _ := homedir.Expand(filepath.Join(defaultConfigDir, config.DefaultCredentialFile))
if ok, _ := config.FileExists(originCertPath); ok {
return originCertPath
}
}
return ""
}
// returns the first path that contains a config file. If none of the combination of
// defaultConfigDirs (differs by OS for legacy reasons) and defaultConfigFiles
// contains a config file, return empty string
func findDefaultConfigPath() string {
for _, configDir := range defaultConfigDirs {
for _, configFile := range defaultConfigFiles {
dirPath, err := homedir.Expand(configDir)
if err != nil {
continue
}
path := filepath.Join(dirPath, configFile)
if ok, _ := fileExists(path); ok {
return path
}
}
}
return ""
}
func findInputSourceContext(context *cli.Context) (altsrc.InputSourceContext, error) {
if context.String("config") != "" {
return altsrc.NewYamlSourceFromFile(context.String("config"))
}
return nil, nil
}
func generateRandomClientID() string {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
id := make([]byte, 32)
@@ -96,24 +55,6 @@ func generateRandomClientID() string {
return hex.EncodeToString(id)
}
func enoughOptionsSet(c *cli.Context) bool {
// For cloudflared to work, the user needs to at least provide a hostname,
// or runs as stand alone DNS proxy .
// When using sudo, use -E flag to preserve env vars
if c.NumFlags() == 0 && c.NArg() == 0 && os.Getenv("TUNNEL_HOSTNAME") == "" && os.Getenv("TUNNEL_DNS") == "" {
if isRunningFromTerminal() {
logger.Errorf("No arguments were provided. You need to at least specify the hostname for this tunnel. See %s", quickStartUrl)
logger.Infof("If you want to run Argo Tunnel client as a stand alone DNS proxy, run with --proxy-dns option or set TUNNEL_DNS environment variable.")
} else {
logger.Errorf("You need to specify all the options in a configuration file, or use environment variables. See %s and %s", serviceUrl, argumentsUrl)
logger.Infof("If you want to run Argo Tunnel client as a stand alone DNS proxy, specify proxy-dns option in the configuration file, or set TUNNEL_DNS environment variable.")
}
cli.ShowAppHelp(c)
return false
}
return true
}
func handleDeprecatedOptions(c *cli.Context) error {
// Fail if the user provided an old authentication method
if c.IsSet("api-key") || c.IsSet("api-email") || c.IsSet("api-ca-key") {
@@ -123,12 +64,14 @@ func handleDeprecatedOptions(c *cli.Context) error {
return nil
}
// validate url. It can be either from --url or argument
func validateUrl(c *cli.Context) (string, error) {
var url = c.String("url")
if c.NArg() > 0 {
if !c.IsSet("url") {
return "", errors.New("Please specify an origin URL.")
if c.IsSet("url") {
return "", errors.New("Specified origin urls using both --url and argument. Decide which one you want, I can only support one.")
}
url = c.Args().Get(0)
}
validUrl, err := validation.ValidateUrl(url)
return validUrl, err
@@ -165,7 +108,7 @@ func dnsProxyStandAlone(c *cli.Context) bool {
func getOriginCert(c *cli.Context) ([]byte, error) {
if c.String("origincert") == "" {
logger.Warnf("Cannot determine default origin certificate path. No file %s in %v", defaultCredentialFile, defaultConfigDirs)
logger.Warnf("Cannot determine default origin certificate path. No file %s in %v", config.DefaultCredentialFile, config.DefaultConfigDirs)
if isRunningFromTerminal() {
logger.Errorf("You need to specify the origin certificate path with --origincert option, or set TUNNEL_ORIGIN_CERT environment variable. See %s for more information.", argumentsUrl)
return nil, fmt.Errorf("Client didn't specify origincert path when running from terminal")
@@ -180,7 +123,7 @@ func getOriginCert(c *cli.Context) ([]byte, error) {
logger.WithError(err).Errorf("Cannot resolve path %s", c.String("origincert"))
return nil, fmt.Errorf("Cannot resolve path %s", c.String("origincert"))
}
ok, err := fileExists(originCertPath)
ok, err := config.FileExists(originCertPath)
if err != nil {
logger.Errorf("Cannot check if origin cert exists at path %s", c.String("origincert"))
return nil, fmt.Errorf("Cannot check if origin cert exists at path %s", c.String("origincert"))
@@ -206,7 +149,7 @@ If you don't have a certificate signed by Cloudflare, run the command:
return originCert, nil
}
func prepareTunnelConfig(c *cli.Context, buildInfo *origin.BuildInfo, logger, protoLogger *logrus.Logger) (*origin.TunnelConfig, error) {
func prepareTunnelConfig(c *cli.Context, buildInfo *origin.BuildInfo, version string, logger, protoLogger *logrus.Logger) (*origin.TunnelConfig, error) {
hostname, err := validation.ValidateHostname(c.String("hostname"))
if err != nil {
logger.WithError(err).Error("Invalid hostname")
@@ -225,12 +168,12 @@ func prepareTunnelConfig(c *cli.Context, buildInfo *origin.BuildInfo, logger, pr
tags = append(tags, tunnelpogs.Tag{Name: "ID", Value: clientID})
url, err := validateUrl(c)
originURL, err := validateUrl(c)
if err != nil {
logger.WithError(err).Error("Error validating url")
return nil, errors.Wrap(err, "Error validating url")
logger.WithError(err).Error("Error validating origin URL")
return nil, errors.Wrap(err, "Error validating origin URL")
}
logger.Infof("Proxying tunnel requests to %s", url)
logger.Infof("Proxying tunnel requests to %s", originURL)
originCert, err := getOriginCert(c)
if err != nil {
@@ -262,9 +205,15 @@ func prepareTunnelConfig(c *cli.Context, buildInfo *origin.BuildInfo, logger, pr
httpTransport.TLSClientConfig.ServerName = c.String("origin-server-name")
}
err = validation.ValidateHTTPService(originURL, httpTransport)
if err != nil {
logger.WithError(err).Error("unable to connect to the origin")
return nil, errors.Wrap(err, "unable to connect to the origin")
}
return &origin.TunnelConfig{
EdgeAddrs: c.StringSlice("edge"),
OriginUrl: url,
OriginUrl: originURL,
Hostname: hostname,
OriginCert: originCert,
TlsConfig: tlsconfig.CreateTunnelConfig(c, c.StringSlice("edge")),
@@ -274,7 +223,7 @@ func prepareTunnelConfig(c *cli.Context, buildInfo *origin.BuildInfo, logger, pr
MaxHeartbeats: c.Uint64("heartbeat-count"),
ClientID: clientID,
BuildInfo: buildInfo,
ReportedVersion: Version,
ReportedVersion: version,
LBPool: c.String("lb-pool"),
Tags: tags,
HAConnections: c.Int("ha-connections"),
@@ -316,3 +265,7 @@ func loadCertPool(c *cli.Context, logger *logrus.Logger) (*x509.CertPool, error)
return originCertPool, nil
}
func isRunningFromTerminal() bool {
return terminal.IsTerminal(int(os.Stdout.Fd()))
}

View File

@@ -1,4 +1,4 @@
package main
package tunnel
import (
"fmt"
@@ -8,7 +8,6 @@ import (
"github.com/cloudflare/cloudflared/hello"
)
func helloWorld(c *cli.Context) error {
address := fmt.Sprintf(":%d", c.Int("port"))
listener, err := hello.CreateTLSListener(address)

View File

@@ -1,11 +1,10 @@
package main
package tunnel
import (
"fmt"
"os"
"github.com/cloudflare/cloudflared/log"
"github.com/rifflock/lfshook"
"github.com/sirupsen/logrus"
"gopkg.in/urfave/cli.v2"

View File

@@ -0,0 +1,69 @@
package tunnel
import (
"fmt"
"net/url"
"os"
"path/filepath"
"syscall"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/cmd/cloudflared/transfer"
homedir "github.com/mitchellh/go-homedir"
cli "gopkg.in/urfave/cli.v2"
)
const (
baseLoginURL = "https://dash.cloudflare.com/argotunnel"
callbackStoreURL = "https://login.cloudflarewarp.com/"
)
func login(c *cli.Context) error {
path, ok, err := checkForExistingCert()
if ok {
fmt.Fprintf(os.Stdout, "You have an existing certificate at %s which login would overwrite.\nIf this is intentional, please move or delete that file then run this command again.\n", path)
return nil
} else if err != nil {
return err
}
loginURL, err := url.Parse(baseLoginURL)
if err != nil {
// shouldn't happen, URL is hardcoded
return err
}
_, err = transfer.Run(c, loginURL, "cert", "callback", callbackStoreURL, path, false)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to write the certificate due to the following error:\n%v\n\nYour browser will download the certificate instead. You will have to manually\ncopy it to the following path:\n\n%s\n", err, path)
return err
}
fmt.Fprintf(os.Stdout, "You have successfully logged in.\nIf you wish to copy your credentials to a server, they have been saved to:\n%s\n", path)
return nil
}
func checkForExistingCert() (string, bool, error) {
configPath, err := homedir.Expand(config.DefaultConfigDirs[0])
if err != nil {
return "", false, err
}
ok, err := config.FileExists(configPath)
if !ok && err == nil {
// create config directory if doesn't already exist
err = os.Mkdir(configPath, 0700)
}
if err != nil {
return "", false, err
}
path := filepath.Join(configPath, config.DefaultCredentialFile)
fileInfo, err := os.Stat(path)
if err == nil && fileInfo.Size() > 0 {
return path, true, nil
}
if err != nil && err.(*os.PathError).Err != syscall.ENOENT {
return path, false, err
}
return path, false, nil
}

View File

@@ -1,4 +1,4 @@
package main
package tunnel
import (
"github.com/cloudflare/cloudflared/tunneldns"

View File

@@ -1,4 +1,4 @@
package main
package tunnel
import (
"os"

View File

@@ -1,4 +1,4 @@
package main
package tunnel
import (
"fmt"

View File

@@ -1,4 +1,4 @@
package main
package tunnel
import (
"fmt"

View File

@@ -1,4 +1,4 @@
package main
package tunnel
import (
"testing"

View File

@@ -1,4 +1,4 @@
package main
package updater
import (
"os"
@@ -8,6 +8,7 @@ import (
"golang.org/x/crypto/ssh/terminal"
"gopkg.in/urfave/cli.v2"
"github.com/cloudflare/cloudflared/log"
"github.com/equinox-io/equinox"
"github.com/facebookgo/grace/gracenet"
)
@@ -18,13 +19,16 @@ const (
noUpdateOnWindowsMessage = "cloudflared will not automatically update on Windows systems."
)
var publicKey = []byte(`
var (
publicKey = []byte(`
-----BEGIN ECDSA PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4OWZocTVZ8Do/L6ScLdkV+9A0IYMHoOf
dsCmJ/QZ6aw0w9qkkwEpne1Lmo6+0pGexZzFZOH6w5amShn+RXt7qkSid9iWlzGq
EKx0BZogHSor9Wy5VztdFaAaVbsJiCbO
-----END ECDSA PUBLIC KEY-----
`)
logger = log.CreateLogger()
)
type ReleaseInfo struct {
Updated bool
@@ -54,14 +58,14 @@ func checkForUpdates() ReleaseInfo {
return ReleaseInfo{Updated: true, Version: resp.ReleaseVersion}
}
func update(_ *cli.Context) error {
func Update(_ *cli.Context) error {
if updateApplied() {
os.Exit(64)
}
return nil
}
func autoupdate(freq time.Duration, listeners *gracenet.Net, shutdownC chan struct{}) error {
func Autoupdate(freq time.Duration, listeners *gracenet.Net, shutdownC chan struct{}) error {
tickC := time.Tick(freq)
for {
if updateApplied() {
@@ -96,7 +100,7 @@ func updateApplied() bool {
return false
}
func isAutoupdateEnabled(c *cli.Context) bool {
func IsAutoupdateEnabled(c *cli.Context) bool {
if runtime.GOOS == "windows" {
logger.Info(noUpdateOnWindowsMessage)
return false

View File

@@ -22,6 +22,7 @@ import (
const (
windowsServiceName = "Cloudflared"
windowsServiceDescription = "Argo Tunnel agent"
windowsServiceUrl = "https://developers.cloudflare.com/argo-tunnel/reference/service/"
recoverActionDelay = time.Second * 20
failureCountResetPeriod = time.Hour * 24
@@ -163,7 +164,7 @@ func installWindowsService(c *cli.Context) error {
err = configRecoveryOption(s.Handle)
if err != nil {
logger.WithError(err).Errorf("Cannot set service recovery actions")
logger.Infof("See %s to manually configure service recovery actions", serviceUrl)
logger.Infof("See %s to manually configure service recovery actions", windowsServiceUrl)
}
return nil
}