TUN-7361: Add a label to override hostname

It might make sense for users to sometimes name their cloudflared
connectors to make identification easier than relying on hostnames that
TUN-7360 provides. This PR provides a new --label option to cloudflared
tunnel that a user could provide to give custom names to their
connectors.
This commit is contained in:
Sudarsan Reddy 2023-04-19 12:41:01 +01:00
parent 0b5b9b8297
commit e426693330
10 changed files with 58 additions and 28 deletions

View File

@ -96,6 +96,7 @@ Eg. cloudflared tunnel --url localhost:8080/.
Please note that Quick Tunnels are meant to be ephemeral and should only be used for testing purposes. Please note that Quick Tunnels are meant to be ephemeral and should only be used for testing purposes.
For production usage, we recommend creating Named Tunnels. (https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/tunnel-guide/) For production usage, we recommend creating Named Tunnels. (https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/tunnel-guide/)
` `
connectorLabelFlag = "label"
) )
var ( var (
@ -407,7 +408,14 @@ func StartServer(
} }
} }
mgmt := management.New(c.String("management-hostname"), serviceIP, clientID, logger.ManagementLogger.Log, logger.ManagementLogger) mgmt := management.New(
c.String("management-hostname"),
serviceIP,
clientID,
c.String(connectorLabelFlag),
logger.ManagementLogger.Log,
logger.ManagementLogger,
)
localRules = []ingress.Rule{ingress.NewManagementRule(mgmt)} localRules = []ingress.Rule{ingress.NewManagementRule(mgmt)}
} }
orchestrator, err := orchestration.NewOrchestrator(ctx, orchestratorConfig, tunnelConfig.Tags, localRules, tunnelConfig.Log) orchestrator, err := orchestration.NewOrchestrator(ctx, orchestratorConfig, tunnelConfig.Tags, localRules, tunnelConfig.Log)
@ -675,6 +683,11 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
Value: 4, Value: 4,
Hidden: true, Hidden: true,
}), }),
altsrc.NewStringFlag(&cli.StringFlag{
Name: connectorLabelFlag,
Usage: "Use this option to give a meaningful label to a specific connector. When a tunnel starts up, a connector id unique to the tunnel is generated. This is a uuid. To make it easier to identify a connector, we will use the hostname of the machine the tunnel is running on along with the connector ID. This option exists if one wants to have more control over what their individual connectors are called.",
Value: "",
}),
altsrc.NewDurationFlag(&cli.DurationFlag{ altsrc.NewDurationFlag(&cli.DurationFlag{
Name: "grace-period", Name: "grace-period",
Usage: "When cloudflared receives SIGINT/SIGTERM it will stop accepting new requests, wait for in-progress requests to terminate, then shutdown. Waiting for in-progress requests will timeout after this grace period, or when a second SIGTERM/SIGINT is received.", Usage: "When cloudflared receives SIGINT/SIGTERM it will stop accepting new requests, wait for in-progress requests to terminate, then shutdown. Waiting for in-progress requests will timeout after this grace period, or when a second SIGTERM/SIGINT is received.",

View File

@ -74,7 +74,7 @@ def assert_log_to_dir(config, log_dir):
class TestLogging: class TestLogging:
def test_logging_to_terminal(self, tmp_path, component_tests_config): def test_logging_to_terminal(self, tmp_path, component_tests_config):
config = component_tests_config() config = component_tests_config()
with start_cloudflared(tmp_path, config, new_process=True) as cloudflared: with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True) as cloudflared:
wait_tunnel_ready(tunnel_url=config.get_url()) wait_tunnel_ready(tunnel_url=config.get_url())
assert_log_to_terminal(cloudflared) assert_log_to_terminal(cloudflared)
@ -85,7 +85,7 @@ class TestLogging:
"logfile": str(log_file), "logfile": str(log_file),
} }
config = component_tests_config(extra_config) config = component_tests_config(extra_config)
with start_cloudflared(tmp_path, config, new_process=True, capture_output=False): with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True, capture_output=False):
wait_tunnel_ready(tunnel_url=config.get_url(), cfd_logs=str(log_file)) wait_tunnel_ready(tunnel_url=config.get_url(), cfd_logs=str(log_file))
assert_log_in_file(log_file) assert_log_in_file(log_file)
assert_json_log(log_file) assert_json_log(log_file)
@ -98,6 +98,6 @@ class TestLogging:
"log-directory": str(log_dir), "log-directory": str(log_dir),
} }
config = component_tests_config(extra_config) config = component_tests_config(extra_config)
with start_cloudflared(tmp_path, config, new_process=True, capture_output=False): with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True, capture_output=False):
wait_tunnel_ready(tunnel_url=config.get_url(), cfd_logs=str(log_dir)) wait_tunnel_ready(tunnel_url=config.get_url(), cfd_logs=str(log_dir))
assert_log_to_dir(config, log_dir) assert_log_to_dir(config, log_dir)

