Compare commits

...

7 Commits

29 changed files with 525 additions and 130 deletions
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
set -euo pipefail
if ! VERSION="$(git describe --tags --exact-match 2>/dev/null)" ; then
echo "Skipping public release for an untagged commit."
echo "##teamcity[buildStatus status='SUCCESS' text='Skipped due to lack of tag']"
exit 0
fi
if [[ "${HOMEBREW_GITHUB_API_TOKEN:-}" == "" ]] ; then
echo "Missing GITHUB_API_TOKEN"
exit 1
fi
# "install" Homebrew
git clone https://github.com/Homebrew/brew tmp/homebrew
eval "$(tmp/homebrew/bin/brew shellenv)"
brew update --force --quiet
chmod -R go-w "$(brew --prefix)/share/zsh"
# bump formula pr
brew bump-formula-pr cloudflared
+11 -1
View File
@@ -1,7 +1,17 @@
## 2022.3.4
### New Features
- It is now possible to retrieve the credentials that allow to run a Tunnel in case you forgot/lost them. This is
achievable with: `cloudflared tunnel token --cred-file /path/to/file.json TUNNEL`. This new feature only works for
Tunnels created with cloudflared version 2022.3.0 or more recent.
### Bug Fixes
- `cloudflared service install` now starts the underlying agent service on Linux operating system (similarly to the
behaviour in Windows and MacOS).
## 2022.3.3
### Bug Fixes
- `cloudflared service install` now starts the underlying agent service on Windows operating system (similarly to the
behaviour in Linux and MacOS).
behaviour in MacOS).
## 2022.3.1
### Bug Fixes
+8
View File
@@ -1,3 +1,11 @@
2022.3.4
- 2022-03-22 TUN-5918: Clean up text in cloudflared tunnel --help
- 2022-03-22 TUN-5895 run brew bump-formula-pr on release
- 2022-03-22 TUN-5915: New cloudflared command to allow to retrieve the token credentials for a Tunnel
- 2022-03-24 TUN-5933: Better messaging to help user when installing service if it is already installed
- 2022-03-25 TUN-5954: Start cloudflared service in Linux too similarly to other OSs
- 2022-03-14 TUN-5869: Add configuration endpoint in metrics server
2022.3.3
- 2022-03-17 TUN-5893: Start windows service on install, stop on uninstall. Previously user had to manually start the service after running 'cloudflared tunnel install' and stop the service before running uninstall command.
- 2022-03-17 Revert "CC-796: Remove dependency on unsupported version of go-oidc"
+1
View File
@@ -7,6 +7,7 @@ import (
type TunnelClient interface {
CreateTunnel(name string, tunnelSecret []byte) (*TunnelWithToken, error)
GetTunnel(tunnelID uuid.UUID) (*Tunnel, error)
GetTunnelToken(tunnelID uuid.UUID) (string, error)
DeleteTunnel(tunnelID uuid.UUID) error
ListTunnels(filter *TunnelFilter) ([]*Tunnel, error)
ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error)
+17
View File
@@ -116,6 +116,23 @@ func (r *RESTClient) GetTunnel(tunnelID uuid.UUID) (*Tunnel, error) {
return nil, r.statusCodeToError("get tunnel", resp)
}
func (r *RESTClient) GetTunnelToken(tunnelID uuid.UUID) (token string, err error) {
endpoint := r.baseEndpoints.accountLevel
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/token", tunnelID))
resp, err := r.sendRequest("GET", endpoint, nil)
if err != nil {
return "", errors.Wrap(err, "REST request failed")
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
err = parseResponse(resp.Body, &token)
return token, err
}
return "", r.statusCodeToError("get tunnel token", resp)
}
func (r *RESTClient) DeleteTunnel(tunnelID uuid.UUID) error {
endpoint := r.baseEndpoints.accountLevel
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
+2
View File
@@ -192,8 +192,10 @@ stretch: &stretch
builddeps:
- openssh-client
- s3cmd
- jq
post-cache:
- .teamcity/update-homebrew.sh
- .teamcity/update-homebrew-core.sh
github-message-release:
build_dir: *build_dir
builddeps:
+24 -8
View File
@@ -43,11 +43,12 @@ const (
serviceConfigFile = "config.yml"
serviceCredentialFile = "cert.pem"
serviceConfigPath = serviceConfigDir + "/" + serviceConfigFile
cloudflaredService = "cloudflared.service"
)
var systemdTemplates = []ServiceTemplate{
{
Path: "/etc/systemd/system/cloudflared.service",
Path: fmt.Sprintf("/etc/systemd/system/%s", cloudflaredService),
Content: `[Unit]
Description=cloudflared
After=network.target
@@ -268,16 +269,19 @@ func installSystemd(templateArgs *ServiceTemplateArgs, log *zerolog.Logger) erro
return err
}
}
if err := runCommand("systemctl", "enable", "cloudflared.service"); err != nil {
log.Err(err).Msg("systemctl enable cloudflared.service error")
if err := runCommand("systemctl", "enable", cloudflaredService); err != nil {
log.Err(err).Msgf("systemctl enable %s error", cloudflaredService)
return err
}
if err := runCommand("systemctl", "start", "cloudflared-update.timer"); err != nil {
log.Err(err).Msg("systemctl start cloudflared-update.timer error")
return err
}
log.Info().Msg("running systemctl daemon-reload")
return runCommand("systemctl", "daemon-reload")
if err := runCommand("systemctl", "daemon-reload"); err != nil {
log.Err(err).Msg("systemctl daemon-reload error")
return err
}
return runCommand("systemctl", "start", cloudflaredService)
}
func installSysv(templateArgs *ServiceTemplateArgs, log *zerolog.Logger) error {
@@ -300,7 +304,7 @@ func installSysv(templateArgs *ServiceTemplateArgs, log *zerolog.Logger) error {
continue
}
}
return nil
return runCommand("service", "cloudflared", "start")
}
func uninstallLinuxService(c *cli.Context) error {
@@ -323,8 +327,12 @@ func uninstallLinuxService(c *cli.Context) error {
}
func uninstallSystemd(log *zerolog.Logger) error {
if err := runCommand("systemctl", "disable", "cloudflared.service"); err != nil {
log.Err(err).Msg("systemctl disable cloudflared.service error")
if err := runCommand("systemctl", "disable", cloudflaredService); err != nil {
log.Err(err).Msgf("systemctl disable %s error", cloudflaredService)
return err
}
if err := runCommand("systemctl", "stop", cloudflaredService); err != nil {
log.Err(err).Msgf("systemctl stop %s error", cloudflaredService)
return err
}
if err := runCommand("systemctl", "stop", "cloudflared-update.timer"); err != nil {
@@ -337,10 +345,18 @@ func uninstallSystemd(log *zerolog.Logger) error {
return err
}
}
if err := runCommand("systemctl", "daemon-reload"); err != nil {
log.Err(err).Msg("systemctl daemon-reload error")
return err
}
return nil
}
func uninstallSysv(log *zerolog.Logger) error {
if err := runCommand("service", "cloudflared", "stop"); err != nil {
log.Err(err).Msg("service cloudflared stop error")
return err
}
if err := sysvTemplate.Remove(); err != nil {
log.Err(err).Msg("error removing service template")
return err
+1 -1
View File
@@ -73,7 +73,7 @@ func Run(c *cli.Context) error {
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(
c.String("address"),
+12 -3
View File
@@ -44,7 +44,7 @@ func (st *ServiceTemplate) Generate(args *ServiceTemplateArgs) error {
return err
}
if _, err = os.Stat(resolvedPath); err == nil {
return fmt.Errorf("cloudflared service is already installed at %s", resolvedPath)
return fmt.Errorf(serviceAlreadyExistsWarn(resolvedPath))
}
var buffer bytes.Buffer
@@ -75,6 +75,15 @@ func (st *ServiceTemplate) Remove() error {
return nil
}
func serviceAlreadyExistsWarn(service string) string {
return fmt.Sprintf("cloudflared service is already installed at %s; if you are running a cloudflared tunnel, you "+
"can point it to multiple origins, avoiding the need to run more than one cloudflared service in the "+
"same machine; otherwise if you are really sure, you can do `cloudflared service uninstall` to clean "+
"up the existing service and then try again this command",
service,
)
}
func runCommand(command string, args ...string) error {
cmd := exec.Command(command, args...)
stderr, err := cmd.StderrPipe()
@@ -86,10 +95,10 @@ func runCommand(command string, args ...string) error {
return fmt.Errorf("error starting %s: %v", command, err)
}
_, _ = ioutil.ReadAll(stderr)
output, _ := ioutil.ReadAll(stderr)
err = cmd.Wait()
if err != nil {
return fmt.Errorf("%s returned with error: %v", command, err)
return fmt.Errorf("%s %v returned with error code %v due to: %v", command, args, err, string(output))
}
return nil
}
+28 -19
View File
@@ -109,6 +109,7 @@ func Commands() []*cli.Command {
buildIngressSubcommand(),
buildDeleteCommand(),
buildCleanupCommand(),
buildTokenCommand(),
// for compatibility, allow following as tunnel subcommands
proxydns.Command(true),
cliutil.RemovedCommand("db-connect"),
@@ -127,26 +128,34 @@ func buildTunnelCommand(subcommands []*cli.Command) *cli.Command {
Name: "tunnel",
Action: cliutil.ConfiguredAction(TunnelCommand),
Category: "Tunnel",
Usage: "Make a locally-running web service accessible over the internet using Cloudflare Tunnel.",
Usage: "Use Cloudflare Tunnel to expose private services to the Internet or to Cloudflare connected private users.",
ArgsUsage: " ",
Description: `Cloudflare 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.
Description: ` Cloudflare Tunnel allows to expose private services without opening any ingress port on this machine. It can expose:
A) Locally reachable HTTP-based private services to the Internet on DNS with Cloudflare as authority (which you can
then protect with Cloudflare Access).
B) Locally reachable TCP/UDP-based private services to Cloudflare connected private users in the same account, e.g.,
those enrolled to a Zero Trust WARP Client.
This feature requires your Cloudflare account be subscribed to the Cloudflare Smart Routing feature.
You can manage your Tunnels via dash.teams.cloudflare.com. This approach will only require you to run a single command
later in each machine where you wish to run a Tunnel.
To use, begin by calling login to download a certificate:
Alternatively, you can manage your Tunnels via the command line. Begin by obtaining a certificate to be able to do so:
$ cloudflared tunnel login
$ cloudflared tunnel login
With your certificate installed you can then launch your first tunnel,
replacing my.site.com with a subdomain of your site:
With your certificate installed you can then get started with Tunnels:
$ cloudflared tunnel --hostname my.site.com --url http://localhost:8080
$ cloudflared tunnel create my-first-tunnel
$ cloudflared tunnel route dns my-first-tunnel my-first-tunnel.mydomain.com
$ cloudflared tunnel run --hello-world my-first-tunnel
If you have a web server running on port 8080 (in this example), it will be available on
the internet!`,
You can now access my-first-tunnel.mydomain.com and be served an example page by your local cloudflared process.
For exposing local TCP/UDP services by IP to your privately connected users, check out:
$ cloudflared tunnel route ip --help
See https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/tunnel-guide/ for more info.`,
Subcommands: subcommands,
Flags: tunnelFlags(false),
}
@@ -340,6 +349,11 @@ func StartServer(
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"))
if err != nil {
log.Err(err).Msg("Error opening metrics server listener")
@@ -351,14 +365,9 @@ func StartServer(
defer wg.Done()
readinessServer := metrics.NewReadyServer(log)
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)
if c.IsSet("stdin-control") {
log.Info().Msg("Enabling control through stdin")
@@ -341,6 +341,21 @@ func (sc *subcommandContext) cleanupConnections(tunnelIDs []uuid.UUID) error {
return nil
}
func (sc *subcommandContext) getTunnelTokenCredentials(tunnelID uuid.UUID) (*connection.TunnelToken, error) {
client, err := sc.client()
if err != nil {
return nil, err
}
token, err := client.GetTunnelToken(tunnelID)
if err != nil {
sc.log.Err(err).Msgf("Could not get the Token for the given Tunnel %v", tunnelID)
return nil, err
}
return ParseToken(token)
}
func (sc *subcommandContext) route(tunnelID uuid.UUID, r cfapi.HostnameRoute) (cfapi.HostnameRouteResult, error) {
client, err := sc.client()
if err != nil {
@@ -216,6 +216,10 @@ func (d *deleteMockTunnelStore) GetTunnel(tunnelID uuid.UUID) (*cfapi.Tunnel, er
return &tunnel.tunnel, nil
}
func (d *deleteMockTunnelStore) GetTunnelToken(tunnelID uuid.UUID) (string, error) {
return "token", nil
}
func (d *deleteMockTunnelStore) DeleteTunnel(tunnelID uuid.UUID) error {
tunnel, ok := d.mockTunnels[tunnelID]
if !ok {
+53
View File
@@ -714,6 +714,59 @@ func cleanupCommand(c *cli.Context) error {
return sc.cleanupConnections(tunnelIDs)
}
func buildTokenCommand() *cli.Command {
return &cli.Command{
Name: "token",
Action: cliutil.ConfiguredAction(tokenCommand),
Usage: "Fetch the credentials token for an existing tunnel (by name or UUID) that allows to run it",
UsageText: "cloudflared tunnel [tunnel command options] token [subcommand options] TUNNEL",
Description: "cloudflared tunnel token will fetch the credentials token for a given tunnel (by its name or UUID), which is then used to run the tunnel. This command fails if the tunnel does not exist or has been deleted. Use the flag `cloudflared tunnel token --cred-file /my/path/file.json TUNNEL` to output the token to the credentials JSON file. Note: this command only works for Tunnels created since cloudflared version 2022.3.0",
Flags: []cli.Flag{credentialsFileFlagCLIOnly},
CustomHelpTemplate: commandHelpTemplate(),
}
}
func tokenCommand(c *cli.Context) error {
sc, err := newSubcommandContext(c)
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
warningChecker := updater.StartWarningCheck(c)
defer warningChecker.LogWarningIfAny(sc.log)
if c.NArg() != 1 {
return cliutil.UsageError(`"cloudflared tunnel token" requires exactly 1 argument, the name or UUID of tunnel to fetch the credentials token for.`)
}
tunnelID, err := sc.findID(c.Args().First())
if err != nil {
return errors.Wrap(err, "error parsing tunnel ID")
}
token, err := sc.getTunnelTokenCredentials(tunnelID)
if err != nil {
return err
}
if path := c.String(CredFileFlag); path != "" {
credentials := token.Credentials()
err := writeTunnelCredentials(path, &credentials)
if err != nil {
return errors.Wrapf(err, "error writing token credentials to JSON file in path %s", path)
}
return nil
}
encodedToken, err := token.Encode()
if err != nil {
return err
}
fmt.Printf("%s", encodedToken)
return nil
}
func buildRouteCommand() *cli.Command {
return &cli.Command{
Name: "route",
+1 -1
View File
@@ -191,7 +191,7 @@ func installWindowsService(c *cli.Context) error {
log := zeroLogger.With().Str(LogFieldWindowsServiceName, windowsServiceName).Logger()
if err == nil {
s.Close()
return fmt.Errorf("Service %s already exists", windowsServiceName)
return fmt.Errorf(serviceAlreadyExistsWarn(windowsServiceName))
}
extraArgs, err := getServiceExtraArgsFromCliArgs(c, &log)
if err != nil {
+11 -6
View File
@@ -72,13 +72,18 @@ class NamedTunnelConfig(NamedTunnelBaseConfig):
return config
def get_token(self):
with open(self.credentials_file) as json_file:
creds = json.load(json_file)
token_dict = {"a": creds["AccountTag"], "t": creds["TunnelID"], "s": creds["TunnelSecret"]}
token_json_str = json.dumps(token_dict)
def get_tunnel_id(self):
return self.full_config["tunnel"]
return base64.b64encode(token_json_str.encode('utf-8'))
def get_token(self):
creds = self.get_credentials_json()
token_dict = {"a": creds["AccountTag"], "t": creds["TunnelID"], "s": creds["TunnelSecret"]}
token_json_str = json.dumps(token_dict)
return base64.b64encode(token_json_str.encode('utf-8'))
def get_credentials_json(self):
with open(self.credentials_file) as json_file:
return json.load(json_file)
@dataclass(frozen=True)
-2
View File
@@ -141,12 +141,10 @@ class TestServiceMode:
def sysv_service_scenario(self, config, tmp_path, extra_assertions=None, use_token=False):
with self.run_service(tmp_path, config, root=True, use_token=use_token):
self.sysv_cmd("start")
self.sysv_cmd("status")
wait_tunnel_ready(tunnel_url=config.get_url())
if extra_assertions is not None:
extra_assertions()
self.sysv_cmd("stop")
# Service install copies config file to /etc/cloudflared/config.yml
subprocess.run(["sudo", "rm", "/etc/cloudflared/config.yml"])
+35
View File
@@ -0,0 +1,35 @@
import base64
import json
from setup import get_config_from_file, persist_origin_cert
from util import start_cloudflared
class TestToken:
def test_get_token(self, tmp_path, component_tests_config):
config = component_tests_config()
tunnel_id = config.get_tunnel_id()
token_args = ["--origincert", cert_path(), "token", tunnel_id]
output = start_cloudflared(tmp_path, config, token_args)
assert parse_token(config.get_token()) == parse_token(output.stdout)
def test_get_credentials_file(self, tmp_path, component_tests_config):
config = component_tests_config()
tunnel_id = config.get_tunnel_id()
cred_file = tmp_path / "test_get_credentials_file.json"
token_args = ["--origincert", cert_path(), "token", "--cred-file", cred_file, tunnel_id]
start_cloudflared(tmp_path, config, token_args)
with open(cred_file) as json_file:
assert config.get_credentials_json() == json.load(json_file)
def cert_path():
return get_config_from_file()["origincert"]
def parse_token(token):
return json.loads(base64.b64decode(token))
+1 -1
View File
@@ -411,7 +411,7 @@ type CustomDuration struct {
time.Duration
}
func (s *CustomDuration) MarshalJSON() ([]byte, error) {
func (s CustomDuration) MarshalJSON() ([]byte, error) {
return json.Marshal(s.Duration.Seconds())
}
+11
View File
@@ -2,6 +2,7 @@ package connection
import (
"context"
"encoding/base64"
"fmt"
"io"
"math"
@@ -11,6 +12,7 @@ import (
"time"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/cloudflare/cloudflared/websocket"
@@ -65,6 +67,15 @@ func (t TunnelToken) Credentials() Credentials {
}
}
func (t TunnelToken) Encode() (string, error) {
val, err := json.Marshal(t)
if err != nil {
return "", errors.Wrap(err, "could not JSON encode token")
}
return base64.StdEncoding.EncodeToString(val), nil
}
type ClassicTunnelProperties struct {
Hostname string
OriginCert []byte
+41 -39
View File
@@ -11,14 +11,16 @@ import (
"github.com/cloudflare/cloudflared/tlsconfig"
)
const (
defaultConnectTimeout = 30 * time.Second
defaultTLSTimeout = 10 * time.Second
defaultTCPKeepAlive = 30 * time.Second
defaultKeepAliveConnections = 100
defaultKeepAliveTimeout = 90 * time.Second
defaultProxyAddress = "127.0.0.1"
var (
defaultConnectTimeout = config.CustomDuration{Duration: 30 * time.Second}
defaultTLSTimeout = config.CustomDuration{Duration: 10 * time.Second}
defaultTCPKeepAlive = config.CustomDuration{Duration: 30 * time.Second}
defaultKeepAliveTimeout = config.CustomDuration{Duration: 90 * time.Second}
)
const (
defaultProxyAddress = "127.0.0.1"
defaultKeepAliveConnections = 100
SSHServerFlag = "ssh-server"
Socks5Flag = "socks5"
ProxyConnectTimeoutFlag = "proxy-connect-timeout"
@@ -68,12 +70,12 @@ func (rc *RemoteConfig) UnmarshalJSON(b []byte) error {
}
func originRequestFromSingeRule(c *cli.Context) OriginRequestConfig {
var connectTimeout time.Duration = defaultConnectTimeout
var tlsTimeout time.Duration = defaultTLSTimeout
var tcpKeepAlive time.Duration = defaultTCPKeepAlive
var connectTimeout config.CustomDuration = defaultConnectTimeout
var tlsTimeout config.CustomDuration = defaultTLSTimeout
var tcpKeepAlive config.CustomDuration = defaultTCPKeepAlive
var noHappyEyeballs bool
var keepAliveConnections int = defaultKeepAliveConnections
var keepAliveTimeout time.Duration = defaultKeepAliveTimeout
var keepAliveTimeout config.CustomDuration = defaultKeepAliveTimeout
var httpHostHeader string
var originServerName string
var caPool string
@@ -84,13 +86,13 @@ func originRequestFromSingeRule(c *cli.Context) OriginRequestConfig {
var proxyPort uint
var proxyType string
if flag := ProxyConnectTimeoutFlag; c.IsSet(flag) {
connectTimeout = c.Duration(flag)
connectTimeout = config.CustomDuration{Duration: c.Duration(flag)}
}
if flag := ProxyTLSTimeoutFlag; c.IsSet(flag) {
tlsTimeout = c.Duration(flag)
tlsTimeout = config.CustomDuration{Duration: c.Duration(flag)}
}
if flag := ProxyTCPKeepAliveFlag; c.IsSet(flag) {
tcpKeepAlive = c.Duration(flag)
tcpKeepAlive = config.CustomDuration{Duration: c.Duration(flag)}
}
if flag := ProxyNoHappyEyeballsFlag; c.IsSet(flag) {
noHappyEyeballs = c.Bool(flag)
@@ -99,7 +101,7 @@ func originRequestFromSingeRule(c *cli.Context) OriginRequestConfig {
keepAliveConnections = c.Int(flag)
}
if flag := ProxyKeepAliveTimeoutFlag; c.IsSet(flag) {
keepAliveTimeout = c.Duration(flag)
keepAliveTimeout = config.CustomDuration{Duration: c.Duration(flag)}
}
if flag := HTTPHostHeaderFlag; c.IsSet(flag) {
httpHostHeader = c.String(flag)
@@ -158,13 +160,13 @@ func originRequestFromConfig(c config.OriginRequestConfig) OriginRequestConfig {
ProxyAddress: defaultProxyAddress,
}
if c.ConnectTimeout != nil {
out.ConnectTimeout = c.ConnectTimeout.Duration
out.ConnectTimeout = *c.ConnectTimeout
}
if c.TLSTimeout != nil {
out.TLSTimeout = c.TLSTimeout.Duration
out.TLSTimeout = *c.TLSTimeout
}
if c.TCPKeepAlive != nil {
out.TCPKeepAlive = c.TCPKeepAlive.Duration
out.TCPKeepAlive = *c.TCPKeepAlive
}
if c.NoHappyEyeballs != nil {
out.NoHappyEyeballs = *c.NoHappyEyeballs
@@ -173,7 +175,7 @@ func originRequestFromConfig(c config.OriginRequestConfig) OriginRequestConfig {
out.KeepAliveConnections = *c.KeepAliveConnections
}
if c.KeepAliveTimeout != nil {
out.KeepAliveTimeout = c.KeepAliveTimeout.Duration
out.KeepAliveTimeout = *c.KeepAliveTimeout
}
if c.HTTPHostHeader != nil {
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".
type OriginRequestConfig struct {
// 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
TLSTimeout time.Duration `yaml:"tlsTimeout"`
TLSTimeout config.CustomDuration `yaml:"tlsTimeout" json:"tlsTimeout"`
// 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
NoHappyEyeballs bool `yaml:"noHappyEyeballs"`
NoHappyEyeballs bool `yaml:"noHappyEyeballs" json:"noHappyEyeballs"`
// 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
KeepAliveConnections int `yaml:"keepAliveConnections"`
KeepAliveConnections int `yaml:"keepAliveConnections" json:"keepAliveConnections"`
// 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.
OriginServerName string `yaml:"originServerName"`
OriginServerName string `yaml:"originServerName" json:"originServerName"`
// Path to the CA for the certificate of your origin.
// 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.
// Will allow any certificate from the origin to be accepted.
// 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.
// Useful if you are running a WSGI server.
DisableChunkedEncoding bool `yaml:"disableChunkedEncoding"`
DisableChunkedEncoding bool `yaml:"disableChunkedEncoding" json:"disableChunkedEncoding"`
// Runs as jump host
BastionMode bool `yaml:"bastionMode"`
BastionMode bool `yaml:"bastionMode" json:"bastionMode"`
// Listen address for the proxy.
ProxyAddress string `yaml:"proxyAddress"`
ProxyAddress string `yaml:"proxyAddress" json:"proxyAddress"`
// Listen port for the proxy.
ProxyPort uint `yaml:"proxyPort"`
ProxyPort uint `yaml:"proxyPort" json:"proxyPort"`
// What sort of proxy should be started
ProxyType string `yaml:"proxyType"`
ProxyType string `yaml:"proxyType" json:"proxyType"`
// 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) {
if val := overrides.ConnectTimeout; val != nil {
defaults.ConnectTimeout = val.Duration
defaults.ConnectTimeout = *val
}
}
func (defaults *OriginRequestConfig) setTLSTimeout(overrides config.OriginRequestConfig) {
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) {
if val := overrides.KeepAliveTimeout; val != nil {
defaults.KeepAliveTimeout = val.Duration
defaults.KeepAliveTimeout = *val
}
}
func (defaults *OriginRequestConfig) setTCPKeepAlive(overrides config.OriginRequestConfig) {
if val := overrides.TCPKeepAlive; val != nil {
defaults.TCPKeepAlive = val.Duration
defaults.TCPKeepAlive = *val
}
}
+13 -13
View File
@@ -65,7 +65,7 @@ func TestUnmarshalRemoteConfigOverridesGlobal(t *testing.T) {
err := json.Unmarshal(rawConfig, &remoteConfig)
require.NoError(t, err)
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) {
@@ -74,11 +74,11 @@ func TestOriginRequestConfigOverrides(t *testing.T) {
// root-level configuration.
actual0 := ing.Rules[0].Config
expected0 := OriginRequestConfig{
ConnectTimeout: 1 * time.Minute,
TLSTimeout: 1 * time.Second,
TCPKeepAlive: 1 * time.Second,
ConnectTimeout: config.CustomDuration{Duration: 1 * time.Minute},
TLSTimeout: config.CustomDuration{Duration: 1 * time.Second},
TCPKeepAlive: config.CustomDuration{Duration: 1 * time.Second},
NoHappyEyeballs: true,
KeepAliveTimeout: 1 * time.Second,
KeepAliveTimeout: config.CustomDuration{Duration: 1 * time.Second},
KeepAliveConnections: 1,
HTTPHostHeader: "abc",
OriginServerName: "a1",
@@ -99,11 +99,11 @@ func TestOriginRequestConfigOverrides(t *testing.T) {
// Rule 1 overrode all the root-level config.
actual1 := ing.Rules[1].Config
expected1 := OriginRequestConfig{
ConnectTimeout: 2 * time.Minute,
TLSTimeout: 2 * time.Second,
TCPKeepAlive: 2 * time.Second,
ConnectTimeout: config.CustomDuration{Duration: 2 * time.Minute},
TLSTimeout: config.CustomDuration{Duration: 2 * time.Second},
TCPKeepAlive: config.CustomDuration{Duration: 2 * time.Second},
NoHappyEyeballs: false,
KeepAliveTimeout: 2 * time.Second,
KeepAliveTimeout: config.CustomDuration{Duration: 2 * time.Second},
KeepAliveConnections: 2,
HTTPHostHeader: "def",
OriginServerName: "b2",
@@ -286,11 +286,11 @@ func TestOriginRequestConfigDefaults(t *testing.T) {
// Rule 1 overrode all defaults.
actual1 := ing.Rules[1].Config
expected1 := OriginRequestConfig{
ConnectTimeout: 2 * time.Minute,
TLSTimeout: 2 * time.Second,
TCPKeepAlive: 2 * time.Second,
ConnectTimeout: config.CustomDuration{Duration: 2 * time.Minute},
TLSTimeout: config.CustomDuration{Duration: 2 * time.Second},
TCPKeepAlive: config.CustomDuration{Duration: 2 * time.Second},
NoHappyEyeballs: false,
KeepAliveTimeout: 2 * time.Second,
KeepAliveTimeout: config.CustomDuration{Duration: 2 * time.Second},
KeepAliveConnections: 2,
HTTPHostHeader: "def",
OriginServerName: "b2",
+9 -8
View File
@@ -64,8 +64,8 @@ func matchHost(ruleHost, reqHost string) bool {
// Ingress maps eyeball requests to origins.
type Ingress struct {
Rules []Rule
defaults OriginRequestConfig
Rules []Rule `json:"ingress"`
Defaults OriginRequestConfig `json:"originRequest"`
}
// 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{}),
},
},
defaults: defaults,
Defaults: defaults,
}
return ing, err
}
@@ -180,7 +180,7 @@ func validateIngress(ingress []config.UnvalidatedIngressRule, defaults OriginReq
}
srv := newStatusCode(status)
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)
} else if r.Service == ServiceSocksProxy {
rules := make([]ipaccess.Rule, len(r.OriginRequest.IPRules))
@@ -230,23 +230,24 @@ func validateIngress(ingress []config.UnvalidatedIngressRule, defaults OriginReq
return Ingress{}, err
}
var pathRegex *regexp.Regexp
var pathRegexp *Regexp
if r.Path != "" {
var err error
pathRegex, err = regexp.Compile(r.Path)
regex, err := regexp.Compile(r.Path)
if err != nil {
return Ingress{}, errors.Wrapf(err, "Rule #%d has an invalid regex", i+1)
}
pathRegexp = &Regexp{Regexp: regex}
}
rules[i] = Rule{
Hostname: r.Hostname,
Service: service,
Path: pathRegex,
Path: pathRegexp,
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 {
+7 -7
View File
@@ -482,12 +482,12 @@ func TestSingleOriginSetsConfig(t *testing.T) {
ingress, err := NewSingleOrigin(cliCtx, allowURLFromArgs)
require.NoError(t, err)
assert.Equal(t, time.Second, ingress.Rules[0].Config.ConnectTimeout)
assert.Equal(t, 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.ConnectTimeout)
assert.Equal(t, config.CustomDuration{Duration: time.Second}, ingress.Rules[0].Config.TLSTimeout)
assert.Equal(t, config.CustomDuration{Duration: time.Second}, ingress.Rules[0].Config.TCPKeepAlive)
assert.True(t, ingress.Rules[0].Config.NoHappyEyeballs)
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", ingress.Rules[0].Config.OriginServerName)
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",
Path: mustParsePath(t, "/health"),
Path: MustParsePath(t, "/health"),
},
{
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)
assert.NoError(t, err)
return regexp
return &Regexp{Regexp: regexp}
}
func MustParseURL(t *testing.T, rawURL string) *url.URL {
+46 -7
View File
@@ -3,6 +3,7 @@ package ingress
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net"
@@ -20,7 +21,8 @@ import (
)
const (
HelloWorldService = "Hello World test origin"
HelloWorldService = "hello_world"
HttpStatusService = "http_status"
)
// OriginService is something a tunnel can proxy traffic to.
@@ -31,6 +33,7 @@ type OriginService interface {
// starting the origin service.
// Implementor of services managed by cloudflared should terminate the service if shutdownC is closed
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)
@@ -41,7 +44,11 @@ type unixSocketPath struct {
}
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 {
@@ -53,6 +60,10 @@ func (o *unixSocketPath) start(log *zerolog.Logger, _ <-chan struct{}, cfg Origi
return nil
}
func (o unixSocketPath) MarshalJSON() ([]byte, error) {
return json.Marshal(o.String())
}
type httpService struct {
url *url.URL
hostHeader string
@@ -73,6 +84,10 @@ func (o *httpService) String() 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
// It's used by warp routing
type rawTCPService struct {
@@ -87,6 +102,10 @@ func (o *rawTCPService) start(log *zerolog.Logger, _ <-chan struct{}, cfg Origin
return nil
}
func (o rawTCPService) MarshalJSON() ([]byte, error) {
return json.Marshal(o.String())
}
// tcpOverWSService models TCP origins serving eyeballs connecting over websocket, such as
// cloudflared access commands.
type tcpOverWSService struct {
@@ -153,6 +172,10 @@ func (o *tcpOverWSService) start(log *zerolog.Logger, _ <-chan struct{}, cfg Ori
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 {
return nil
}
@@ -161,6 +184,10 @@ func (o *socksProxyOverWSService) String() string {
return ServiceSocksProxy
}
func (o socksProxyOverWSService) MarshalJSON() ([]byte, error) {
return json.Marshal(o.String())
}
// HelloWorld is an OriginService for the built-in Hello World server.
// Users only use this for testing and experimenting with cloudflared.
type helloWorld struct {
@@ -197,6 +224,10 @@ func (o *helloWorld) start(
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.
// Typical use-case is "user wants the catch-all rule to just respond 404".
type statusCode struct {
@@ -213,7 +244,7 @@ func newStatusCode(status int) statusCode {
}
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(
@@ -224,6 +255,10 @@ func (o *statusCode) start(
return nil
}
func (o statusCode) MarshalJSON() ([]byte, error) {
return json.Marshal(o.String())
}
type NopReadCloser struct{}
// Read always returns EOF to signal end of input
@@ -245,8 +280,8 @@ func newHTTPTransport(service OriginService, cfg OriginRequestConfig, log *zerol
Proxy: http.ProxyFromEnvironment,
MaxIdleConns: cfg.KeepAliveConnections,
MaxIdleConnsPerHost: cfg.KeepAliveConnections,
IdleConnTimeout: cfg.KeepAliveTimeout,
TLSHandshakeTimeout: cfg.TLSTimeout,
IdleConnTimeout: cfg.KeepAliveTimeout.Duration,
TLSHandshakeTimeout: cfg.TLSTimeout.Duration,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{RootCAs: originCertPool, InsecureSkipVerify: cfg.NoTLSVerify},
}
@@ -255,8 +290,8 @@ func newHTTPTransport(service OriginService, cfg OriginRequestConfig, log *zerol
}
dialer := &net.Dialer{
Timeout: cfg.ConnectTimeout,
KeepAlive: cfg.TCPKeepAlive,
Timeout: cfg.ConnectTimeout.Duration,
KeepAlive: cfg.TCPKeepAlive.Duration,
}
if cfg.NoHappyEyeballs {
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 {
return nil
}
func (mos MockOriginHTTPService) MarshalJSON() ([]byte, error) {
return json.Marshal(mos.String())
}
+20 -7
View File
@@ -1,6 +1,7 @@
package ingress
import (
"encoding/json"
"regexp"
"strings"
)
@@ -9,18 +10,18 @@ import (
// service running on the given URL.
type Rule struct {
// 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 *regexp.Regexp
Path *Regexp `json:"path"`
// A (probably local) address. Requests for a hostname which matches this
// rule's hostname pattern will be proxied to the service running on this
// address.
Service OriginService
Service OriginService `json:"service"`
// 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
@@ -32,9 +33,9 @@ func (r Rule) MultiLineString() string {
out.WriteString(r.Hostname)
out.WriteRune('\n')
}
if r.Path != nil {
if r.Path != nil && r.Path.Regexp != nil {
out.WriteString("\tpath: ")
out.WriteString(r.Path.String())
out.WriteString(r.Path.Regexp.String())
out.WriteRune('\n')
}
out.WriteString("\tservice: ")
@@ -45,6 +46,18 @@ func (r Rule) MultiLineString() string {
// Matches checks if the rule matches a given hostname/path combination.
func (r *Rule) Matches(hostname, path string) bool {
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
}
// 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())
}
+76 -2
View File
@@ -1,6 +1,7 @@
package ingress
import (
"encoding/json"
"io"
"net/http/httptest"
"net/url"
@@ -8,12 +9,14 @@ import (
"testing"
"github.com/stretchr/testify/require"
"github.com/cloudflare/cloudflared/config"
)
func Test_rule_matches(t *testing.T) {
type fields struct {
Hostname string
Path *regexp.Regexp
Path *Regexp
Service OriginService
}
type args struct {
@@ -99,13 +102,35 @@ func Test_rule_matches(t *testing.T) {
name: "Hostname and path",
fields: fields{
Hostname: "*.example.com",
Path: regexp.MustCompile("/static/.*\\.html"),
Path: &Regexp{Regexp: regexp.MustCompile("/static/.*\\.html")},
},
args: args{
requestURL: MustParseURL(t, "https://www.example.com/static/index.html"),
},
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 {
t.Run(tt.name, func(t *testing.T) {
@@ -144,3 +169,52 @@ func TestStaticHTTPStatus(t *testing.T) {
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))
})
}
}
+24 -2
View File
@@ -22,7 +22,16 @@ const (
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.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) {
_, _ = 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
}
@@ -45,6 +66,7 @@ func ServeMetrics(
shutdownC <-chan struct{},
readyServer *ReadyServer,
quickTunnelHostname string,
orchestrator orchestrator,
log *zerolog.Logger,
) (err error) {
var wg sync.WaitGroup
@@ -52,7 +74,7 @@ func ServeMetrics(
trace.AuthRequest = func(*http.Request) (bool, bool) { return true, true }
// TODO: parameterize ReadTimeout and WriteTimeout. The maximum time we can
// profile CPU usage depends on WriteTimeout
h := newMetricsHandler(readyServer, quickTunnelHostname)
h := newMetricsHandler(readyServer, quickTunnelHostname, orchestrator, log)
server := &http.Server{
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
+27
View File
@@ -10,6 +10,7 @@ import (
"github.com/pkg/errors"
"github.com/rs/zerolog"
"github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/ingress"
"github.com/cloudflare/cloudflared/proxy"
@@ -130,6 +131,32 @@ func (o *Orchestrator) updateIngress(ingressRules ingress.Ingress, warpRoutingEn
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
func (o *Orchestrator) GetOriginProxy() (connection.OriginProxy, error) {
val := o.proxy.Load()
+4 -3
View File
@@ -17,6 +17,7 @@ import (
"github.com/rs/zerolog"
"github.com/stretchr/testify/require"
"github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/ingress"
"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.Len(t, configV2.Ingress.Rules, 3)
// 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)
// Inherited from global default
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.Equal(t, "http://172.32.20.6:80", configV2.Ingress.Rules[1].Service.String())
// 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)
// Inherited from global default
require.Equal(t, true, configV2.Ingress.Rules[1].Config.NoHappyEyeballs)
// Validate ingress rule 2, it's the catch-all rule
require.True(t, configV2.Ingress.Rules[2].Matches("blogs.tunnel.io", "/2022/02/10"))
// 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, true, configV2.Ingress.Rules[2].Config.NoHappyEyeballs)
require.True(t, configV2.WarpRoutingEnabled)