321 lines
11 KiB
Go
321 lines
11 KiB
Go
|
package main
|
||
|
|
||
|
import (
|
||
|
"crypto/tls"
|
||
|
"crypto/x509"
|
||
|
"encoding/hex"
|
||
|
"fmt"
|
||
|
"io/ioutil"
|
||
|
"math/rand"
|
||
|
"net"
|
||
|
"net/http"
|
||
|
"os"
|
||
|
"path/filepath"
|
||
|
"runtime"
|
||
|
"strings"
|
||
|
"time"
|
||
|
|
||
|
"github.com/cloudflare/cloudflared/origin"
|
||
|
"github.com/cloudflare/cloudflared/tlsconfig"
|
||
|
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||
|
"github.com/cloudflare/cloudflared/validation"
|
||
|
|
||
|
"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"}
|
||
|
)
|
||
|
|
||
|
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
|
||
|
func findDefaultOriginCertPath() string {
|
||
|
for _, defaultConfigDir := range defaultConfigDirs {
|
||
|
originCertPath, _ := homedir.Expand(filepath.Join(defaultConfigDir, defaultCredentialFile))
|
||
|
if ok, _ := 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)
|
||
|
r.Read(id)
|
||
|
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") {
|
||
|
logger.Error("You don't need to give us your api-key anymore. Please use the new login method. Just run cloudflared login")
|
||
|
return fmt.Errorf("Client provided deprecated options")
|
||
|
}
|
||
|
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("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
|
||
|
}
|
||
|
|
||
|
func logClientOptions(c *cli.Context) {
|
||
|
flags := make(map[string]interface{})
|
||
|
for _, flag := range c.LocalFlagNames() {
|
||
|
flags[flag] = c.Generic(flag)
|
||
|
}
|
||
|
if len(flags) > 0 {
|
||
|
logger.Infof("Flags %v", flags)
|
||
|
}
|
||
|
|
||
|
envs := make(map[string]string)
|
||
|
// Find env variables for Argo Tunnel
|
||
|
for _, env := range os.Environ() {
|
||
|
// All Argo Tunnel env variables start with TUNNEL_
|
||
|
if strings.Contains(env, "TUNNEL_") {
|
||
|
vars := strings.Split(env, "=")
|
||
|
if len(vars) == 2 {
|
||
|
envs[vars[0]] = vars[1]
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if len(envs) > 0 {
|
||
|
logger.Infof("Environmental variables %v", envs)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func dnsProxyStandAlone(c *cli.Context) bool {
|
||
|
return c.IsSet("proxy-dns") && (!c.IsSet("hostname") && !c.IsSet("tag") && !c.IsSet("hello-world"))
|
||
|
}
|
||
|
|
||
|
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)
|
||
|
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")
|
||
|
} else {
|
||
|
logger.Errorf("You need to specify the origin certificate path by specifying the origincert option in the configuration file, or set TUNNEL_ORIGIN_CERT environment variable. See %s for more information.", serviceUrl)
|
||
|
return nil, fmt.Errorf("Client didn't specify origincert path")
|
||
|
}
|
||
|
}
|
||
|
// Check that the user has acquired a certificate using the login command
|
||
|
originCertPath, err := homedir.Expand(c.String("origincert"))
|
||
|
if err != nil {
|
||
|
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)
|
||
|
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"))
|
||
|
}
|
||
|
if !ok {
|
||
|
logger.Errorf(`Cannot find a valid certificate for your origin at the path:
|
||
|
|
||
|
%s
|
||
|
|
||
|
If the path above is wrong, specify the path with the -origincert option.
|
||
|
If you don't have a certificate signed by Cloudflare, run the command:
|
||
|
|
||
|
%s login
|
||
|
`, originCertPath, os.Args[0])
|
||
|
return nil, fmt.Errorf("Cannot find a valid certificate at the path %s", originCertPath)
|
||
|
}
|
||
|
// Easier to send the certificate as []byte via RPC than decoding it at this point
|
||
|
originCert, err := ioutil.ReadFile(originCertPath)
|
||
|
if err != nil {
|
||
|
logger.WithError(err).Errorf("Cannot read %s to load origin certificate", originCertPath)
|
||
|
return nil, fmt.Errorf("Cannot read %s to load origin certificate", originCertPath)
|
||
|
}
|
||
|
return originCert, nil
|
||
|
}
|
||
|
|
||
|
func prepareTunnelConfig(c *cli.Context, buildInfo *origin.BuildInfo, logger, protoLogger *logrus.Logger) (*origin.TunnelConfig, error) {
|
||
|
hostname, err := validation.ValidateHostname(c.String("hostname"))
|
||
|
if err != nil {
|
||
|
logger.WithError(err).Error("Invalid hostname")
|
||
|
return nil, errors.Wrap(err, "Invalid hostname")
|
||
|
}
|
||
|
clientID := c.String("id")
|
||
|
if !c.IsSet("id") {
|
||
|
clientID = generateRandomClientID()
|
||
|
}
|
||
|
|
||
|
tags, err := NewTagSliceFromCLI(c.StringSlice("tag"))
|
||
|
if err != nil {
|
||
|
logger.WithError(err).Error("Tag parse failure")
|
||
|
return nil, errors.Wrap(err, "Tag parse failure")
|
||
|
}
|
||
|
|
||
|
tags = append(tags, tunnelpogs.Tag{Name: "ID", Value: clientID})
|
||
|
|
||
|
url, err := validateUrl(c)
|
||
|
if err != nil {
|
||
|
logger.WithError(err).Error("Error validating url")
|
||
|
return nil, errors.Wrap(err, "Error validating url")
|
||
|
}
|
||
|
logger.Infof("Proxying tunnel requests to %s", url)
|
||
|
|
||
|
originCert, err := getOriginCert(c)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "Error getting origin cert")
|
||
|
}
|
||
|
|
||
|
originCertPool, err := loadCertPool(c, logger)
|
||
|
if err != nil {
|
||
|
logger.WithError(err).Error("Error loading cert pool")
|
||
|
return nil, errors.Wrap(err, "Error loading cert pool")
|
||
|
}
|
||
|
|
||
|
tunnelMetrics := origin.NewTunnelMetrics()
|
||
|
httpTransport := &http.Transport{
|
||
|
Proxy: http.ProxyFromEnvironment,
|
||
|
DialContext: (&net.Dialer{
|
||
|
Timeout: c.Duration("proxy-connect-timeout"),
|
||
|
KeepAlive: c.Duration("proxy-tcp-keepalive"),
|
||
|
DualStack: !c.Bool("proxy-no-happy-eyeballs"),
|
||
|
}).DialContext,
|
||
|
MaxIdleConns: c.Int("proxy-keepalive-connections"),
|
||
|
IdleConnTimeout: c.Duration("proxy-keepalive-timeout"),
|
||
|
TLSHandshakeTimeout: c.Duration("proxy-tls-timeout"),
|
||
|
ExpectContinueTimeout: 1 * time.Second,
|
||
|
TLSClientConfig: &tls.Config{RootCAs: originCertPool, InsecureSkipVerify: c.IsSet("no-tls-verify")},
|
||
|
}
|
||
|
|
||
|
if !c.IsSet("hello-world") && c.IsSet("origin-server-name") {
|
||
|
httpTransport.TLSClientConfig.ServerName = c.String("origin-server-name")
|
||
|
}
|
||
|
|
||
|
return &origin.TunnelConfig{
|
||
|
EdgeAddrs: c.StringSlice("edge"),
|
||
|
OriginUrl: url,
|
||
|
Hostname: hostname,
|
||
|
OriginCert: originCert,
|
||
|
TlsConfig: tlsconfig.CreateTunnelConfig(c, c.StringSlice("edge")),
|
||
|
ClientTlsConfig: httpTransport.TLSClientConfig,
|
||
|
Retries: c.Uint("retries"),
|
||
|
HeartbeatInterval: c.Duration("heartbeat-interval"),
|
||
|
MaxHeartbeats: c.Uint64("heartbeat-count"),
|
||
|
ClientID: clientID,
|
||
|
BuildInfo: buildInfo,
|
||
|
ReportedVersion: Version,
|
||
|
LBPool: c.String("lb-pool"),
|
||
|
Tags: tags,
|
||
|
HAConnections: c.Int("ha-connections"),
|
||
|
HTTPTransport: httpTransport,
|
||
|
Metrics: tunnelMetrics,
|
||
|
MetricsUpdateFreq: c.Duration("metrics-update-freq"),
|
||
|
ProtocolLogger: protoLogger,
|
||
|
Logger: logger,
|
||
|
IsAutoupdated: c.Bool("is-autoupdated"),
|
||
|
GracePeriod: c.Duration("grace-period"),
|
||
|
RunFromTerminal: isRunningFromTerminal(),
|
||
|
NoChunkedEncoding: c.Bool("no-chunked-encoding"),
|
||
|
CompressionQuality: c.Uint64("compression-quality"),
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
func loadCertPool(c *cli.Context, logger *logrus.Logger) (*x509.CertPool, error) {
|
||
|
const originCAPoolFlag = "origin-ca-pool"
|
||
|
originCAPoolFilename := c.String(originCAPoolFlag)
|
||
|
var originCustomCAPool []byte
|
||
|
|
||
|
if originCAPoolFilename != "" {
|
||
|
var err error
|
||
|
originCustomCAPool, err = ioutil.ReadFile(originCAPoolFilename)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, fmt.Sprintf("unable to read the file %s for --%s", originCAPoolFilename, originCAPoolFlag))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
originCertPool, err := tlsconfig.LoadOriginCertPool(originCustomCAPool)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "error loading the certificate pool")
|
||
|
}
|
||
|
|
||
|
// Windows users should be notified that they can use the flag
|
||
|
if runtime.GOOS == "windows" && originCAPoolFilename == "" {
|
||
|
logger.Infof("cloudflared does not support loading the system root certificate pool on Windows. Please use the --%s to specify it", originCAPoolFlag)
|
||
|
}
|
||
|
|
||
|
return originCertPool, nil
|
||
|
}
|