View File

@ -12,6 +12,6 @@ class TestPostQuantum:
def test_post_quantum(self, tmp_path, component_tests_config): def test_post_quantum(self, tmp_path, component_tests_config):
config = component_tests_config(self._extra_config()) config = component_tests_config(self._extra_config())
LOGGER.debug(config) LOGGER.debug(config)
with start_cloudflared(tmp_path, config, cfd_args=["run", "--post-quantum"], new_process=True): with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["run", "--post-quantum"], new_process=True):
wait_tunnel_ready(tunnel_url=config.get_url(), wait_tunnel_ready(tunnel_url=config.get_url(),
require_min_connections=4) require_min_connections=1)

View File

@ -25,7 +25,7 @@ def run_test_scenario(tmp_path, component_tests_config, cfd_mode, run_proxy_dns)
if cfd_mode == CfdModes.NAMED: if cfd_mode == CfdModes.NAMED:
expect_tunnel = True expect_tunnel = True
pre_args = ["tunnel"] pre_args = ["tunnel", "--ha-connections", "1"]
args = ["run"] args = ["run"]
elif cfd_mode == CfdModes.PROXY_DNS: elif cfd_mode == CfdModes.PROXY_DNS:
expect_proxy_dns = True expect_proxy_dns = True

View File

@ -7,7 +7,7 @@ class TestQuickTunnels:
def test_quick_tunnel(self, tmp_path, component_tests_config): def test_quick_tunnel(self, tmp_path, component_tests_config):
config = component_tests_config(cfd_mode=CfdModes.QUICK, run_proxy_dns=False) config = component_tests_config(cfd_mode=CfdModes.QUICK, run_proxy_dns=False)
LOGGER.debug(config) LOGGER.debug(config)
with start_cloudflared(tmp_path, config, cfd_args=["--hello-world"], new_process=True): with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["--hello-world"], new_process=True):
wait_tunnel_ready(require_min_connections=1) wait_tunnel_ready(require_min_connections=1)
url = get_quicktunnel_url() url = get_quicktunnel_url()
send_requests(url, 3, True) send_requests(url, 3, True)
@ -15,8 +15,8 @@ class TestQuickTunnels:
def test_quick_tunnel_url(self, tmp_path, component_tests_config): def test_quick_tunnel_url(self, tmp_path, component_tests_config):
config = component_tests_config(cfd_mode=CfdModes.QUICK, run_proxy_dns=False) config = component_tests_config(cfd_mode=CfdModes.QUICK, run_proxy_dns=False)
LOGGER.debug(config) LOGGER.debug(config)
with start_cloudflared(tmp_path, config, cfd_args=["--url", f"http://localhost:{METRICS_PORT}/"], new_process=True): with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["--url", f"http://localhost:{METRICS_PORT}/"], new_process=True):
wait_tunnel_ready() wait_tunnel_ready(require_min_connections=1)
url = get_quicktunnel_url() url = get_quicktunnel_url()
send_requests(url+"/ready", 3, True) send_requests(url+"/ready", 3, True)

View File

