TUN-5869: Add configuration endpoint in metrics server

This commit is contained in:
Devin Carr 2022-03-14 10:51:10 -07:00
parent 6eeaf4be4b
commit e2a8302bbc
13 changed files with 275 additions and 96 deletions

View File

@ -73,7 +73,7 @@ func Run(c *cli.Context) error {
log.Fatal().Err(err).Msg("Failed to open the metrics listener") log.Fatal().Err(err).Msg("Failed to open the metrics listener")
} }
go metrics.ServeMetrics(metricsListener, nil, nil, "", log) go metrics.ServeMetrics(metricsListener, nil, nil, "", nil, log)
listener, err := tunneldns.CreateListener( listener, err := tunneldns.CreateListener(
c.String("address"), c.String("address"),

View File

@ -340,6 +340,11 @@ func StartServer(
return err return err
} }
orchestrator, err := orchestration.NewOrchestrator(ctx, dynamicConfig, tunnelConfig.Tags, tunnelConfig.Log)
if err != nil {
return err
}
metricsListener, err := listeners.Listen("tcp", c.String("metrics")) metricsListener, err := listeners.Listen("tcp", c.String("metrics"))
if err != nil { if err != nil {
log.Err(err).Msg("Error opening metrics server listener") log.Err(err).Msg("Error opening metrics server listener")
@ -351,14 +356,9 @@ func StartServer(
defer wg.Done() defer wg.Done()
readinessServer := metrics.NewReadyServer(log) readinessServer := metrics.NewReadyServer(log)
observer.RegisterSink(readinessServer) observer.RegisterSink(readinessServer)
errC <- metrics.ServeMetrics(metricsListener, ctx.Done(), readinessServer, quickTunnelURL, log) errC <- metrics.ServeMetrics(metricsListener, ctx.Done(), readinessServer, quickTunnelURL, orchestrator, log)
}() }()
orchestrator, err := orchestration.NewOrchestrator(ctx, dynamicConfig, tunnelConfig.Tags, tunnelConfig.Log)
if err != nil {
return err
}
reconnectCh := make(chan supervisor.ReconnectSignal, 1) reconnectCh := make(chan supervisor.ReconnectSignal, 1)
if c.IsSet("stdin-control") { if c.IsSet("stdin-control") {
log.Info().Msg("Enabling control through stdin") log.Info().Msg("Enabling control through stdin")

View File

@ -411,7 +411,7 @@ type CustomDuration struct {
time.Duration time.Duration
} }
func (s *CustomDuration) MarshalJSON() ([]byte, error) { func (s CustomDuration) MarshalJSON() ([]byte, error) {
return json.Marshal(s.Duration.Seconds()) return json.Marshal(s.Duration.Seconds())
} }

View File

@ -11,14 +11,16 @@ import (
"github.com/cloudflare/cloudflared/tlsconfig" "github.com/cloudflare/cloudflared/tlsconfig"
) )
const ( var (
defaultConnectTimeout = 30 * time.Second defaultConnectTimeout = config.CustomDuration{Duration: 30 * time.Second}
defaultTLSTimeout = 10 * time.Second defaultTLSTimeout = config.CustomDuration{Duration: 10 * time.Second}
defaultTCPKeepAlive = 30 * time.Second defaultTCPKeepAlive = config.CustomDuration{Duration: 30 * time.Second}
defaultKeepAliveConnections = 100 defaultKeepAliveTimeout = config.CustomDuration{Duration: 90 * time.Second}
defaultKeepAliveTimeout = 90 * time.Second )
defaultProxyAddress = "127.0.0.1"
const (
defaultProxyAddress = "127.0.0.1"
defaultKeepAliveConnections = 100
SSHServerFlag = "ssh-server" SSHServerFlag = "ssh-server"
Socks5Flag = "socks5" Socks5Flag = "socks5"
ProxyConnectTimeoutFlag = "proxy-connect-timeout" ProxyConnectTimeoutFlag = "proxy-connect-timeout"
@ -68,12 +70,12 @@ func (rc *RemoteConfig) UnmarshalJSON(b []byte) error {
} }
func originRequestFromSingeRule(c *cli.Context) OriginRequestConfig { func originRequestFromSingeRule(c *cli.Context) OriginRequestConfig {
var connectTimeout time.Duration = defaultConnectTimeout var connectTimeout config.CustomDuration = defaultConnectTimeout
var tlsTimeout time.Duration = defaultTLSTimeout var tlsTimeout config.CustomDuration = defaultTLSTimeout
var tcpKeepAlive time.Duration = defaultTCPKeepAlive var tcpKeepAlive config.CustomDuration = defaultTCPKeepAlive
var noHappyEyeballs bool var noHappyEyeballs bool
var keepAliveConnections int = defaultKeepAliveConnections var keepAliveConnections int = defaultKeepAliveConnections
var keepAliveTimeout time.Duration = defaultKeepAliveTimeout var keepAliveTimeout config.CustomDuration = defaultKeepAliveTimeout
var httpHostHeader string var httpHostHeader string
var originServerName string var originServerName string
var caPool string var caPool string
@ -84,13 +86,13 @@ func originRequestFromSingeRule(c *cli.Context) OriginRequestConfig {
var proxyPort uint var proxyPort uint
var proxyType string var proxyType string
if flag := ProxyConnectTimeoutFlag; c.IsSet(flag) { if flag := ProxyConnectTimeoutFlag; c.IsSet(flag) {
connectTimeout = c.Duration(flag) connectTimeout = config.CustomDuration{Duration: c.Duration(flag)}
} }
if flag := ProxyTLSTimeoutFlag; c.IsSet(flag) { if flag := ProxyTLSTimeoutFlag; c.IsSet(flag) {
tlsTimeout = c.Duration(flag) tlsTimeout = config.CustomDuration{Duration: c.Duration(flag)}
} }
if flag := ProxyTCPKeepAliveFlag; c.IsSet(flag) { if flag := ProxyTCPKeepAliveFlag; c.IsSet(flag) {
tcpKeepAlive = c.Duration(flag) tcpKeepAlive = config.CustomDuration{Duration: c.Duration(flag)}
} }
if flag := ProxyNoHappyEyeballsFlag; c.IsSet(flag) { if flag := ProxyNoHappyEyeballsFlag; c.IsSet(flag) {
noHappyEyeballs = c.Bool(flag) noHappyEyeballs = c.Bool(flag)
@ -99,7 +101,7 @@ func originRequestFromSingeRule(c *cli.Context) OriginRequestConfig {
keepAliveConnections = c.Int(flag) keepAliveConnections = c.Int(flag)
} }
if flag := ProxyKeepAliveTimeoutFlag; c.IsSet(flag) { if flag := ProxyKeepAliveTimeoutFlag; c.IsSet(flag) {
keepAliveTimeout = c.Duration(flag) keepAliveTimeout = config.CustomDuration{Duration: c.Duration(flag)}
} }
if flag := HTTPHostHeaderFlag; c.IsSet(flag) { if flag := HTTPHostHeaderFlag; c.IsSet(flag) {
httpHostHeader = c.String(flag) httpHostHeader = c.String(flag)
@ -158,13 +160,13 @@ func originRequestFromConfig(c config.OriginRequestConfig) OriginRequestConfig {
ProxyAddress: defaultProxyAddress, ProxyAddress: defaultProxyAddress,
} }
if c.ConnectTimeout != nil { if c.ConnectTimeout != nil {
out.ConnectTimeout = c.ConnectTimeout.Duration out.ConnectTimeout = *c.ConnectTimeout
} }
if c.TLSTimeout != nil { if c.TLSTimeout != nil {
out.TLSTimeout = c.TLSTimeout.Duration out.TLSTimeout = *c.TLSTimeout
} }
if c.TCPKeepAlive != nil { if c.TCPKeepAlive != nil {
out.TCPKeepAlive = c.TCPKeepAlive.Duration out.TCPKeepAlive = *c.TCPKeepAlive
} }
if c.NoHappyEyeballs != nil { if c.NoHappyEyeballs != nil {
out.NoHappyEyeballs = *c.NoHappyEyeballs out.NoHappyEyeballs = *c.NoHappyEyeballs
@ -173,7 +175,7 @@ func originRequestFromConfig(c config.OriginRequestConfig) OriginRequestConfig {
out.KeepAliveConnections = *c.KeepAliveConnections out.KeepAliveConnections = *c.KeepAliveConnections
} }
if c.KeepAliveTimeout != nil { if c.KeepAliveTimeout != nil {
out.KeepAliveTimeout = c.KeepAliveTimeout.Duration out.KeepAliveTimeout = *c.KeepAliveTimeout
} }
if c.HTTPHostHeader != nil { if c.HTTPHostHeader != nil {
out.HTTPHostHeader = *c.HTTPHostHeader out.HTTPHostHeader = *c.HTTPHostHeader
@ -218,52 +220,52 @@ func originRequestFromConfig(c config.OriginRequestConfig) OriginRequestConfig {
// Note: To specify a time.Duration in go-yaml, use e.g. "3s" or "24h". // Note: To specify a time.Duration in go-yaml, use e.g. "3s" or "24h".
type OriginRequestConfig struct { type OriginRequestConfig struct {
// HTTP proxy timeout for establishing a new connection // HTTP proxy timeout for establishing a new connection
ConnectTimeout time.Duration `yaml:"connectTimeout"` ConnectTimeout config.CustomDuration `yaml:"connectTimeout" json:"connectTimeout"`
// HTTP proxy timeout for completing a TLS handshake // HTTP proxy timeout for completing a TLS handshake
TLSTimeout time.Duration `yaml:"tlsTimeout"` TLSTimeout config.CustomDuration `yaml:"tlsTimeout" json:"tlsTimeout"`
// HTTP proxy TCP keepalive duration // HTTP proxy TCP keepalive duration
TCPKeepAlive time.Duration `yaml:"tcpKeepAlive"` TCPKeepAlive config.CustomDuration `yaml:"tcpKeepAlive" json:"tcpKeepAlive"`
// HTTP proxy should disable "happy eyeballs" for IPv4/v6 fallback // HTTP proxy should disable "happy eyeballs" for IPv4/v6 fallback
NoHappyEyeballs bool `yaml:"noHappyEyeballs"` NoHappyEyeballs bool `yaml:"noHappyEyeballs" json:"noHappyEyeballs"`
// HTTP proxy timeout for closing an idle connection // HTTP proxy timeout for closing an idle connection
KeepAliveTimeout time.Duration `yaml:"keepAliveTimeout"` KeepAliveTimeout config.CustomDuration `yaml:"keepAliveTimeout" json:"keepAliveTimeout"`
// HTTP proxy maximum keepalive connection pool size // HTTP proxy maximum keepalive connection pool size
KeepAliveConnections int `yaml:"keepAliveConnections"` KeepAliveConnections int `yaml:"keepAliveConnections" json:"keepAliveConnections"`
// Sets the HTTP Host header for the local webserver. // Sets the HTTP Host header for the local webserver.
HTTPHostHeader string `yaml:"httpHostHeader"` HTTPHostHeader string `yaml:"httpHostHeader" json:"httpHostHeader"`
// Hostname on the origin server certificate. // Hostname on the origin server certificate.
OriginServerName string `yaml:"originServerName"` OriginServerName string `yaml:"originServerName" json:"originServerName"`
// Path to the CA for the certificate of your origin. // Path to the CA for the certificate of your origin.
// This option should be used only if your certificate is not signed by Cloudflare. // This option should be used only if your certificate is not signed by Cloudflare.
CAPool string `yaml:"caPool"` CAPool string `yaml:"caPool" json:"caPool"`
// Disables TLS verification of the certificate presented by your origin. // Disables TLS verification of the certificate presented by your origin.
// Will allow any certificate from the origin to be accepted. // Will allow any certificate from the origin to be accepted.
// Note: The connection from your machine to Cloudflare's Edge is still encrypted. // Note: The connection from your machine to Cloudflare's Edge is still encrypted.
NoTLSVerify bool `yaml:"noTLSVerify"` NoTLSVerify bool `yaml:"noTLSVerify" json:"noTLSVerify"`
// Disables chunked transfer encoding. // Disables chunked transfer encoding.
// Useful if you are running a WSGI server. // Useful if you are running a WSGI server.
DisableChunkedEncoding bool `yaml:"disableChunkedEncoding"` DisableChunkedEncoding bool `yaml:"disableChunkedEncoding" json:"disableChunkedEncoding"`
// Runs as jump host // Runs as jump host
BastionMode bool `yaml:"bastionMode"` BastionMode bool `yaml:"bastionMode" json:"bastionMode"`
// Listen address for the proxy. // Listen address for the proxy.
ProxyAddress string `yaml:"proxyAddress"` ProxyAddress string `yaml:"proxyAddress" json:"proxyAddress"`
// Listen port for the proxy. // Listen port for the proxy.
ProxyPort uint `yaml:"proxyPort"` ProxyPort uint `yaml:"proxyPort" json:"proxyPort"`
// What sort of proxy should be started // What sort of proxy should be started
ProxyType string `yaml:"proxyType"` ProxyType string `yaml:"proxyType" json:"proxyType"`
// IP rules for the proxy service // IP rules for the proxy service
IPRules []ipaccess.Rule `yaml:"ipRules"` IPRules []ipaccess.Rule `yaml:"ipRules" json:"ipRules"`
} }
func (defaults *OriginRequestConfig) setConnectTimeout(overrides config.OriginRequestConfig) { func (defaults *OriginRequestConfig) setConnectTimeout(overrides config.OriginRequestConfig) {
if val := overrides.ConnectTimeout; val != nil { if val := overrides.ConnectTimeout; val != nil {
defaults.ConnectTimeout = val.Duration defaults.ConnectTimeout = *val
} }
} }
func (defaults *OriginRequestConfig) setTLSTimeout(overrides config.OriginRequestConfig) { func (defaults *OriginRequestConfig) setTLSTimeout(overrides config.OriginRequestConfig) {
if val := overrides.TLSTimeout; val != nil { if val := overrides.TLSTimeout; val != nil {
defaults.TLSTimeout = val.Duration defaults.TLSTimeout = *val
} }
} }
@ -281,13 +283,13 @@ func (defaults *OriginRequestConfig) setKeepAliveConnections(overrides config.Or
func (defaults *OriginRequestConfig) setKeepAliveTimeout(overrides config.OriginRequestConfig) { func (defaults *OriginRequestConfig) setKeepAliveTimeout(overrides config.OriginRequestConfig) {
if val := overrides.KeepAliveTimeout; val != nil { if val := overrides.KeepAliveTimeout; val != nil {
defaults.KeepAliveTimeout = val.Duration defaults.KeepAliveTimeout = *val
} }
} }
func (defaults *OriginRequestConfig) setTCPKeepAlive(overrides config.OriginRequestConfig) { func (defaults *OriginRequestConfig) setTCPKeepAlive(overrides config.OriginRequestConfig) {
if val := overrides.TCPKeepAlive; val != nil { if val := overrides.TCPKeepAlive; val != nil {
defaults.TCPKeepAlive = val.Duration defaults.TCPKeepAlive = *val
} }
} }

View File

@ -65,7 +65,7 @@ func TestUnmarshalRemoteConfigOverridesGlobal(t *testing.T) {
err := json.Unmarshal(rawConfig, &remoteConfig) err := json.Unmarshal(rawConfig, &remoteConfig)
require.NoError(t, err) require.NoError(t, err)
require.True(t, remoteConfig.Ingress.Rules[0].Config.NoTLSVerify) require.True(t, remoteConfig.Ingress.Rules[0].Config.NoTLSVerify)
require.True(t, remoteConfig.Ingress.defaults.NoHappyEyeballs) require.True(t, remoteConfig.Ingress.Defaults.NoHappyEyeballs)
} }
func TestOriginRequestConfigOverrides(t *testing.T) { func TestOriginRequestConfigOverrides(t *testing.T) {
@ -74,11 +74,11 @@ func TestOriginRequestConfigOverrides(t *testing.T) {
// root-level configuration. // root-level configuration.
actual0 := ing.Rules[0].Config actual0 := ing.Rules[0].Config
expected0 := OriginRequestConfig{ expected0 := OriginRequestConfig{
ConnectTimeout: 1 * time.Minute, ConnectTimeout: config.CustomDuration{Duration: 1 * time.Minute},
TLSTimeout: 1 * time.Second, TLSTimeout: config.CustomDuration{Duration: 1 * time.Second},
TCPKeepAlive: 1 * time.Second, TCPKeepAlive: config.CustomDuration{Duration: 1 * time.Second},
NoHappyEyeballs: true, NoHappyEyeballs: true,
KeepAliveTimeout: 1 * time.Second, KeepAliveTimeout: config.CustomDuration{Duration: 1 * time.Second},
KeepAliveConnections: 1, KeepAliveConnections: 1,
HTTPHostHeader: "abc", HTTPHostHeader: "abc",
OriginServerName: "a1", OriginServerName: "a1",
@ -99,11 +99,11 @@ func TestOriginRequestConfigOverrides(t *testing.T) {
// Rule 1 overrode all the root-level config. // Rule 1 overrode all the root-level config.
actual1 := ing.Rules[1].Config actual1 := ing.Rules[1].Config
expected1 := OriginRequestConfig{ expected1 := OriginRequestConfig{
ConnectTimeout: 2 * time.Minute, ConnectTimeout: config.CustomDuration{Duration: 2 * time.Minute},
TLSTimeout: 2 * time.Second, TLSTimeout: config.CustomDuration{Duration: 2 * time.Second},
TCPKeepAlive: 2 * time.Second, TCPKeepAlive: config.CustomDuration{Duration: 2 * time.Second},
NoHappyEyeballs: false, NoHappyEyeballs: false,
KeepAliveTimeout: 2 * time.Second, KeepAliveTimeout: config.CustomDuration{Duration: 2 * time.Second},
KeepAliveConnections: 2, KeepAliveConnections: 2,
HTTPHostHeader: "def", HTTPHostHeader: "def",
OriginServerName: "b2", OriginServerName: "b2",
@ -286,11 +286,11 @@ func TestOriginRequestConfigDefaults(t *testing.T) {
// Rule 1 overrode all defaults. // Rule 1 overrode all defaults.
actual1 := ing.Rules[1].Config actual1 := ing.Rules[1].Config
expected1 := OriginRequestConfig{ expected1 := OriginRequestConfig{
ConnectTimeout: 2 * time.Minute, ConnectTimeout: config.CustomDuration{Duration: 2 * time.Minute},
TLSTimeout: 2 * time.Second, TLSTimeout: config.CustomDuration{Duration: 2 * time.Second},
TCPKeepAlive: 2 * time.Second, TCPKeepAlive: config.CustomDuration{Duration: 2 * time.Second},
NoHappyEyeballs: false, NoHappyEyeballs: false,
KeepAliveTimeout: 2 * time.Second, KeepAliveTimeout: config.CustomDuration{Duration: 2 * time.Second},
KeepAliveConnections: 2, KeepAliveConnections: 2,
HTTPHostHeader: "def", HTTPHostHeader: "def",
OriginServerName: "b2", OriginServerName: "b2",

View File

@ -64,8 +64,8 @@ func matchHost(ruleHost, reqHost string) bool {
// Ingress maps eyeball requests to origins. // Ingress maps eyeball requests to origins.
type Ingress struct { type Ingress struct {
Rules []Rule Rules []Rule `json:"ingress"`
defaults OriginRequestConfig Defaults OriginRequestConfig `json:"originRequest"`
} }
// NewSingleOrigin constructs an Ingress set with only one rule, constructed from // NewSingleOrigin constructs an Ingress set with only one rule, constructed from
@ -86,7 +86,7 @@ func NewSingleOrigin(c *cli.Context, allowURLFromArgs bool) (Ingress, error) {
Config: setConfig(defaults, config.OriginRequestConfig{}), Config: setConfig(defaults, config.OriginRequestConfig{}),
}, },
}, },
defaults: defaults, Defaults: defaults,
} }
return ing, err return ing, err
} }
@ -180,7 +180,7 @@ func validateIngress(ingress []config.UnvalidatedIngressRule, defaults OriginReq
} }
srv := newStatusCode(status) srv := newStatusCode(status)
service = &srv service = &srv
} else if r.Service == "hello_world" || r.Service == "hello-world" || r.Service == "helloworld" { } else if r.Service == HelloWorldService || r.Service == "hello-world" || r.Service == "helloworld" {
service = new(helloWorld) service = new(helloWorld)
} else if r.Service == ServiceSocksProxy { } else if r.Service == ServiceSocksProxy {
rules := make([]ipaccess.Rule, len(r.OriginRequest.IPRules)) rules := make([]ipaccess.Rule, len(r.OriginRequest.IPRules))
@ -230,23 +230,24 @@ func validateIngress(ingress []config.UnvalidatedIngressRule, defaults OriginReq
return Ingress{}, err return Ingress{}, err
} }
var pathRegex *regexp.Regexp var pathRegexp *Regexp
if r.Path != "" { if r.Path != "" {
var err error var err error
pathRegex, err = regexp.Compile(r.Path) regex, err := regexp.Compile(r.Path)
if err != nil { if err != nil {
return Ingress{}, errors.Wrapf(err, "Rule #%d has an invalid regex", i+1) return Ingress{}, errors.Wrapf(err, "Rule #%d has an invalid regex", i+1)
} }
pathRegexp = &Regexp{Regexp: regex}
} }
rules[i] = Rule{ rules[i] = Rule{
Hostname: r.Hostname, Hostname: r.Hostname,
Service: service, Service: service,
Path: pathRegex, Path: pathRegexp,
Config: cfg, Config: cfg,
} }
} }
return Ingress{Rules: rules, defaults: defaults}, nil return Ingress{Rules: rules, Defaults: defaults}, nil
} }
func validateHostname(r config.UnvalidatedIngressRule, ruleIndex, totalRules int) error { func validateHostname(r config.UnvalidatedIngressRule, ruleIndex, totalRules int) error {

View File

@ -482,12 +482,12 @@ func TestSingleOriginSetsConfig(t *testing.T) {
ingress, err := NewSingleOrigin(cliCtx, allowURLFromArgs) ingress, err := NewSingleOrigin(cliCtx, allowURLFromArgs)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, time.Second, ingress.Rules[0].Config.ConnectTimeout) assert.Equal(t, config.CustomDuration{Duration: time.Second}, ingress.Rules[0].Config.ConnectTimeout)
assert.Equal(t, time.Second, ingress.Rules[0].Config.TLSTimeout) assert.Equal(t, config.CustomDuration{Duration: time.Second}, ingress.Rules[0].Config.TLSTimeout)
assert.Equal(t, time.Second, ingress.Rules[0].Config.TCPKeepAlive) assert.Equal(t, config.CustomDuration{Duration: time.Second}, ingress.Rules[0].Config.TCPKeepAlive)
assert.True(t, ingress.Rules[0].Config.NoHappyEyeballs) assert.True(t, ingress.Rules[0].Config.NoHappyEyeballs)
assert.Equal(t, 10, ingress.Rules[0].Config.KeepAliveConnections) assert.Equal(t, 10, ingress.Rules[0].Config.KeepAliveConnections)
assert.Equal(t, time.Second, ingress.Rules[0].Config.KeepAliveTimeout) assert.Equal(t, config.CustomDuration{Duration: time.Second}, ingress.Rules[0].Config.KeepAliveTimeout)
assert.Equal(t, "example.com:8080", ingress.Rules[0].Config.HTTPHostHeader) assert.Equal(t, "example.com:8080", ingress.Rules[0].Config.HTTPHostHeader)
assert.Equal(t, "example.com", ingress.Rules[0].Config.OriginServerName) assert.Equal(t, "example.com", ingress.Rules[0].Config.OriginServerName)
assert.Equal(t, "/etc/certs/ca.pem", ingress.Rules[0].Config.CAPool) assert.Equal(t, "/etc/certs/ca.pem", ingress.Rules[0].Config.CAPool)
@ -508,7 +508,7 @@ func TestFindMatchingRule(t *testing.T) {
}, },
{ {
Hostname: "tunnel-b.example.com", Hostname: "tunnel-b.example.com",
Path: mustParsePath(t, "/health"), Path: MustParsePath(t, "/health"),
}, },
{ {
Hostname: "*", Hostname: "*",
@ -591,10 +591,10 @@ func TestIsHTTPService(t *testing.T) {
} }
} }
func mustParsePath(t *testing.T, path string) *regexp.Regexp { func MustParsePath(t *testing.T, path string) *Regexp {
regexp, err := regexp.Compile(path) regexp, err := regexp.Compile(path)
assert.NoError(t, err) assert.NoError(t, err)
return regexp return &Regexp{Regexp: regexp}
} }
func MustParseURL(t *testing.T, rawURL string) *url.URL { func MustParseURL(t *testing.T, rawURL string) *url.URL {

View File

@ -3,6 +3,7 @@ package ingress
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"encoding/json"
"fmt" "fmt"
"io" "io"
"net" "net"
@ -20,7 +21,8 @@ import (
) )
const ( const (
HelloWorldService = "Hello World test origin" HelloWorldService = "hello_world"
HttpStatusService = "http_status"
) )
// OriginService is something a tunnel can proxy traffic to. // OriginService is something a tunnel can proxy traffic to.
@ -31,6 +33,7 @@ type OriginService interface {
// starting the origin service. // starting the origin service.
// Implementor of services managed by cloudflared should terminate the service if shutdownC is closed // Implementor of services managed by cloudflared should terminate the service if shutdownC is closed
start(log *zerolog.Logger, shutdownC <-chan struct{}, cfg OriginRequestConfig) error start(log *zerolog.Logger, shutdownC <-chan struct{}, cfg OriginRequestConfig) error
MarshalJSON() ([]byte, error)
} }
// unixSocketPath is an OriginService representing a unix socket (which accepts HTTP or HTTPS) // unixSocketPath is an OriginService representing a unix socket (which accepts HTTP or HTTPS)
@ -41,7 +44,11 @@ type unixSocketPath struct {
} }
func (o *unixSocketPath) String() string { func (o *unixSocketPath) String() string {
return "unix socket: " + o.path scheme := ""
if o.scheme == "https" {
scheme = "+tls"
}
return fmt.Sprintf("unix%s:%s", scheme, o.path)
} }
func (o *unixSocketPath) start(log *zerolog.Logger, _ <-chan struct{}, cfg OriginRequestConfig) error { func (o *unixSocketPath) start(log *zerolog.Logger, _ <-chan struct{}, cfg OriginRequestConfig) error {
@ -53,6 +60,10 @@ func (o *unixSocketPath) start(log *zerolog.Logger, _ <-chan struct{}, cfg Origi
return nil return nil
} }
func (o unixSocketPath) MarshalJSON() ([]byte, error) {
return json.Marshal(o.String())
}
type httpService struct { type httpService struct {
url *url.URL url *url.URL
hostHeader string hostHeader string
@ -73,6 +84,10 @@ func (o *httpService) String() string {
return o.url.String() return o.url.String()
} }
func (o httpService) MarshalJSON() ([]byte, error) {
return json.Marshal(o.String())
}
// rawTCPService dials TCP to the destination specified by the client // rawTCPService dials TCP to the destination specified by the client
// It's used by warp routing // It's used by warp routing
type rawTCPService struct { type rawTCPService struct {
@ -87,6 +102,10 @@ func (o *rawTCPService) start(log *zerolog.Logger, _ <-chan struct{}, cfg Origin
return nil return nil
} }
func (o rawTCPService) MarshalJSON() ([]byte, error) {
return json.Marshal(o.String())
}
// tcpOverWSService models TCP origins serving eyeballs connecting over websocket, such as // tcpOverWSService models TCP origins serving eyeballs connecting over websocket, such as
// cloudflared access commands. // cloudflared access commands.
type tcpOverWSService struct { type tcpOverWSService struct {
@ -153,6 +172,10 @@ func (o *tcpOverWSService) start(log *zerolog.Logger, _ <-chan struct{}, cfg Ori
return nil return nil
} }
func (o tcpOverWSService) MarshalJSON() ([]byte, error) {
return json.Marshal(o.String())
}
func (o *socksProxyOverWSService) start(log *zerolog.Logger, _ <-chan struct{}, cfg OriginRequestConfig) error { func (o *socksProxyOverWSService) start(log *zerolog.Logger, _ <-chan struct{}, cfg OriginRequestConfig) error {
return nil return nil
} }
@ -161,6 +184,10 @@ func (o *socksProxyOverWSService) String() string {
return ServiceSocksProxy return ServiceSocksProxy
} }
func (o socksProxyOverWSService) MarshalJSON() ([]byte, error) {
return json.Marshal(o.String())
}
// HelloWorld is an OriginService for the built-in Hello World server. // HelloWorld is an OriginService for the built-in Hello World server.
// Users only use this for testing and experimenting with cloudflared. // Users only use this for testing and experimenting with cloudflared.
type helloWorld struct { type helloWorld struct {
@ -197,6 +224,10 @@ func (o *helloWorld) start(
return nil return nil
} }
func (o helloWorld) MarshalJSON() ([]byte, error) {
return json.Marshal(o.String())
}
// statusCode is an OriginService that just responds with a given HTTP status. // statusCode is an OriginService that just responds with a given HTTP status.
// Typical use-case is "user wants the catch-all rule to just respond 404". // Typical use-case is "user wants the catch-all rule to just respond 404".
type statusCode struct { type statusCode struct {
@ -213,7 +244,7 @@ func newStatusCode(status int) statusCode {
} }
func (o *statusCode) String() string { func (o *statusCode) String() string {
return fmt.Sprintf("HTTP %d", o.resp.StatusCode) return fmt.Sprintf("http_status:%d", o.resp.StatusCode)
} }
func (o *statusCode) start( func (o *statusCode) start(
@ -224,6 +255,10 @@ func (o *statusCode) start(
return nil return nil
} }
func (o statusCode) MarshalJSON() ([]byte, error) {
return json.Marshal(o.String())
}
type NopReadCloser struct{} type NopReadCloser struct{}
// Read always returns EOF to signal end of input // Read always returns EOF to signal end of input
@ -245,8 +280,8 @@ func newHTTPTransport(service OriginService, cfg OriginRequestConfig, log *zerol
Proxy: http.ProxyFromEnvironment, Proxy: http.ProxyFromEnvironment,
MaxIdleConns: cfg.KeepAliveConnections, MaxIdleConns: cfg.KeepAliveConnections,
MaxIdleConnsPerHost: cfg.KeepAliveConnections, MaxIdleConnsPerHost: cfg.KeepAliveConnections,
IdleConnTimeout: cfg.KeepAliveTimeout, IdleConnTimeout: cfg.KeepAliveTimeout.Duration,
TLSHandshakeTimeout: cfg.TLSTimeout, TLSHandshakeTimeout: cfg.TLSTimeout.Duration,
ExpectContinueTimeout: 1 * time.Second, ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{RootCAs: originCertPool, InsecureSkipVerify: cfg.NoTLSVerify}, TLSClientConfig: &tls.Config{RootCAs: originCertPool, InsecureSkipVerify: cfg.NoTLSVerify},
} }
@ -255,8 +290,8 @@ func newHTTPTransport(service OriginService, cfg OriginRequestConfig, log *zerol
} }
dialer := &net.Dialer{ dialer := &net.Dialer{
Timeout: cfg.ConnectTimeout, Timeout: cfg.ConnectTimeout.Duration,
KeepAlive: cfg.TCPKeepAlive, KeepAlive: cfg.TCPKeepAlive.Duration,
} }
if cfg.NoHappyEyeballs { if cfg.NoHappyEyeballs {
dialer.FallbackDelay = -1 // As of Golang 1.12, a negative delay disables "happy eyeballs" dialer.FallbackDelay = -1 // As of Golang 1.12, a negative delay disables "happy eyeballs"
@ -296,3 +331,7 @@ func (mos MockOriginHTTPService) String() string {
func (mos MockOriginHTTPService) start(log *zerolog.Logger, _ <-chan struct{}, cfg OriginRequestConfig) error { func (mos MockOriginHTTPService) start(log *zerolog.Logger, _ <-chan struct{}, cfg OriginRequestConfig) error {
return nil return nil
} }
func (mos MockOriginHTTPService) MarshalJSON() ([]byte, error) {
return json.Marshal(mos.String())
}

View File

@ -1,6 +1,7 @@
package ingress package ingress
import ( import (
"encoding/json"
"regexp" "regexp"
"strings" "strings"
) )
@ -9,18 +10,18 @@ import (
// service running on the given URL. // service running on the given URL.
type Rule struct { type Rule struct {
// Requests for this hostname will be proxied to this rule's service. // Requests for this hostname will be proxied to this rule's service.
Hostname string Hostname string `json:"hostname"`
// Path is an optional regex that can specify path-driven ingress rules. // Path is an optional regex that can specify path-driven ingress rules.
Path *regexp.Regexp Path *Regexp `json:"path"`
// A (probably local) address. Requests for a hostname which matches this // A (probably local) address. Requests for a hostname which matches this
// rule's hostname pattern will be proxied to the service running on this // rule's hostname pattern will be proxied to the service running on this
// address. // address.
Service OriginService Service OriginService `json:"service"`
// Configure the request cloudflared sends to this specific origin. // Configure the request cloudflared sends to this specific origin.
Config OriginRequestConfig Config OriginRequestConfig `json:"originRequest"`
} }
// MultiLineString is for outputting rules in a human-friendly way when Cloudflared // MultiLineString is for outputting rules in a human-friendly way when Cloudflared
@ -32,9 +33,9 @@ func (r Rule) MultiLineString() string {
out.WriteString(r.Hostname) out.WriteString(r.Hostname)
out.WriteRune('\n') out.WriteRune('\n')
} }
if r.Path != nil { if r.Path != nil && r.Path.Regexp != nil {
out.WriteString("\tpath: ") out.WriteString("\tpath: ")
out.WriteString(r.Path.String()) out.WriteString(r.Path.Regexp.String())
out.WriteRune('\n') out.WriteRune('\n')
} }
out.WriteString("\tservice: ") out.WriteString("\tservice: ")
@ -45,6 +46,18 @@ func (r Rule) MultiLineString() string {
// Matches checks if the rule matches a given hostname/path combination. // Matches checks if the rule matches a given hostname/path combination.
func (r *Rule) Matches(hostname, path string) bool { func (r *Rule) Matches(hostname, path string) bool {
hostMatch := r.Hostname == "" || r.Hostname == "*" || matchHost(r.Hostname, hostname) hostMatch := r.Hostname == "" || r.Hostname == "*" || matchHost(r.Hostname, hostname)
pathMatch := r.Path == nil || r.Path.MatchString(path) pathMatch := r.Path == nil || r.Path.Regexp == nil || r.Path.Regexp.MatchString(path)
return hostMatch && pathMatch return hostMatch && pathMatch
} }
// Regexp adds unmarshalling from json for regexp.Regexp
type Regexp struct {
*regexp.Regexp
}
func (r *Regexp) MarshalJSON() ([]byte, error) {
if r.Regexp == nil {
return json.Marshal(nil)
}
return json.Marshal(r.Regexp.String())
}

View File

@ -1,6 +1,7 @@
package ingress package ingress
import ( import (
"encoding/json"
"io" "io"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
@ -8,12 +9,14 @@ import (
"testing" "testing"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/cloudflare/cloudflared/config"
) )
func Test_rule_matches(t *testing.T) { func Test_rule_matches(t *testing.T) {
type fields struct { type fields struct {
Hostname string Hostname string
Path *regexp.Regexp Path *Regexp
Service OriginService Service OriginService
} }
type args struct { type args struct {
@ -99,13 +102,35 @@ func Test_rule_matches(t *testing.T) {
name: "Hostname and path", name: "Hostname and path",
fields: fields{ fields: fields{
Hostname: "*.example.com", Hostname: "*.example.com",
Path: regexp.MustCompile("/static/.*\\.html"), Path: &Regexp{Regexp: regexp.MustCompile("/static/.*\\.html")},
}, },
args: args{ args: args{
requestURL: MustParseURL(t, "https://www.example.com/static/index.html"), requestURL: MustParseURL(t, "https://www.example.com/static/index.html"),
}, },
want: true, want: true,
}, },
{
name: "Hostname and empty Regex",
fields: fields{
Hostname: "example.com",
Path: &Regexp{},
},
args: args{
requestURL: MustParseURL(t, "https://example.com/"),
},
want: true,
},
{
name: "Hostname and nil path",
fields: fields{
Hostname: "example.com",
Path: nil,
},
args: args{
requestURL: MustParseURL(t, "https://example.com/"),
},
want: true,
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -144,3 +169,52 @@ func TestStaticHTTPStatus(t *testing.T) {
sendReq() sendReq()
sendReq() sendReq()
} }
func TestMarshalJSON(t *testing.T) {
localhost8000 := MustParseURL(t, "https://localhost:8000")
defaultConfig := setConfig(originRequestFromConfig(config.OriginRequestConfig{}), config.OriginRequestConfig{})
tests := []struct {
name string
path *Regexp
expected string
want bool
}{
{
name: "Nil",
path: nil,
expected: `{"hostname":"example.com","path":null,"service":"https://localhost:8000","originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null}}`,
want: true,
},
{
name: "Nil regex",
path: &Regexp{Regexp: nil},
expected: `{"hostname":"example.com","path":null,"service":"https://localhost:8000","originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null}}`,
want: true,
},
{
name: "Empty",
path: &Regexp{Regexp: regexp.MustCompile("")},
expected: `{"hostname":"example.com","path":"","service":"https://localhost:8000","originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null}}`,
want: true,
},
{
name: "Basic",
path: &Regexp{Regexp: regexp.MustCompile("/echo")},
expected: `{"hostname":"example.com","path":"/echo","service":"https://localhost:8000","originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null}}`,
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := Rule{
Hostname: "example.com",
Service: &httpService{url: localhost8000},
Path: tt.path,
Config: defaultConfig,
}
bytes, err := json.Marshal(r)
require.NoError(t, err)
require.Equal(t, tt.expected, string(bytes))
})
}
}

View File

@ -22,7 +22,16 @@ const (
startupTime = time.Millisecond * 500 startupTime = time.Millisecond * 500
) )
func newMetricsHandler(readyServer *ReadyServer, quickTunnelHostname string) *mux.Router { type orchestrator interface {
GetConfigJSON() ([]byte, error)
}
func newMetricsHandler(
readyServer *ReadyServer,
quickTunnelHostname string,
orchestrator orchestrator,
log *zerolog.Logger,
) *mux.Router {
router := mux.NewRouter() router := mux.NewRouter()
router.PathPrefix("/debug/").Handler(http.DefaultServeMux) router.PathPrefix("/debug/").Handler(http.DefaultServeMux)
@ -36,6 +45,18 @@ func newMetricsHandler(readyServer *ReadyServer, quickTunnelHostname string) *mu
router.HandleFunc("/quicktunnel", func(w http.ResponseWriter, r *http.Request) { router.HandleFunc("/quicktunnel", func(w http.ResponseWriter, r *http.Request) {
_, _ = fmt.Fprintf(w, `{"hostname":"%s"}`, quickTunnelHostname) _, _ = fmt.Fprintf(w, `{"hostname":"%s"}`, quickTunnelHostname)
}) })
if orchestrator != nil {
router.HandleFunc("/config", func(w http.ResponseWriter, r *http.Request) {
json, err := orchestrator.GetConfigJSON()
if err != nil {
w.WriteHeader(500)
_, _ = fmt.Fprintf(w, "ERR: %v", err)
log.Err(err).Msg("Failed to serve config")
return
}
_, _ = w.Write(json)
})
}
return router return router
} }
@ -45,6 +66,7 @@ func ServeMetrics(
shutdownC <-chan struct{}, shutdownC <-chan struct{},
readyServer *ReadyServer, readyServer *ReadyServer,
quickTunnelHostname string, quickTunnelHostname string,
orchestrator orchestrator,
log *zerolog.Logger, log *zerolog.Logger,
) (err error) { ) (err error) {
var wg sync.WaitGroup var wg sync.WaitGroup
@ -52,7 +74,7 @@ func ServeMetrics(
trace.AuthRequest = func(*http.Request) (bool, bool) { return true, true } trace.AuthRequest = func(*http.Request) (bool, bool) { return true, true }
// TODO: parameterize ReadTimeout and WriteTimeout. The maximum time we can // TODO: parameterize ReadTimeout and WriteTimeout. The maximum time we can
// profile CPU usage depends on WriteTimeout // profile CPU usage depends on WriteTimeout
h := newMetricsHandler(readyServer, quickTunnelHostname) h := newMetricsHandler(readyServer, quickTunnelHostname, orchestrator, log)
server := &http.Server{ server := &http.Server{
ReadTimeout: 10 * time.Second, ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second,

View File

@ -10,6 +10,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/connection" "github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/ingress" "github.com/cloudflare/cloudflared/ingress"
"github.com/cloudflare/cloudflared/proxy" "github.com/cloudflare/cloudflared/proxy"
@ -130,6 +131,32 @@ func (o *Orchestrator) updateIngress(ingressRules ingress.Ingress, warpRoutingEn
return nil return nil
} }
// GetConfigJSON returns the current version and configuration as JSON
func (o *Orchestrator) GetConfigJSON() ([]byte, error) {
o.lock.RLock()
defer o.lock.RUnlock()
var currentConfiguration = struct {
Version int32 `json:"version"`
Config struct {
Ingress []ingress.Rule `json:"ingress"`
WarpRouting config.WarpRoutingConfig `json:"warp-routing"`
OriginRequest ingress.OriginRequestConfig `json:"originRequest"`
} `json:"config"`
}{
Version: o.currentVersion,
Config: struct {
Ingress []ingress.Rule `json:"ingress"`
WarpRouting config.WarpRoutingConfig `json:"warp-routing"`
OriginRequest ingress.OriginRequestConfig `json:"originRequest"`
}{
Ingress: o.config.Ingress.Rules,
WarpRouting: config.WarpRoutingConfig{Enabled: o.config.WarpRoutingEnabled},
OriginRequest: o.config.Ingress.Defaults,
},
}
return json.Marshal(currentConfiguration)
}
// GetOriginProxy returns an interface to proxy to origin. It satisfies connection.ConfigManager interface // GetOriginProxy returns an interface to proxy to origin. It satisfies connection.ConfigManager interface
func (o *Orchestrator) GetOriginProxy() (connection.OriginProxy, error) { func (o *Orchestrator) GetOriginProxy() (connection.OriginProxy, error) {
val := o.proxy.Load() val := o.proxy.Load()

View File

@ -17,6 +17,7 @@ import (
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/connection" "github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/ingress" "github.com/cloudflare/cloudflared/ingress"
"github.com/cloudflare/cloudflared/proxy" "github.com/cloudflare/cloudflared/proxy"
@ -99,7 +100,7 @@ func TestUpdateConfiguration(t *testing.T) {
require.Equal(t, "http://192.16.19.1:443", configV2.Ingress.Rules[0].Service.String()) require.Equal(t, "http://192.16.19.1:443", configV2.Ingress.Rules[0].Service.String())
require.Len(t, configV2.Ingress.Rules, 3) require.Len(t, configV2.Ingress.Rules, 3)
// originRequest of this ingress rule overrides global default // originRequest of this ingress rule overrides global default
require.Equal(t, time.Second*10, configV2.Ingress.Rules[0].Config.ConnectTimeout) require.Equal(t, config.CustomDuration{Duration: time.Second * 10}, configV2.Ingress.Rules[0].Config.ConnectTimeout)
require.Equal(t, true, configV2.Ingress.Rules[0].Config.NoTLSVerify) require.Equal(t, true, configV2.Ingress.Rules[0].Config.NoTLSVerify)
// Inherited from global default // Inherited from global default
require.Equal(t, true, configV2.Ingress.Rules[0].Config.NoHappyEyeballs) require.Equal(t, true, configV2.Ingress.Rules[0].Config.NoHappyEyeballs)
@ -108,14 +109,14 @@ func TestUpdateConfiguration(t *testing.T) {
require.True(t, configV2.Ingress.Rules[1].Matches("jira.tunnel.org", "/users")) require.True(t, configV2.Ingress.Rules[1].Matches("jira.tunnel.org", "/users"))
require.Equal(t, "http://172.32.20.6:80", configV2.Ingress.Rules[1].Service.String()) require.Equal(t, "http://172.32.20.6:80", configV2.Ingress.Rules[1].Service.String())
// originRequest of this ingress rule overrides global default // originRequest of this ingress rule overrides global default
require.Equal(t, time.Second*30, configV2.Ingress.Rules[1].Config.ConnectTimeout) require.Equal(t, config.CustomDuration{Duration: time.Second * 30}, configV2.Ingress.Rules[1].Config.ConnectTimeout)
require.Equal(t, true, configV2.Ingress.Rules[1].Config.NoTLSVerify) require.Equal(t, true, configV2.Ingress.Rules[1].Config.NoTLSVerify)
// Inherited from global default // Inherited from global default
require.Equal(t, true, configV2.Ingress.Rules[1].Config.NoHappyEyeballs) require.Equal(t, true, configV2.Ingress.Rules[1].Config.NoHappyEyeballs)
// Validate ingress rule 2, it's the catch-all rule // Validate ingress rule 2, it's the catch-all rule
require.True(t, configV2.Ingress.Rules[2].Matches("blogs.tunnel.io", "/2022/02/10")) require.True(t, configV2.Ingress.Rules[2].Matches("blogs.tunnel.io", "/2022/02/10"))
// Inherited from global default // Inherited from global default
require.Equal(t, time.Second*90, configV2.Ingress.Rules[2].Config.ConnectTimeout) require.Equal(t, config.CustomDuration{Duration: time.Second * 90}, configV2.Ingress.Rules[2].Config.ConnectTimeout)
require.Equal(t, false, configV2.Ingress.Rules[2].Config.NoTLSVerify) require.Equal(t, false, configV2.Ingress.Rules[2].Config.NoTLSVerify)
require.Equal(t, true, configV2.Ingress.Rules[2].Config.NoHappyEyeballs) require.Equal(t, true, configV2.Ingress.Rules[2].Config.NoHappyEyeballs)
require.True(t, configV2.WarpRoutingEnabled) require.True(t, configV2.WarpRoutingEnabled)