feat(cfsetup): start the tunnel connector as a setup step

Setup created the tunnel, routed DNS, and wrote config.yml, but nothing
installed or started a connector for it. A one-click run therefore left the
tunnel routed-but-dead: every web hostname returned Cloudflare error 1033
(tunnel has no connector) even though the config was correct on disk.

Add a StartConnector step to the Runner seam, invoked right after the config
is written (and gated on ConfigPath, so a caller wanting only the Access
config is not forced to install a service). The ExecRunner implementation
runs `cloudflared --config <path> service install`, which installs and starts
a managed system service (systemd/launchd/Windows), and is idempotent on an
already-installed service. The orchestration — connector started, and only
after its config exists — is unit-tested against the fake Runner; the actual
service install is INTEGRATION-ONLY.

Together with the RouteDNS --overwrite-dns fix, this closes both distinct
paths to a 1033 half-state from a fresh setup: a stale DNS binding and a
missing connector.
This commit is contained in:
flyemoji committed 2026-07-01 15:11:35 +09:00
1 parent 2810fe849c
commit 7d3be64919
3 files changed
+110

No files matched your search

+16
View File
@@ -339,6 +339,13 @@ type Runner interface {
RouteDNS(ctx context.Context, tunnelID, hostname string) error
// WriteTunnelConfig persists the rendered config.yml.
WriteTunnelConfig(path string, contents []byte) error
// StartConnector installs and starts the local cloudflared connector bound to
// the written config so the tunnel actually has a running process serving it.
// Without this step the tunnel is created and the DNS is routed, but nothing
// runs the config — so every routed hostname returns Cloudflare error 1033
// (tunnel has no connector), the other half of the 1033 failure mode that
// RouteDNS's --overwrite-dns closes.
StartConnector(ctx context.Context, configPath string) error
// CreateAccessApplication creates the self-hosted Access app and returns its
// id and the issued JWT `aud` (which felis [auth] access_jwt_aud must adopt).
CreateAccessApplication(ctx context.Context, app AccessApplication) (appID, aud string, err error)
@@ -453,6 +460,15 @@ func Setup(ctx context.Context, runner Runner, p Params) (*Result, error) {
return nil, fmt.Errorf("cfsetup: write config: %w", err)
}
prog = append(prog, "Wrote "+p.ConfigPath)
// 6b. Install and start the connector for the config just written, so the
// tunnel is actually served rather than routed-but-dead (error 1033).
// Guarded by ConfigPath: with no config there is nothing to run, and a
// caller wanting only the Access config is not forced to install a service.
notify("Starting tunnel connector…")
if err := runner.StartConnector(ctx, p.ConfigPath); err != nil {
return nil, fmt.Errorf("cfsetup: start connector: %w", err)
}
prog = append(prog, "Started connector")
}
// 7. Front the admin face with a self-hosted Access app.
notify("Creating Access application…")