@ -13,7 +13,7 @@ from util import start_cloudflared, wait_tunnel_ready, check_tunnel_not_connecte
@flaky(max_runs=3, min_passes=1) @flaky(max_runs=3, min_passes=1)
class TestReconnect: class TestReconnect:
default_ha_conns = 4 default_ha_conns = 1
default_reconnect_secs = 15 default_reconnect_secs = 15
extra_config = { extra_config = {
"stdin-control": True, "stdin-control": True,
@ -29,7 +29,7 @@ class TestReconnect:
@pytest.mark.parametrize("protocol", protocols()) @pytest.mark.parametrize("protocol", protocols())
def test_named_reconnect(self, tmp_path, component_tests_config, protocol): def test_named_reconnect(self, tmp_path, component_tests_config, protocol):
config = component_tests_config(self._extra_config(protocol)) config = component_tests_config(self._extra_config(protocol))
with start_cloudflared(tmp_path, config, new_process=True, allow_input=True, capture_output=False) as cloudflared: with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True, allow_input=True, capture_output=False) as cloudflared:
# Repeat the test multiple times because some issues only occur after multiple reconnects # Repeat the test multiple times because some issues only occur after multiple reconnects
self.assert_reconnect(config, cloudflared, 5) self.assert_reconnect(config, cloudflared, 5)

View File

@ -34,7 +34,7 @@ class TestTermination:
def test_graceful_shutdown(self, tmp_path, component_tests_config, signal, protocol): def test_graceful_shutdown(self, tmp_path, component_tests_config, signal, protocol):
config = component_tests_config(self._extra_config(protocol)) config = component_tests_config(self._extra_config(protocol))
with start_cloudflared( with start_cloudflared(
tmp_path, config, new_process=True, capture_output=False) as cloudflared: tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True, capture_output=False) as cloudflared:
wait_tunnel_ready(tunnel_url=config.get_url()) wait_tunnel_ready(tunnel_url=config.get_url())
connected = threading.Condition() connected = threading.Condition()
@ -56,7 +56,7 @@ class TestTermination:
def test_shutdown_once_no_connection(self, tmp_path, component_tests_config, signal, protocol): def test_shutdown_once_no_connection(self, tmp_path, component_tests_config, signal, protocol):
config = component_tests_config(self._extra_config(protocol)) config = component_tests_config(self._extra_config(protocol))
with start_cloudflared( with start_cloudflared(
tmp_path, config, new_process=True, capture_output=False) as cloudflared: tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True, capture_output=False) as cloudflared:
wait_tunnel_ready(tunnel_url=config.get_url()) wait_tunnel_ready(tunnel_url=config.get_url())
connected = threading.Condition() connected = threading.Condition()
@ -76,7 +76,7 @@ class TestTermination:
def test_no_connection_shutdown(self, tmp_path, component_tests_config, signal, protocol): def test_no_connection_shutdown(self, tmp_path, component_tests_config, signal, protocol):
config = component_tests_config(self._extra_config(protocol)) config = component_tests_config(self._extra_config(protocol))
with start_cloudflared( with start_cloudflared(
tmp_path, config, new_process=True, capture_output=False) as cloudflared: tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], new_process=True, capture_output=False) as cloudflared:
wait_tunnel_ready(tunnel_url=config.get_url()) wait_tunnel_ready(tunnel_url=config.get_url())
with self.within_grace_period(): with self.within_grace_period():
self.terminate_by_signal(cloudflared, signal) self.terminate_by_signal(cloudflared, signal)

View File

@ -13,9 +13,9 @@ class TestTunnel:
def test_tunnel_hello_world(self, tmp_path, component_tests_config): def test_tunnel_hello_world(self, tmp_path, component_tests_config):
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False) config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
LOGGER.debug(config) LOGGER.debug(config)
with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True): with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["run", "--hello-world"], new_process=True):
wait_tunnel_ready(tunnel_url=config.get_url(), wait_tunnel_ready(tunnel_url=config.get_url(),
require_min_connections=4) require_min_connections=1)
""" """
test_get_host_details does the following: test_get_host_details does the following:
@ -34,9 +34,9 @@ class TestTunnel:
headers = {} headers = {}
headers["Content-Type"] = "application/json" headers["Content-Type"] = "application/json"
config_path = write_config(tmp_path, config.full_config) config_path = write_config(tmp_path, config.full_config)
with start_cloudflared(tmp_path, config, cfd_args=["run", "--hello-world"], new_process=True): with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1", "--label" , "test"], cfd_args=["run", "--hello-world"], new_process=True):
wait_tunnel_ready(tunnel_url=config.get_url(), wait_tunnel_ready(tunnel_url=config.get_url(),
require_min_connections=4) require_min_connections=1)
cfd_cli = CloudflaredCli(config, config_path, LOGGER) cfd_cli = CloudflaredCli(config, config_path, LOGGER)
access_jwt = cfd_cli.get_management_token(config, config_path) access_jwt = cfd_cli.get_management_token(config, config_path)
connector_id = cfd_cli.get_connector_id(config)[0] connector_id = cfd_cli.get_connector_id(config)[0]
@ -45,7 +45,7 @@ class TestTunnel:
# Assert response json. # Assert response json.
assert resp.status_code == 200, "Expected cloudflared to return 200 for host details" assert resp.status_code == 200, "Expected cloudflared to return 200 for host details"
assert resp.json()["hostname"] != "", "Expected cloudflared to return hostname" assert resp.json()["hostname"] == "custom:test", "Expected cloudflared to return hostname"
assert resp.json()["ip"] != "", "Expected cloudflared to return ip" assert resp.json()["ip"] != "", "Expected cloudflared to return ip"
assert resp.json()["connector_id"] == connector_id, "Expected cloudflared to return connector_id" assert resp.json()["connector_id"] == connector_id, "Expected cloudflared to return connector_id"
@ -53,8 +53,8 @@ class TestTunnel:
def test_tunnel_url(self, tmp_path, component_tests_config): def test_tunnel_url(self, tmp_path, component_tests_config):
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False) config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
LOGGER.debug(config) LOGGER.debug(config)
with start_cloudflared(tmp_path, config, cfd_args=["run", "--url", f"http://localhost:{METRICS_PORT}/"], new_process=True): with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["run", "--url", f"http://localhost:{METRICS_PORT}/"], new_process=True):
wait_tunnel_ready(require_min_connections=4) wait_tunnel_ready(require_min_connections=1)
send_requests(config.get_url()+"/ready", 3, True) send_requests(config.get_url()+"/ready", 3, True)
def test_tunnel_no_ingress(self, tmp_path, component_tests_config): def test_tunnel_no_ingress(self, tmp_path, component_tests_config):
@ -64,8 +64,8 @@ class TestTunnel:
''' '''
config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False) config = component_tests_config(cfd_mode=CfdModes.NAMED, run_proxy_dns=False, provide_ingress=False)
LOGGER.debug(config) LOGGER.debug(config)
with start_cloudflared(tmp_path, config, cfd_args=["run"], new_process=True): with start_cloudflared(tmp_path, config, cfd_pre_args=["tunnel", "--ha-connections", "1"], cfd_args=["run"], new_process=True):
wait_tunnel_ready(require_min_connections=4) wait_tunnel_ready(require_min_connections=1)
resp = send_request(config.get_url()+"/") resp = send_request(config.get_url()+"/")
assert resp.status_code == 503, "Expected cloudflared to return 503 for all requests with no ingress defined" assert resp.status_code == 503, "Expected cloudflared to return 503 for all requests with no ingress defined"
resp = send_request(config.get_url()+"/test") resp = send_request(config.get_url()+"/test")

