Initial WIP refactoring logic out of main package

This commit is contained in:
Matthew Holt 2018-02-23 18:49:28 -07:00
parent 29c42adaa1
commit 6ab87d01de
6 changed files with 289 additions and 139 deletions

View File

@ -1,13 +1,7 @@
package main
import (
"crypto/tls"
"encoding/hex"
"fmt"
"io/ioutil"
"math/rand"
"net"
"net/http"
"os"
"os/signal"
"path/filepath"
@ -18,10 +12,9 @@ import (
"time"
"github.com/cloudflare/cloudflare-warp/metrics"
"github.com/cloudflare/cloudflare-warp/origin"
"github.com/cloudflare/cloudflare-warp/tlsconfig"
tunnelpogs "github.com/cloudflare/cloudflare-warp/tunnelrpc/pogs"
"github.com/cloudflare/cloudflare-warp/validation"
"github.com/cloudflare/cloudflare-warp/warp"
"github.com/facebookgo/grace/gracenet"
"github.com/getsentry/raven-go"
@ -316,8 +309,8 @@ WARNING:
func startServer(c *cli.Context) {
var wg sync.WaitGroup
errC := make(chan error)
wg.Add(2)
errC := make(chan error)
// If the user choose to supply all options through env variables,
// c.NumFlags() == 0 && c.NArg() == 0. For warp to work, the user needs to at
@ -326,6 +319,7 @@ func startServer(c *cli.Context) {
cli.ShowAppHelp(c)
return
}
logLevel, err := logrus.ParseLevel(c.String("loglevel"))
if err != nil {
Log.WithError(err).Fatal("Unknown logging level specified")
@ -353,22 +347,16 @@ func startServer(c *cli.Context) {
go autoupdate(c.Duration("autoupdate-freq"), shutdownC)
}
hostname, err := validation.ValidateHostname(c.String("hostname"))
if err != nil {
Log.WithError(err).Fatal("Invalid hostname")
}
clientID := c.String("id")
if !c.IsSet("id") {
clientID = generateRandomClientID()
}
tags, err := NewTagSliceFromCLI(c.StringSlice("tag"))
if err != nil {
Log.WithError(err).Fatal("Tag parse failure")
}
tags = append(tags, tunnelpogs.Tag{Name: "ID", Value: clientID})
validURL, err := validateUrl(c)
if err != nil {
Log.WithError(err).Fatal("Error validating url")
}
if c.IsSet("hello-world") {
wg.Add(1)
listener, err := createListener("127.0.0.1:")
@ -381,86 +369,18 @@ func startServer(c *cli.Context) {
wg.Done()
listener.Close()
}()
c.Set("url", "https://"+listener.Addr().String())
validURL = "https://" + listener.Addr().String()
}
url, err := validateUrl(c)
if err != nil {
Log.WithError(err).Fatal("Error validating url")
}
Log.Infof("Proxying tunnel requests to %s", url)
Log.Infof("Proxying tunnel requests to %s", validURL)
// Fail if the user provided an old authentication method
if c.IsSet("api-key") || c.IsSet("api-email") || c.IsSet("api-ca-key") {
Log.Fatal("You don't need to give us your api-key anymore. Please use the new log in method. Just run cloudflare-warp login")
}
// Check that the user has acquired a certificate using the log in command
originCertPath, err := homedir.Expand(c.String("origincert"))
if err != nil {
Log.WithError(err).Fatalf("Cannot resolve path %s", c.String("origincert"))
}
ok, err := fileExists(originCertPath)
if !ok {
Log.Fatalf(`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])
}
// Easier to send the certificate as []byte via RPC than decoding it at this point
originCert, err := ioutil.ReadFile(originCertPath)
if err != nil {
Log.WithError(err).Fatalf("Cannot read %s to load origin certificate", originCertPath)
}
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: tlsconfig.LoadOriginCertsPool()},
}
tunnelConfig := &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,
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: Log,
IsAutoupdated: c.Bool("is-autoupdated"),
}
connectedSignal := make(chan struct{})
go writePidFile(connectedSignal, c.String("pidfile"))
go func() {
errC <- origin.StartTunnelDaemon(tunnelConfig, shutdownC, connectedSignal)
wg.Done()
}()
metricsListener, err := listeners.Listen("tcp", c.String("metrics"))
if err != nil {
@ -471,6 +391,44 @@ If you don't have a certificate signed by Cloudflare, run the command:
wg.Done()
}()
tlsConfig := tlsconfig.CLIFlags{RootCA: "cacert"}.GetConfig(c)
// Start the server
go func() {
errC <- warp.StartServer(warp.ServerConfig{
Hostname: c.String("hostname"),
ServerURL: validURL,
HelloWorld: c.IsSet("hello-world"),
Tags: tags,
OriginCert: c.String("origincert"),
ConnectedChan: connectedSignal,
ShutdownChan: shutdownC,
Timeout: c.Duration("proxy-connect-timeout"),
KeepAlive: c.Duration("proxy-tcp-keepalive"),
DualStack: !c.Bool("proxy-no-happy-eyeballs"),
MaxIdleConns: c.Int("proxy-keepalive-connections"),
IdleConnTimeout: c.Duration("proxy-keepalive-timeout"),
TLSHandshakeTimeout: c.Duration("proxy-tls-timeout"),
EdgeAddrs: c.StringSlice("edge"),
Retries: c.Uint("retries"),
HeartbeatInterval: c.Duration("heartbeat-interval"),
MaxHeartbeats: c.Uint64("heartbeat-count"),
LBPool: c.String("lb-pool"),
HAConnections: c.Int("ha-connections"),
MetricsUpdateFreq: c.Duration("metrics-update-freq"),
IsAutoupdated: c.Bool("is-autoupdated"),
TLSConfig: tlsConfig,
ReportedVersion: Version,
ProtoLogger: protoLogger,
Logger: Log,
})
wg.Done()
}()
var errCode int
err = WaitForSignal(errC, shutdownC)
if err != nil {
@ -504,6 +462,14 @@ func WaitForSignal(errC chan error, shutdownC chan struct{}) error {
return nil
}
func login(c *cli.Context) error {
err := warp.Login(defaultConfigDir, credentialFile, c.String("url"))
if err != nil {
fmt.Println(err)
}
return err
}
func update(c *cli.Context) error {
if updateApplied() {
os.Exit(64)
@ -584,13 +550,6 @@ func findInputSourceContext(context *cli.Context) (altsrc.InputSourceContext, er
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 writePidFile(waitForSignal chan struct{}, pidFile string) {
<-waitForSignal
daemon.SdNotify(false, "READY=1")

View File

@ -93,11 +93,11 @@ func (c *TunnelConfig) RegistrationOptions(connectionID uint8, OriginLocalIP str
Version: c.ReportedVersion,
OS: fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH),
ExistingTunnelPolicy: policy,
PoolName: c.LBPool,
// PoolName: c.LBPool, // TODO - see issue #2
Tags: c.Tags,
ConnectionID: connectionID,
OriginLocalIP: OriginLocalIP,
IsAutoupdated: c.IsAutoupdated,
// IsAutoupdated: c.IsAutoupdated, // TODO - see issue #2
}
}

View File

@ -90,8 +90,10 @@ func LoadOriginCertsPool() *x509.CertPool {
return certPool
}
func CreateTunnelConfig(c *cli.Context, addrs []string) *tls.Config {
tlsConfig := CLIFlags{RootCA: "cacert"}.GetConfig(c)
func CreateTunnelConfig(tlsConfig *tls.Config, addrs []string) *tls.Config {
if tlsConfig == nil {
tlsConfig = new(tls.Config)
}
if tlsConfig.RootCAs == nil {
tlsConfig.RootCAs = GetCloudflareRootCA()
tlsConfig.ServerName = "cftunnel.com"

View File

@ -47,11 +47,11 @@ type RegistrationOptions struct {
Version string
OS string `capnp:"os"`
ExistingTunnelPolicy tunnelrpc.ExistingTunnelPolicy
PoolName string `capnp:"poolName"`
// PoolName string `capnp:"poolName"` // TODO - see issue #2
Tags []Tag
ConnectionID uint8 `capnp:"connectionId"`
OriginLocalIP string `capnp:"originLocalIp"`
IsAutoupdated bool `capnp:"isAutoupdated"`
// IsAutoupdated bool `capnp:"isAutoupdated"` // TODO - see issue #2
}
func MarshalRegistrationOptions(s tunnelrpc.RegistrationOptions, p *RegistrationOptions) error {

View File

@ -1,4 +1,4 @@
package main
package warp
import (
"crypto/rand"
@ -15,15 +15,18 @@ import (
"time"
homedir "github.com/mitchellh/go-homedir"
cli "gopkg.in/urfave/cli.v2"
)
const baseLoginURL = "https://www.cloudflare.com/a/warp"
const baseCertStoreURL = "https://login.cloudflarewarp.com"
const clientTimeout = time.Minute * 20
func login(c *cli.Context) error {
configPath, err := homedir.Expand(defaultConfigDir)
// Login obtains credentials from Cloudflare to enable
// the creation of tunnels with the Warp service.
// baseURL is the base URL from which to login to warp;
// leave empty to use default.
func Login(configDir, credentialFile, baseURL string) error {
configPath, err := homedir.Expand(configDir)
if err != nil {
return err
}
@ -38,21 +41,20 @@ func login(c *cli.Context) error {
path := filepath.Join(configPath, credentialFile)
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.
return fmt.Errorf(`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")
if baseURL == "" {
baseURL = baseCertStoreURL
}
// Generate a random post URL
// generate a random post URL
certURL := baseURL + generateRandomPath()
loginURL, err := url.Parse(baseLoginURL)
if err != nil {
@ -67,7 +69,7 @@ If this is intentional, please move or delete that file then run this command ag
%s
Leave cloudflare-warp running to install the certificate automatically.
Leave the program running to install the certificate automatically.
`, loginURL.String())
} else {
fmt.Fprintf(os.Stderr, `A browser window should have opened at the following URL:
@ -75,11 +77,10 @@ Leave cloudflare-warp running to install the certificate automatically.
%s
If the browser failed to open, open it yourself and visit the URL above.
`, loginURL.String())
}
if download(certURL, path) {
if ok, err := download(certURL, path); ok && err == nil {
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
@ -126,21 +127,24 @@ func open(url string) error {
return exec.Command(cmd, args...).Start()
}
func download(certURL, filePath string) bool {
// download downloads a certificate at certURL to filePath.
// It returns true if the certificate was successfully
// downloaded; false otherwise, with any applicable error.
// An error may be returned even if the certificate was
// downloaded successfully.
func download(certURL, filePath string) (bool, error) {
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
return true, putSuccess(client, certURL)
}
if err != nil {
Log.WithError(err).Error("Error fetching certificate")
return false
return false, fmt.Errorf("fetching certificate: %v", err)
}
}
return false
return false, nil
}
func tryDownload(client *http.Client, certURL, filePath string) (ok bool, err error) {
@ -175,20 +179,19 @@ func tryDownload(client *http.Client, certURL, filePath string) (ok bool, err er
}
}
func putSuccess(client *http.Client, certURL string) {
func putSuccess(client *http.Client, certURL string) error {
// indicate success to the relay server
req, err := http.NewRequest("PUT", certURL+"/ok", nil)
if err != nil {
Log.WithError(err).Error("HTTP request error")
return
return fmt.Errorf("HTTP request error: %v", err)
}
resp, err := client.Do(req)
if err != nil {
Log.WithError(err).Error("HTTP error")
return
return fmt.Errorf("HTTP error: %v", err)
}
resp.Body.Close()
if resp.StatusCode != 200 {
Log.Errorf("Unexpected HTTP error code %d", resp.StatusCode)
return fmt.Errorf("unexpected HTTP status code %d", resp.StatusCode)
}
return nil
}

186
warp/server.go Normal file
View File

@ -0,0 +1,186 @@
package warp
import (
"crypto/tls"
"encoding/hex"
"fmt"
"io/ioutil"
"math/rand"
"net"
"net/http"
"os"
"time"
"github.com/cloudflare/cloudflare-warp/origin"
"github.com/cloudflare/cloudflare-warp/tlsconfig"
tunnelpogs "github.com/cloudflare/cloudflare-warp/tunnelrpc/pogs"
"github.com/cloudflare/cloudflare-warp/validation"
homedir "github.com/mitchellh/go-homedir"
"github.com/sirupsen/logrus"
)
// StartServer starts a warp proxy server with the given configuration.
// It blocks indefinitely.
func StartServer(cfg ServerConfig) error {
hostname, err := validation.ValidateHostname(cfg.Hostname)
if err != nil {
return err
}
if cfg.ClientID == "" {
cfg.ClientID = generateRandomClientID()
}
cfg.Tags = append(cfg.Tags, tunnelpogs.Tag{Name: "ID", Value: cfg.ClientID})
cfg.ServerURL, err = validation.ValidateUrl(cfg.ServerURL)
if err != nil {
return fmt.Errorf("validating server URL: %v", err)
}
// Check that the user has acquired a certificate using the log in command
originCertPath, err := homedir.Expand(cfg.OriginCert)
if err != nil {
return fmt.Errorf("cannot resolve path %s: %v", cfg.OriginCert, err)
}
ok, err := fileExists(originCertPath)
if !ok {
return fmt.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]) // TODO - we need to improve how this is handled
}
// Easier to send the certificate as []byte via RPC than decoding it at this point
originCert, err := ioutil.ReadFile(originCertPath)
if err != nil {
return fmt.Errorf("cannot read %s to load origin certificate: %v", originCertPath, err)
}
tunnelMetrics := origin.NewTunnelMetrics()
httpTransport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: cfg.Timeout,
KeepAlive: cfg.KeepAlive,
DualStack: cfg.DualStack,
}).DialContext,
MaxIdleConns: cfg.MaxIdleConns,
IdleConnTimeout: cfg.IdleConnTimeout,
TLSHandshakeTimeout: cfg.TLSHandshakeTimeout,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{RootCAs: tlsconfig.LoadOriginCertsPool()},
}
tunnelConfig := &origin.TunnelConfig{
EdgeAddrs: cfg.EdgeAddrs,
OriginUrl: cfg.ServerURL,
Hostname: hostname,
OriginCert: originCert,
TlsConfig: tlsconfig.CreateTunnelConfig(cfg.TLSConfig, cfg.EdgeAddrs),
ClientTlsConfig: httpTransport.TLSClientConfig,
Retries: cfg.Retries,
HeartbeatInterval: cfg.HeartbeatInterval,
MaxHeartbeats: cfg.MaxHeartbeats,
ClientID: cfg.ClientID,
ReportedVersion: cfg.ReportedVersion,
LBPool: cfg.LBPool,
Tags: cfg.Tags,
HAConnections: cfg.HAConnections,
HTTPTransport: httpTransport,
Metrics: tunnelMetrics,
MetricsUpdateFreq: cfg.MetricsUpdateFreq,
ProtocolLogger: cfg.ProtoLogger,
Logger: cfg.Logger,
IsAutoupdated: cfg.IsAutoupdated,
}
// blocking
return origin.StartTunnelDaemon(tunnelConfig, cfg.ShutdownChan, cfg.ConnectedChan)
}
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
}
func generateRandomClientID() string {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
id := make([]byte, 32)
r.Read(id)
return hex.EncodeToString(id)
}
// ServerConfig specifies a warp proxy-server configuration.
type ServerConfig struct {
// The hostname on a Cloudflare zone with which route
// traffic through this tunnel.
// Required.
Hostname string
// The URL of the local web server. If empty (if there
// is no server), set HelloWorld to true for a demo.
// Required.
ServerURL string
// If true, use the established tunnel to expose a
// test HTTP server. If false, ServerURL must be set.
HelloWorld bool
// The tunnel ID; leave blank to use a random ID.
ClientID string
// Custom tags to identify this tunnel
Tags []tunnelpogs.Tag
// Specifies the Warp certificate for one of your zones,
// authorizing the client to serve as an origin for that zone.
// A certificate is required to use Warp. You can obtain a
// certificate by using the login command or by visiting
// https://www.cloudflare.com/a/warp.
OriginCert string
// The channel to close when the tunnel is connected.
ConnectedChan chan struct{}
// The channel to close when shutting down.
ShutdownChan chan struct{}
Timeout time.Duration // proxy-connect-timeout
KeepAlive time.Duration // proxy-tcp-keepalive
DualStack bool // proxy-no-happy-eyeballs
MaxIdleConns int // proxy-keepalive-connections
IdleConnTimeout time.Duration // proxy-keepalive-timeout
TLSHandshakeTimeout time.Duration // proxy-tls-timeout
EdgeAddrs []string // edge
Retries uint // retries
HeartbeatInterval time.Duration // heartbeat-interval
MaxHeartbeats uint64 // heartbeat-count
LBPool string // lb-pool
HAConnections int // ha-connections
MetricsUpdateFreq time.Duration // metrics-update-freq
IsAutoupdated bool // is-autoupdated
// The TLS client config used when making the tunnel.
TLSConfig *tls.Config
// The version of the client to report
ReportedVersion string
ProtoLogger *logrus.Logger
Logger *logrus.Logger
}