diff --git a/RELEASE_NOTES b/RELEASE_NOTES
index 153685ed..e3118e14 100644
--- a/RELEASE_NOTES
+++ b/RELEASE_NOTES
@@ -1,3 +1,12 @@
+2022.3.0
+- 2022-03-02 TUN-5680: Adapt component tests for new service install based on token
+- 2022-02-21 TUN-5682: Remove name field from credentials
+- 2022-02-21 TUN-5681: Add support for running tunnel using Token
+- 2022-02-28 TUN-5824: Update updater no-update-in-shell link
+- 2022-02-28 TUN-5823: Warn about legacy flags that are ignored when ingress rules are used
+- 2022-02-28 TUN-5737: Support https protocol over unix socket origin
+- 2022-02-23 TUN-5679: Add support for service install using Tunnel Token
+
2022.2.2
- 2022-02-22 TUN-5754: Allow ingress validate to take plaintext option
- 2022-02-17 TUN-5678: Cloudflared uses typed tunnel API
diff --git a/cmd/cloudflared/common_service.go b/cmd/cloudflared/common_service.go
new file mode 100644
index 00000000..db7338c0
--- /dev/null
+++ b/cmd/cloudflared/common_service.go
@@ -0,0 +1,30 @@
+package main
+
+import (
+ "github.com/rs/zerolog"
+ "github.com/urfave/cli/v2"
+
+ "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
+ "github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
+)
+
+func buildArgsForToken(c *cli.Context, log *zerolog.Logger) ([]string, error) {
+ token := c.Args().First()
+ if _, err := tunnel.ParseToken(token); err != nil {
+ return nil, cliutil.UsageError("Provided tunnel token is not valid (%s).", err)
+ }
+
+ return []string{
+ "tunnel", "run", "--token", token,
+ }, nil
+}
+
+func getServiceExtraArgsFromCliArgs(c *cli.Context, log *zerolog.Logger) ([]string, error) {
+ if c.NArg() > 0 {
+ // currently, we only support extra args for token
+ return buildArgsForToken(c, log)
+ } else {
+ // empty extra args
+ return make([]string, 0), nil
+ }
+}
diff --git a/cmd/cloudflared/linux_service.go b/cmd/cloudflared/linux_service.go
index 85d195fa..2d3060e7 100644
--- a/cmd/cloudflared/linux_service.go
+++ b/cmd/cloudflared/linux_service.go
@@ -6,7 +6,6 @@ package main
import (
"fmt"
"os"
- "path/filepath"
"github.com/rs/zerolog"
"github.com/urfave/cli/v2"
@@ -26,12 +25,6 @@ func runApp(app *cli.App, graceShutdownC chan struct{}) {
Name: "install",
Usage: "Install Cloudflare Tunnel as a system service",
Action: cliutil.ConfiguredAction(installLinuxService),
- Flags: []cli.Flag{
- &cli.BoolFlag{
- Name: "legacy",
- Usage: "Generate service file for non-named tunnels",
- },
- },
},
{
Name: "uninstall",
@@ -62,7 +55,7 @@ After=network.target
[Service]
TimeoutStartSec=0
Type=notify
-ExecStart={{ .Path }} --config /etc/cloudflared/config.yml --no-autoupdate{{ range .ExtraArgs }} {{ . }}{{ end }}
+ExecStart={{ .Path }} --no-autoupdate{{ range .ExtraArgs }} {{ . }}{{ end }}
Restart=on-failure
RestartSec=5s
@@ -112,7 +105,7 @@ var sysvTemplate = ServiceTemplate{
# Description: Cloudflare Tunnel agent
### END INIT INFO
name=$(basename $(readlink -f $0))
-cmd="{{.Path}} --config /etc/cloudflared/config.yml --pidfile /var/run/$name.pid --autoupdate-freq 24h0m0s{{ range .ExtraArgs }} {{ . }}{{ end }}"
+cmd="{{.Path}} --pidfile /var/run/$name.pid --autoupdate-freq 24h0m0s{{ range .ExtraArgs }} {{ . }}{{ end }}"
pid_file="/var/run/$name.pid"
stdout_log="/var/log/$name.log"
stderr_log="/var/log/$name.err"
@@ -191,27 +184,6 @@ func isSystemd() bool {
return false
}
-func copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile string, log *zerolog.Logger) error {
- srcCredentialPath := filepath.Join(userConfigDir, userCredentialFile)
- destCredentialPath := filepath.Join(serviceConfigDir, serviceCredentialFile)
- if srcCredentialPath != destCredentialPath {
- if err := copyCredential(srcCredentialPath, destCredentialPath); err != nil {
- return err
- }
- }
-
- srcConfigPath := filepath.Join(userConfigDir, userConfigFile)
- destConfigPath := filepath.Join(serviceConfigDir, serviceConfigFile)
- if srcConfigPath != destConfigPath {
- if err := copyConfig(srcConfigPath, destConfigPath); err != nil {
- return err
- }
- log.Info().Msgf("Copied %s to %s", srcConfigPath, destConfigPath)
- }
-
- return nil
-}
-
func installLinuxService(c *cli.Context) error {
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
@@ -223,52 +195,19 @@ func installLinuxService(c *cli.Context) error {
Path: etPath,
}
- if err := ensureConfigDirExists(serviceConfigDir); err != nil {
+ var extraArgsFunc func(c *cli.Context, log *zerolog.Logger) ([]string, error)
+ if c.NArg() == 0 {
+ extraArgsFunc = buildArgsForConfig
+ } else {
+ extraArgsFunc = buildArgsForToken
+ }
+
+ extraArgs, err := extraArgsFunc(c, log)
+ if err != nil {
return err
}
- if c.Bool("legacy") {
- userConfigDir := filepath.Dir(c.String("config"))
- userConfigFile := filepath.Base(c.String("config"))
- userCredentialFile := config.DefaultCredentialFile
- if err = copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile, log); err != nil {
- log.Err(err).Msgf("Failed to copy user configuration. Before running the service, ensure that %s contains two files, %s and %s",
- serviceConfigDir, serviceCredentialFile, serviceConfigFile)
- return err
- }
- templateArgs.ExtraArgs = []string{
- "--origincert", serviceConfigDir + "/" + serviceCredentialFile,
- }
- } else {
- src, _, err := config.ReadConfigFile(c, log)
- if err != nil {
- return err
- }
- // can't use context because this command doesn't define "credentials-file" flag
- configPresent := func(s string) bool {
- val, err := src.String(s)
- return err == nil && val != ""
- }
- if src.TunnelID == "" || !configPresent(tunnel.CredFileFlag) {
- return fmt.Errorf(`Configuration file %s must contain entries for the tunnel to run and its associated credentials:
-tunnel: TUNNEL-UUID
-credentials-file: CREDENTIALS-FILE
-`, src.Source())
- }
- if src.Source() != serviceConfigPath {
- if exists, err := config.FileExists(serviceConfigPath); err != nil || exists {
- return fmt.Errorf("Possible conflicting configuration in %[1]s and %[2]s. Either remove %[2]s or run `cloudflared --config %[2]s service install`", src.Source(), serviceConfigPath)
- }
-
- if err := copyFile(src.Source(), serviceConfigPath); err != nil {
- return fmt.Errorf("failed to copy %s to %s: %w", src.Source(), serviceConfigPath, err)
- }
- }
-
- templateArgs.ExtraArgs = []string{
- "tunnel", "run",
- }
- }
+ templateArgs.ExtraArgs = extraArgs
switch {
case isSystemd():
@@ -280,6 +219,42 @@ credentials-file: CREDENTIALS-FILE
}
}
+func buildArgsForConfig(c *cli.Context, log *zerolog.Logger) ([]string, error) {
+ if err := ensureConfigDirExists(serviceConfigDir); err != nil {
+ return nil, err
+ }
+
+ src, _, err := config.ReadConfigFile(c, log)
+ if err != nil {
+ return nil, err
+ }
+
+ // can't use context because this command doesn't define "credentials-file" flag
+ configPresent := func(s string) bool {
+ val, err := src.String(s)
+ return err == nil && val != ""
+ }
+ if src.TunnelID == "" || !configPresent(tunnel.CredFileFlag) {
+ return nil, fmt.Errorf(`Configuration file %s must contain entries for the tunnel to run and its associated credentials:
+tunnel: TUNNEL-UUID
+credentials-file: CREDENTIALS-FILE
+`, src.Source())
+ }
+ if src.Source() != serviceConfigPath {
+ if exists, err := config.FileExists(serviceConfigPath); err != nil || exists {
+ return nil, fmt.Errorf("Possible conflicting configuration in %[1]s and %[2]s. Either remove %[2]s or run `cloudflared --config %[2]s service install`", src.Source(), serviceConfigPath)
+ }
+
+ if err := copyFile(src.Source(), serviceConfigPath); err != nil {
+ return nil, fmt.Errorf("failed to copy %s to %s: %w", src.Source(), serviceConfigPath, err)
+ }
+ }
+
+ return []string{
+ "--config", "/etc/cloudflared/config.yml", "tunnel", "run",
+ }, nil
+}
+
func installSystemd(templateArgs *ServiceTemplateArgs, log *zerolog.Logger) error {
for _, serviceTemplate := range systemdTemplates {
err := serviceTemplate.Generate(templateArgs)
diff --git a/cmd/cloudflared/macos_service.go b/cmd/cloudflared/macos_service.go
index e987df87..542b8849 100644
--- a/cmd/cloudflared/macos_service.go
+++ b/cmd/cloudflared/macos_service.go
@@ -50,6 +50,9 @@ func newLaunchdTemplate(installPath, stdoutPath, stderrPath string) *ServiceTemp
ProgramArguments
{{ .Path }}
+ {{- range $i, $item := .ExtraArgs}}
+ {{ $item }}
+ {{- end}}
RunAtLoad
@@ -129,6 +132,13 @@ func installLaunchd(c *cli.Context) error {
log.Err(err).Msg("Error determining install path")
return errors.Wrap(err, "Error determining install path")
}
+ extraArgs, err := getServiceExtraArgsFromCliArgs(c, log)
+ if err != nil {
+ errMsg := "Unable to determine extra arguments for launch daemon"
+ log.Err(err).Msg(errMsg)
+ return errors.Wrap(err, errMsg)
+ }
+
stdoutPath, err := stdoutPath()
if err != nil {
log.Err(err).Msg("error determining stdout path")
@@ -140,7 +150,7 @@ func installLaunchd(c *cli.Context) error {
return errors.Wrap(err, "error determining stderr path")
}
launchdTemplate := newLaunchdTemplate(installPath, stdoutPath, stderrPath)
- templateArgs := ServiceTemplateArgs{Path: etPath}
+ templateArgs := ServiceTemplateArgs{Path: etPath, ExtraArgs: extraArgs}
err = launchdTemplate.Generate(&templateArgs)
if err != nil {
log.Err(err).Msg("error generating launchd template")
diff --git a/cmd/cloudflared/tunnel/cmd.go b/cmd/cloudflared/tunnel/cmd.go
index 2a692f73..43eee046 100644
--- a/cmd/cloudflared/tunnel/cmd.go
+++ b/cmd/cloudflared/tunnel/cmd.go
@@ -724,43 +724,43 @@ func configureProxyFlags(shouldHide bool) []cli.Flag {
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: ingress.Socks5Flag,
- Usage: "specify if this tunnel is running as a SOCK5 Server",
+ Usage: legacyTunnelFlag("specify if this tunnel is running as a SOCK5 Server"),
EnvVars: []string{"TUNNEL_SOCKS"},
Value: false,
Hidden: shouldHide,
}),
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: ingress.ProxyConnectTimeoutFlag,
- Usage: "HTTP proxy timeout for establishing a new connection",
+ Usage: legacyTunnelFlag("HTTP proxy timeout for establishing a new connection"),
Value: time.Second * 30,
Hidden: shouldHide,
}),
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: ingress.ProxyTLSTimeoutFlag,
- Usage: "HTTP proxy timeout for completing a TLS handshake",
+ Usage: legacyTunnelFlag("HTTP proxy timeout for completing a TLS handshake"),
Value: time.Second * 10,
Hidden: shouldHide,
}),
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: ingress.ProxyTCPKeepAliveFlag,
- Usage: "HTTP proxy TCP keepalive duration",
+ Usage: legacyTunnelFlag("HTTP proxy TCP keepalive duration"),
Value: time.Second * 30,
Hidden: shouldHide,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: ingress.ProxyNoHappyEyeballsFlag,
- Usage: "HTTP proxy should disable \"happy eyeballs\" for IPv4/v6 fallback",
+ Usage: legacyTunnelFlag("HTTP proxy should disable \"happy eyeballs\" for IPv4/v6 fallback"),
Hidden: shouldHide,
}),
altsrc.NewIntFlag(&cli.IntFlag{
Name: ingress.ProxyKeepAliveConnectionsFlag,
- Usage: "HTTP proxy maximum keepalive connection pool size",
+ Usage: legacyTunnelFlag("HTTP proxy maximum keepalive connection pool size"),
Value: 100,
Hidden: shouldHide,
}),
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: ingress.ProxyKeepAliveTimeoutFlag,
- Usage: "HTTP proxy timeout for closing an idle connection",
+ Usage: legacyTunnelFlag("HTTP proxy timeout for closing an idle connection"),
Value: time.Second * 90,
Hidden: shouldHide,
}),
@@ -778,13 +778,13 @@ func configureProxyFlags(shouldHide bool) []cli.Flag {
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: ingress.HTTPHostHeaderFlag,
- Usage: "Sets the HTTP Host header for the local webserver.",
+ Usage: legacyTunnelFlag("Sets the HTTP Host header for the local webserver."),
EnvVars: []string{"TUNNEL_HTTP_HOST_HEADER"},
Hidden: shouldHide,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: ingress.OriginServerNameFlag,
- Usage: "Hostname on the origin server certificate.",
+ Usage: legacyTunnelFlag("Hostname on the origin server certificate."),
EnvVars: []string{"TUNNEL_ORIGIN_SERVER_NAME"},
Hidden: shouldHide,
}),
@@ -796,19 +796,19 @@ func configureProxyFlags(shouldHide bool) []cli.Flag {
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: tlsconfig.OriginCAPoolFlag,
- Usage: "Path to the CA for the certificate of your origin. This option should be used only if your certificate is not signed by Cloudflare.",
+ Usage: legacyTunnelFlag("Path to the CA for the certificate of your origin. This option should be used only if your certificate is not signed by Cloudflare."),
EnvVars: []string{"TUNNEL_ORIGIN_CA_POOL"},
Hidden: shouldHide,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: ingress.NoTLSVerifyFlag,
- Usage: "Disables TLS verification of the certificate presented by your origin. Will allow any certificate from the origin to be accepted. Note: The connection from your machine to Cloudflare's Edge is still encrypted.",
+ Usage: legacyTunnelFlag("Disables TLS verification of the certificate presented by your origin. Will allow any certificate from the origin to be accepted. Note: The connection from your machine to Cloudflare's Edge is still encrypted."),
EnvVars: []string{"NO_TLS_VERIFY"},
Hidden: shouldHide,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: ingress.NoChunkedEncodingFlag,
- Usage: "Disables chunked transfer encoding; useful if you are running a WSGI server.",
+ Usage: legacyTunnelFlag("Disables chunked transfer encoding; useful if you are running a WSGI server."),
EnvVars: []string{"TUNNEL_NO_CHUNKED_ENCODING"},
Hidden: shouldHide,
}),
@@ -816,6 +816,15 @@ func configureProxyFlags(shouldHide bool) []cli.Flag {
return append(flags, sshFlags(shouldHide)...)
}
+func legacyTunnelFlag(msg string) string {
+ return fmt.Sprintf(
+ "%s This flag only takes effect if you define your origin with `--url` and if you do not use ingress rules."+
+ " The recommended way is to rely on ingress rules and define this property under `originRequest` as per"+
+ " https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/configuration/configuration-file/ingress",
+ msg,
+ )
+}
+
func sshFlags(shouldHide bool) []cli.Flag {
return []cli.Flag{
altsrc.NewStringFlag(&cli.StringFlag{
diff --git a/cmd/cloudflared/tunnel/subcommands.go b/cmd/cloudflared/tunnel/subcommands.go
index 8362d8f8..905c8ad6 100644
--- a/cmd/cloudflared/tunnel/subcommands.go
+++ b/cmd/cloudflared/tunnel/subcommands.go
@@ -644,7 +644,7 @@ func runCommand(c *cli.Context) error {
// Check if token is provided and if not use default tunnelID flag method
if tokenStr := c.String(TunnelTokenFlag); tokenStr != "" {
- if token, err := parseToken(tokenStr); err == nil {
+ if token, err := ParseToken(tokenStr); err == nil {
return sc.runWithCredentials(token.Credentials())
}
@@ -663,7 +663,7 @@ func runCommand(c *cli.Context) error {
}
}
-func parseToken(tokenStr string) (*connection.TunnelToken, error) {
+func ParseToken(tokenStr string) (*connection.TunnelToken, error) {
content, err := base64.StdEncoding.DecodeString(tokenStr)
if err != nil {
return nil, err
diff --git a/cmd/cloudflared/tunnel/subcommands_test.go b/cmd/cloudflared/tunnel/subcommands_test.go
index 81f542c7..2016fe6d 100644
--- a/cmd/cloudflared/tunnel/subcommands_test.go
+++ b/cmd/cloudflared/tunnel/subcommands_test.go
@@ -183,7 +183,7 @@ func Test_validateHostname(t *testing.T) {
}
func Test_TunnelToken(t *testing.T) {
- token, err := parseToken("aabc")
+ token, err := ParseToken("aabc")
require.Error(t, err)
require.Nil(t, token)
@@ -198,7 +198,7 @@ func Test_TunnelToken(t *testing.T) {
token64 := base64.StdEncoding.EncodeToString(tokenJsonStr)
- token, err = parseToken(token64)
+ token, err = ParseToken(token64)
require.NoError(t, err)
require.Equal(t, token, expectedToken)
}
diff --git a/cmd/cloudflared/windows_service.go b/cmd/cloudflared/windows_service.go
index 2eba4780..6006c0b0 100644
--- a/cmd/cloudflared/windows_service.go
+++ b/cmd/cloudflared/windows_service.go
@@ -193,8 +193,15 @@ func installWindowsService(c *cli.Context) error {
s.Close()
return fmt.Errorf("Service %s already exists", windowsServiceName)
}
+ extraArgs, err := getServiceExtraArgsFromCliArgs(c, &log)
+ if err != nil {
+ errMsg := "Unable to determine extra arguments for windows service"
+ log.Err(err).Msg(errMsg)
+ return errors.Wrap(err, errMsg)
+ }
+
config := mgr.Config{StartType: mgr.StartAutomatic, DisplayName: windowsServiceDescription}
- s, err = m.CreateService(windowsServiceName, exepath, config)
+ s, err = m.CreateService(windowsServiceName, exepath, config, extraArgs...)
if err != nil {
return errors.Wrap(err, "Cannot install service")
}
diff --git a/component-tests/README.md b/component-tests/README.md
index 27c34783..8a76eeda 100644
--- a/component-tests/README.md
+++ b/component-tests/README.md
@@ -14,7 +14,7 @@ classic_hostname: "classic-tunnel-component-tests.example.com"
origincert: "/Users/tunnel/.cloudflared/cert.pem"
ingress:
- hostname: named-tunnel-component-tests.example.com
- service: http_status:200
+ service: hello_world
- service: http_status:404
```
diff --git a/component-tests/config.py b/component-tests/config.py
index dc438149..f53732f2 100644
--- a/component-tests/config.py
+++ b/component-tests/config.py
@@ -1,5 +1,7 @@
#!/usr/bin/env python
import copy
+import json
+import base64
from dataclasses import dataclass, InitVar
@@ -61,6 +63,23 @@ class NamedTunnelConfig(NamedTunnelBaseConfig):
def get_url(self):
return "https://" + self.ingress[0]['hostname']
+ def base_config(self):
+ config = self.full_config.copy()
+
+ # removes the tunnel reference
+ del(config["tunnel"])
+ del(config["credentials-file"])
+
+ 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)
+
+ return base64.b64encode(token_json_str.encode('utf-8'))
+
@dataclass(frozen=True)
class ClassicTunnelBaseConfig(BaseConfig):
diff --git a/component-tests/test_service.py b/component-tests/test_service.py
index efc3e242..2e02db6b 100644
--- a/component-tests/test_service.py
+++ b/component-tests/test_service.py
@@ -1,5 +1,6 @@
#!/usr/bin/env python
import os
+import pathlib
import platform
import subprocess
from contextlib import contextmanager
@@ -9,7 +10,7 @@ import pytest
import test_logging
from conftest import CfdModes
-from util import start_cloudflared, wait_tunnel_ready
+from util import start_cloudflared, wait_tunnel_ready, write_config
def select_platform(plat):
@@ -43,6 +44,21 @@ class TestServiceMode:
self.launchd_service_scenario(config, assert_log_file)
+ @select_platform("Darwin")
+ @pytest.mark.skipif(os.path.exists(default_config_file()), reason=f"There is already a config file in default path")
+ def test_launchd_service_with_token(self, tmp_path, component_tests_config):
+ log_file = tmp_path / test_logging.default_log_file
+ additional_config = {
+ "logfile": str(log_file),
+ }
+ config = component_tests_config(additional_config=additional_config)
+
+ # service install doesn't install the config file but in this case we want to use some default settings
+ # so we write the base config without the tunnel credentials and ID
+ write_config(pathlib.Path(default_config_dir()), config.base_config())
+
+ self.launchd_service_scenario(config, use_token=True)
+
@select_platform("Darwin")
@pytest.mark.skipif(os.path.exists(default_config_file()), reason=f"There is already a config file in default path")
def test_launchd_service_rotating_log(self, tmp_path, component_tests_config):
@@ -60,12 +76,13 @@ class TestServiceMode:
self.launchd_service_scenario(config, assert_rotating_log)
- def launchd_service_scenario(self, config, extra_assertions):
- with self.run_service(Path(default_config_dir()), config):
+ def launchd_service_scenario(self, config, extra_assertions=None, use_token=False):
+ with self.run_service(Path(default_config_dir()), config, use_token=use_token):
self.launchctl_cmd("list")
self.launchctl_cmd("start")
wait_tunnel_ready(tunnel_url=config.get_url())
- extra_assertions()
+ if extra_assertions is not None:
+ extra_assertions()
self.launchctl_cmd("stop")
os.remove(default_config_file())
@@ -105,12 +122,30 @@ class TestServiceMode:
self.sysv_service_scenario(config, tmp_path, assert_rotating_log)
- def sysv_service_scenario(self, config, tmp_path, extra_assertions):
- with self.run_service(tmp_path, config, root=True):
+ @select_platform("Linux")
+ @pytest.mark.skipif(os.path.exists("/etc/cloudflared/config.yml"),
+ reason=f"There is already a config file in default path")
+ def test_sysv_service_with_token(self, tmp_path, component_tests_config):
+ additional_config = {
+ "loglevel": "debug",
+ }
+
+ config = component_tests_config(additional_config=additional_config)
+
+ # service install doesn't install the config file but in this case we want to use some default settings
+ # so we write the base config without the tunnel credentials and ID
+ config_path = write_config(tmp_path, config.base_config())
+ subprocess.run(["sudo", "cp", config_path, "/etc/cloudflared/config.yml"], check=True)
+
+ self.sysv_service_scenario(config, tmp_path, use_token=True)
+
+ 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())
- extra_assertions()
+ if extra_assertions is not None:
+ extra_assertions()
self.sysv_cmd("stop")
# Service install copies config file to /etc/cloudflared/config.yml
@@ -118,14 +153,19 @@ class TestServiceMode:
self.sysv_cmd("status", success=False)
@contextmanager
- def run_service(self, tmp_path, config, root=False):
+ def run_service(self, tmp_path, config, root=False, use_token=False):
+ args = ["service", "install"]
+
+ if use_token:
+ args.append(config.get_token())
+
try:
service = start_cloudflared(
- tmp_path, config, cfd_args=["service", "install"], cfd_pre_args=[], capture_output=False, root=root)
+ tmp_path, config, cfd_args=args, cfd_pre_args=[], capture_output=False, root=root, skip_config_flag=use_token)
yield service
finally:
start_cloudflared(
- tmp_path, config, cfd_args=["service", "uninstall"], cfd_pre_args=[], capture_output=False, root=root)
+ tmp_path, config, cfd_args=["service", "uninstall"], cfd_pre_args=[], capture_output=False, root=root, skip_config_flag=use_token)
def launchctl_cmd(self, action, success=True):
cmd = subprocess.run(
diff --git a/component-tests/util.py b/component-tests/util.py
index 3c42d2a7..db8f925d 100644
--- a/component-tests/util.py
+++ b/component-tests/util.py
@@ -21,9 +21,14 @@ def write_config(directory, config):
def start_cloudflared(directory, config, cfd_args=["run"], cfd_pre_args=["tunnel"], new_process=False,
- allow_input=False, capture_output=True, root=False):
- config_path = write_config(directory, config.full_config)
+ allow_input=False, capture_output=True, root=False, skip_config_flag=False):
+
+ config_path = None
+ if not skip_config_flag:
+ config_path = write_config(directory, config.full_config)
+
cmd = cloudflared_cmd(config, config_path, cfd_args, cfd_pre_args, root)
+
if new_process:
return run_cloudflared_background(cmd, allow_input, capture_output)
# By setting check=True, it will raise an exception if the process exits with non-zero exit code
@@ -36,7 +41,10 @@ def cloudflared_cmd(config, config_path, cfd_args, cfd_pre_args, root):
cmd += ["sudo"]
cmd += [config.cloudflared_binary]
cmd += cfd_pre_args
- cmd += ["--config", str(config_path)]
+
+ if config_path is not None:
+ cmd += ["--config", str(config_path)]
+
cmd += cfd_args
LOGGER.info(f"Run cmd {cmd} with config {config}")
return cmd
diff --git a/connection/connection.go b/connection/connection.go
index 525c1a6e..ad14a85c 100644
--- a/connection/connection.go
+++ b/connection/connection.go
@@ -80,6 +80,7 @@ const (
TypeTCP
TypeControlStream
TypeHTTP
+ TypeConfiguration
)
// ShouldFlush returns whether this kind of connection should actively flush data
diff --git a/connection/http2.go b/connection/http2.go
index d1e78c1f..55e73ad3 100644
--- a/connection/http2.go
+++ b/connection/http2.go
@@ -2,6 +2,7 @@ package connection
import (
"context"
+ gojson "encoding/json"
"fmt"
"io"
"net"
@@ -23,6 +24,7 @@ const (
InternalTCPProxySrcHeader = "Cf-Cloudflared-Proxy-Src"
WebsocketUpgrade = "websocket"
ControlStreamUpgrade = "control-stream"
+ ConfigurationUpdate = "update-configuration"
)
var errEdgeConnectionClosed = fmt.Errorf("connection with edge closed")
@@ -100,7 +102,7 @@ func (c *HTTP2Connection) ServeHTTP(w http.ResponseWriter, r *http.Request) {
connType := determineHTTP2Type(r)
handleMissingRequestParts(connType, r)
- respWriter, err := NewHTTP2RespWriter(r, w, connType)
+ respWriter, err := NewHTTP2RespWriter(r, w, connType, c.log)
if err != nil {
c.observer.log.Error().Msg(err.Error())
return
@@ -120,6 +122,13 @@ func (c *HTTP2Connection) ServeHTTP(w http.ResponseWriter, r *http.Request) {
respWriter.WriteErrorResponse()
}
+ case TypeConfiguration:
+ fmt.Println("TYPE CONFIGURATION?")
+ if err := c.handleConfigurationUpdate(respWriter, r); err != nil {
+ c.log.Error().Err(err)
+ respWriter.WriteErrorResponse()
+ }
+
case TypeWebsocket, TypeHTTP:
stripWebsocketUpgradeHeader(r)
if err := originProxy.ProxyHTTP(respWriter, r, connType == TypeWebsocket); err != nil {
@@ -152,6 +161,26 @@ func (c *HTTP2Connection) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
+// ConfigurationUpdateBody is the representation followed by the edge to send updates to cloudflared.
+type ConfigurationUpdateBody struct {
+ Version int32 `json:"version"`
+ Config gojson.RawMessage `json:"config"`
+}
+
+func (c *HTTP2Connection) handleConfigurationUpdate(respWriter *http2RespWriter, r *http.Request) error {
+ var configBody ConfigurationUpdateBody
+ if err := json.NewDecoder(r.Body).Decode(&configBody); err != nil {
+ return err
+ }
+ resp := c.orchestrator.UpdateConfig(configBody.Version, configBody.Config)
+ bdy, err := json.Marshal(resp)
+ if err != nil {
+ return err
+ }
+ _, err = respWriter.Write(bdy)
+ return err
+}
+
func (c *HTTP2Connection) close() {
// Wait for all serve HTTP handlers to return
c.activeRequestsWG.Wait()
@@ -163,14 +192,16 @@ type http2RespWriter struct {
w http.ResponseWriter
flusher http.Flusher
shouldFlush bool
+ log *zerolog.Logger
}
-func NewHTTP2RespWriter(r *http.Request, w http.ResponseWriter, connType Type) (*http2RespWriter, error) {
+func NewHTTP2RespWriter(r *http.Request, w http.ResponseWriter, connType Type, log *zerolog.Logger) (*http2RespWriter, error) {
flusher, isFlusher := w.(http.Flusher)
if !isFlusher {
respWriter := &http2RespWriter{
- r: r.Body,
- w: w,
+ r: r.Body,
+ w: w,
+ log: log,
}
respWriter.WriteErrorResponse()
return nil, fmt.Errorf("%T doesn't implement http.Flusher", w)
@@ -181,6 +212,7 @@ func NewHTTP2RespWriter(r *http.Request, w http.ResponseWriter, connType Type) (
w: w,
flusher: flusher,
shouldFlush: connType.shouldFlush(),
+ log: log,
}, nil
}
@@ -239,7 +271,7 @@ func (rp *http2RespWriter) Write(p []byte) (n int, err error) {
// Implementer of OriginClient should make sure it doesn't write to the connection after Proxy returns
// Register a recover routine just in case.
if r := recover(); r != nil {
- println(fmt.Sprintf("Recover from http2 response writer panic, error %s", debug.Stack()))
+ rp.log.Debug().Msgf("Recover from http2 response writer panic, error %s", debug.Stack())
}
}()
n, err = rp.w.Write(p)
@@ -255,6 +287,8 @@ func (rp *http2RespWriter) Close() error {
func determineHTTP2Type(r *http.Request) Type {
switch {
+ case isConfigurationUpdate(r):
+ return TypeConfiguration
case isWebsocketUpgrade(r):
return TypeWebsocket
case IsTCPStream(r):
@@ -288,6 +322,10 @@ func isWebsocketUpgrade(r *http.Request) bool {
return r.Header.Get(InternalUpgradeHeader) == WebsocketUpgrade
}
+func isConfigurationUpdate(r *http.Request) bool {
+ return r.Header.Get(InternalUpgradeHeader) == ConfigurationUpdate
+}
+
// IsTCPStream discerns if the connection request needs a tcp stream proxy.
func IsTCPStream(r *http.Request) bool {
return r.Header.Get(InternalTCPProxySrcHeader) != ""
diff --git a/connection/http2_test.go b/connection/http2_test.go
index c067229c..384d29fb 100644
--- a/connection/http2_test.go
+++ b/connection/http2_test.go
@@ -1,6 +1,7 @@
package connection
import (
+ "bytes"
"context"
"errors"
"fmt"
@@ -53,6 +54,41 @@ func newTestHTTP2Connection() (*HTTP2Connection, net.Conn) {
), edgeConn
}
+func TestHTTP2ConfigurationSet(t *testing.T) {
+ http2Conn, edgeConn := newTestHTTP2Connection()
+
+ ctx, cancel := context.WithCancel(context.Background())
+ var wg sync.WaitGroup
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ http2Conn.Serve(ctx)
+ }()
+
+ edgeHTTP2Conn, err := testTransport.NewClientConn(edgeConn)
+ require.NoError(t, err)
+
+ endpoint := fmt.Sprintf("http://localhost:8080/ok")
+ reqBody := []byte(`{
+"version": 2,
+"config": {"warp-routing": {"enabled": true}, "originRequest" : {"connectTimeout": 10}, "ingress" : [ {"hostname": "test", "service": "https://localhost:8000" } , {"service": "http_status:404"} ]}}
+`)
+ reader := bytes.NewReader(reqBody)
+ req, err := http.NewRequestWithContext(ctx, http.MethodPut, endpoint, reader)
+ require.NoError(t, err)
+ req.Header.Set(InternalUpgradeHeader, ConfigurationUpdate)
+
+ resp, err := edgeHTTP2Conn.RoundTrip(req)
+ require.NoError(t, err)
+ require.Equal(t, http.StatusOK, resp.StatusCode)
+ bdy, err := ioutil.ReadAll(resp.Body)
+ require.NoError(t, err)
+ assert.Equal(t, `{"lastAppliedVersion":2,"err":null}`, string(bdy))
+ cancel()
+ wg.Wait()
+
+}
+
func TestServeHTTP(t *testing.T) {
tests := []testRequest{
{
diff --git a/ingress/ingress.go b/ingress/ingress.go
index 5e5f9655..801bc551 100644
--- a/ingress/ingress.go
+++ b/ingress/ingress.go
@@ -126,7 +126,7 @@ func parseSingleOriginService(c *cli.Context, allowURLFromArgs bool) (OriginServ
if err != nil {
return nil, errors.Wrap(err, "Error validating --unix-socket")
}
- return &unixSocketPath{path: path}, nil
+ return &unixSocketPath{path: path, scheme: "http"}, nil
}
u, err := url.Parse("http://localhost:8080")
return &httpService{url: u}, err
@@ -169,7 +169,10 @@ func validateIngress(ingress []config.UnvalidatedIngressRule, defaults OriginReq
if prefix := "unix:"; strings.HasPrefix(r.Service, prefix) {
// No validation necessary for unix socket filepath services
path := strings.TrimPrefix(r.Service, prefix)
- service = &unixSocketPath{path: path}
+ service = &unixSocketPath{path: path, scheme: "http"}
+ } else if prefix := "unix+tls:"; strings.HasPrefix(r.Service, prefix) {
+ path := strings.TrimPrefix(r.Service, prefix)
+ service = &unixSocketPath{path: path, scheme: "https"}
} else if prefix := "http_status:"; strings.HasPrefix(r.Service, prefix) {
status, err := strconv.Atoi(strings.TrimPrefix(r.Service, prefix))
if err != nil {
diff --git a/ingress/ingress_test.go b/ingress/ingress_test.go
index 9d09e8f8..1e999a4e 100644
--- a/ingress/ingress_test.go
+++ b/ingress/ingress_test.go
@@ -26,8 +26,21 @@ ingress:
`
ing, err := ParseIngress(MustReadIngress(rawYAML))
require.NoError(t, err)
- _, ok := ing.Rules[0].Service.(*unixSocketPath)
+ s, ok := ing.Rules[0].Service.(*unixSocketPath)
require.True(t, ok)
+ require.Equal(t, "http", s.scheme)
+}
+
+func TestParseUnixSocketTLS(t *testing.T) {
+ rawYAML := `
+ingress:
+- service: unix+tls:/tmp/echo.sock
+`
+ ing, err := ParseIngress(MustReadIngress(rawYAML))
+ require.NoError(t, err)
+ s, ok := ing.Rules[0].Service.(*unixSocketPath)
+ require.True(t, ok)
+ require.Equal(t, "https", s.scheme)
}
func Test_parseIngress(t *testing.T) {
diff --git a/ingress/origin_proxy.go b/ingress/origin_proxy.go
index 63c10137..e99e002e 100644
--- a/ingress/origin_proxy.go
+++ b/ingress/origin_proxy.go
@@ -23,7 +23,7 @@ type StreamBasedOriginProxy interface {
}
func (o *unixSocketPath) RoundTrip(req *http.Request) (*http.Response, error) {
- req.URL.Scheme = "http"
+ req.URL.Scheme = o.scheme
return o.transport.RoundTrip(req)
}
diff --git a/ingress/origin_service.go b/ingress/origin_service.go
index 116b77f0..c76c98a4 100644
--- a/ingress/origin_service.go
+++ b/ingress/origin_service.go
@@ -33,9 +33,10 @@ type OriginService interface {
start(log *zerolog.Logger, shutdownC <-chan struct{}, cfg OriginRequestConfig) error
}
-// unixSocketPath is an OriginService representing a unix socket (which accepts HTTP)
+// unixSocketPath is an OriginService representing a unix socket (which accepts HTTP or HTTPS)
type unixSocketPath struct {
path string
+ scheme string
transport *http.Transport
}
diff --git a/orchestration/orchestrator_test.go b/orchestration/orchestrator_test.go
index b4b19224..4fe6c7b1 100644
--- a/orchestration/orchestrator_test.go
+++ b/orchestration/orchestrator_test.go
@@ -332,7 +332,8 @@ func proxyHTTP(t *testing.T, originProxy connection.OriginProxy, hostname string
require.NoError(t, err)
w := httptest.NewRecorder()
- respWriter, err := connection.NewHTTP2RespWriter(req, w, connection.TypeHTTP)
+ log := zerolog.Nop()
+ respWriter, err := connection.NewHTTP2RespWriter(req, w, connection.TypeHTTP, &log)
require.NoError(t, err)
err = originProxy.ProxyHTTP(respWriter, req, false)
@@ -358,7 +359,8 @@ func proxyTCP(t *testing.T, originProxy connection.OriginProxy, originAddr strin
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://%s", originAddr), reqBody)
require.NoError(t, err)
- respWriter, err := connection.NewHTTP2RespWriter(req, w, connection.TypeTCP)
+ log := zerolog.Nop()
+ respWriter, err := connection.NewHTTP2RespWriter(req, w, connection.TypeTCP, &log)
require.NoError(t, err)
tcpReq := &connection.TCPRequest{
@@ -578,7 +580,8 @@ func TestPersistentConnection(t *testing.T) {
// ProxyHTTP will add Connection, Upgrade and Sec-Websocket-Version headers
req.Header.Add("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")
- respWriter, err := connection.NewHTTP2RespWriter(req, wsRespReadWriter, connection.TypeWebsocket)
+ log := zerolog.Nop()
+ respWriter, err := connection.NewHTTP2RespWriter(req, wsRespReadWriter, connection.TypeWebsocket, &log)
require.NoError(t, err)
err = originProxy.ProxyHTTP(respWriter, req, true)
diff --git a/websocket/websocket.go b/websocket/websocket.go
index 67c8916b..7240e368 100644
--- a/websocket/websocket.go
+++ b/websocket/websocket.go
@@ -8,6 +8,7 @@ import (
"fmt"
"io"
"net/http"
+ "runtime/debug"
"sync/atomic"
"time"
@@ -77,7 +78,7 @@ func unidirectionalStream(dst io.Writer, src io.Reader, dir string, status *bidi
// exited. In such case, we stop a possible panic from propagating upstream.
if r := recover(); r != nil {
// We handle such unexpected errors only when we detect that one side of the streaming is done.
- log.Debug().Msgf("Handled gracefully error %v in Streaming for %s", r, dir)
+ log.Debug().Msgf("Gracefully handled error %v in Streaming for %s, error %s", r, dir, debug.Stack())
}
}
}()