View File

@ -2,6 +2,7 @@ package management
import ( import (
"context" "context"
"fmt"
"net" "net"
"net/http" "net/http"
"os" "os"
@ -32,8 +33,10 @@ type ManagementService struct {
// The management tunnel hostname // The management tunnel hostname
Hostname string Hostname string
// Host details related configurations
serviceIP string serviceIP string
clientID uuid.UUID clientID uuid.UUID
label string
log *zerolog.Logger log *zerolog.Logger
router chi.Router router chi.Router
@ -51,6 +54,7 @@ type ManagementService struct {
func New(managementHostname string, func New(managementHostname string,
serviceIP string, serviceIP string,
clientID uuid.UUID, clientID uuid.UUID,
label string,
log *zerolog.Logger, log *zerolog.Logger,
logger LoggerListener, logger LoggerListener,
) *ManagementService { ) *ManagementService {
@ -60,6 +64,7 @@ func New(managementHostname string,
logger: logger, logger: logger,
serviceIP: serviceIP, serviceIP: serviceIP,
clientID: clientID, clientID: clientID,
label: label,
} }
r := chi.NewRouter() r := chi.NewRouter()
r.Get("/ping", ping) r.Get("/ping", ping)
@ -94,15 +99,27 @@ func (m *ManagementService) getHostDetails(w http.ResponseWriter, r *http.Reques
if ip, err := getPrivateIP(m.serviceIP); err == nil { if ip, err := getPrivateIP(m.serviceIP); err == nil {
getHostDetailsResponse.IP = ip getHostDetailsResponse.IP = ip
} }
if hostname, err := os.Hostname(); err == nil { getHostDetailsResponse.HostName = m.getLabel()
getHostDetailsResponse.HostName = hostname
}
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200) w.WriteHeader(200)
json.NewEncoder(w).Encode(getHostDetailsResponse) json.NewEncoder(w).Encode(getHostDetailsResponse)
} }
func (m *ManagementService) getLabel() string {
if m.label != "" {
return fmt.Sprintf("custom:%s", m.label)
}
// If no label is provided we return the system hostname. This is not
// a fqdn hostname.
hostname, err := os.Hostname()
if err != nil {
return "unknown"
}
return hostname
}
// Get preferred private ip of this machine // Get preferred private ip of this machine
func getPrivateIP(addr string) (string, error) { func getPrivateIP(addr string) (string, error) {
conn, err := net.DialTimeout("tcp", addr, 1*time.Second) conn, err := net.DialTimeout("tcp", addr, 1*time.Second)

View File

@ -51,7 +51,7 @@ func TestUpdateConfiguration(t *testing.T) {
initConfig := &Config{ initConfig := &Config{
Ingress: &ingress.Ingress{}, Ingress: &ingress.Ingress{},
} }
orchestrator, err := NewOrchestrator(context.Background(), initConfig, testTags, []ingress.Rule{ingress.NewManagementRule(management.New("management.argotunnel.com", "1.1.1.1:80", uuid.Nil, &testLogger, nil))}, &testLogger) orchestrator, err := NewOrchestrator(context.Background(), initConfig, testTags, []ingress.Rule{ingress.NewManagementRule(management.New("management.argotunnel.com", "1.1.1.1:80", uuid.Nil, "", &testLogger, nil))}, &testLogger)
require.NoError(t, err) require.NoError(t, err)
initOriginProxy, err := orchestrator.GetOriginProxy() initOriginProxy, err := orchestrator.GetOriginProxy()
require.NoError(t, err) require.NoError(t, err)