mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-09-04 13:36:35 +02:00
@@ -44,12 +44,12 @@ There are several ways to download and install `ctrld`.
|
||||
The simplest way to download and install `ctrld` is to use the following installer command on any UNIX-like platform:
|
||||
|
||||
```shell
|
||||
sh -c 'sh -c "$(curl -sL https://api.controld.com/dl?version=2)"'
|
||||
sh -c 'sh -c "$(curl -sL https://api.controld.com/dl?version=client)"'
|
||||
```
|
||||
|
||||
Windows user and prefer Powershell (who doesn't)? No problem, execute this command instead in administrative PowerShell:
|
||||
```shell
|
||||
(Invoke-WebRequest -Uri 'https://api.controld.com/dl/ps1?version=2' -UseBasicParsing).Content | Set-Content "$env:TEMP\ctrld_install.ps1"; Invoke-Expression "& '$env:TEMP\ctrld_install.ps1'"
|
||||
(Invoke-WebRequest -Uri 'https://api.controld.com/dl/ps1?version=client' -UseBasicParsing).Content | Set-Content "$env:TEMP\ctrld_install.ps1"; Invoke-Expression "& '$env:TEMP\ctrld_install.ps1'"
|
||||
```
|
||||
|
||||
Or you can pull and run a Docker container from [Docker Hub](https://hub.docker.com/r/controldns/ctrld)
|
||||
@@ -93,7 +93,7 @@ _/ ___\ __\_ __ \ | / __ |
|
||||
\/ dns forwarding proxy \/
|
||||
|
||||
Usage:
|
||||
ctrld [command]
|
||||
ctrld-client [command]
|
||||
|
||||
Available Commands:
|
||||
run Run the DNS proxy server
|
||||
@@ -109,12 +109,12 @@ Available Commands:
|
||||
log Manage runtime debug logs
|
||||
|
||||
Flags:
|
||||
-h, --help help for ctrld
|
||||
-h, --help help for ctrld-client
|
||||
-s, --silent do not write any log output
|
||||
-v, --verbose count verbose log output, "-v" basic logging, "-vv" debug logging
|
||||
--version version for ctrld
|
||||
--version version for ctrld-client
|
||||
|
||||
Use "ctrld [command] --help" for more information about a command.
|
||||
Use "ctrld-client [command] --help" for more information about a command.
|
||||
```
|
||||
|
||||
## Basic Run Mode
|
||||
@@ -124,12 +124,12 @@ This is the most basic way to run `ctrld`, in foreground mode. Unless you alread
|
||||
|
||||
Windows (Admin Shell)
|
||||
```shell
|
||||
ctrld.exe run
|
||||
ctrld-client.exe run
|
||||
```
|
||||
|
||||
Linux or Macos
|
||||
```shell
|
||||
sudo ctrld run
|
||||
sudo ctrld-client run
|
||||
```
|
||||
|
||||
You can then run a test query using a DNS client, for example, `dig`:
|
||||
@@ -148,17 +148,17 @@ This mode will run the application as a background system service on any Windows
|
||||
|
||||
Windows (Admin Shell)
|
||||
```shell
|
||||
ctrld.exe start
|
||||
ctrld-client.exe start
|
||||
```
|
||||
|
||||
Linux or Macos
|
||||
```
|
||||
sudo ctrld start
|
||||
sudo ctrld-client start
|
||||
```
|
||||
|
||||
If `ctrld` is not in your system path (you installed it manually), you will need to run the above commands from the directory where you installed `ctrld`.
|
||||
If `ctrld-client` is not in your system path (you installed it manually), you will need to run the above commands from the directory where you installed it.
|
||||
|
||||
In order to stop the service, and restore your DNS to original state, simply run `ctrld stop`. If you wish to stop and uninstall the service permanently, run `ctrld uninstall`.
|
||||
In order to stop the service, and restore your DNS to original state, simply run `ctrld-client stop`. If you wish to stop and uninstall the service permanently, run `ctrld-client uninstall`.
|
||||
|
||||
## Unmanaged Service Mode
|
||||
This mode functions similarly to the "Service Mode" above except it will simply start a system service and the config defined listeners, but **will not make any changes to any network interfaces**. You can then set the `ctrld` listener(s) IP on the desired network interfaces manually.
|
||||
@@ -167,12 +167,12 @@ This mode functions similarly to the "Service Mode" above except it will simply
|
||||
|
||||
Windows (Admin Shell)
|
||||
```shell
|
||||
ctrld.exe service start
|
||||
ctrld-client.exe service start
|
||||
```
|
||||
|
||||
Linux or Macos
|
||||
```shell
|
||||
sudo ctrld service start
|
||||
sudo ctrld-client service start
|
||||
```
|
||||
|
||||
## Configuration
|
||||
@@ -185,12 +185,12 @@ The following command will use your own personal Control D Device resolver, and
|
||||
|
||||
Windows (Admin Shell)
|
||||
```shell
|
||||
ctrld.exe start --cd abcd1234
|
||||
ctrld-client.exe start --cd abcd1234
|
||||
```
|
||||
|
||||
Linux or Macos
|
||||
```shell
|
||||
sudo ctrld start --cd abcd1234
|
||||
sudo ctrld-client start --cd abcd1234
|
||||
```
|
||||
|
||||
Once you run the above command, the following things will happen:
|
||||
@@ -236,7 +236,7 @@ If you're unable to use a config file, `ctrld` can be be supplied with basic con
|
||||
|
||||
### Example
|
||||
```
|
||||
ctrld run --listen=127.0.0.1:53 --primary_upstream=https://freedns.controld.com/p2 --secondary_upstream=10.0.10.1:53 --domains=*.company.int,very-secure.local --log /path/to/log.log
|
||||
ctrld-client run --listen=127.0.0.1:53 --primary_upstream=https://freedns.controld.com/p2 --secondary_upstream=10.0.10.1:53 --domains=*.company.int,very-secure.local --log /path/to/log.log
|
||||
```
|
||||
|
||||
The above will start a foreground process and:
|
||||
@@ -260,12 +260,12 @@ Enable DNS Intercept Mode if you:
|
||||
|
||||
Windows (Admin Shell)
|
||||
```shell
|
||||
ctrld.exe start --intercept-mode dns --cd RESOLVER_ID_HERE
|
||||
ctrld-client.exe start --intercept-mode dns --cd RESOLVER_ID_HERE
|
||||
```
|
||||
|
||||
macOS
|
||||
```shell
|
||||
sudo ctrld start --intercept-mode dns --cd RESOLVER_ID_HERE
|
||||
sudo ctrld-client start --intercept-mode dns --cd RESOLVER_ID_HERE
|
||||
```
|
||||
|
||||
`--intercept-mode dns` automatically detects VPN internal domains and routes them to the VPN's DNS server, while Control D handles everything else.
|
||||
@@ -274,12 +274,12 @@ To disable intercept mode on a service that already has it enabled:
|
||||
|
||||
Windows (Admin Shell)
|
||||
```shell
|
||||
ctrld.exe start --intercept-mode off
|
||||
ctrld-client.exe start --intercept-mode off
|
||||
```
|
||||
|
||||
macOS
|
||||
```shell
|
||||
sudo ctrld start --intercept-mode off
|
||||
sudo ctrld-client start --intercept-mode off
|
||||
```
|
||||
|
||||
This removes the intercept rules and reverts to standard interface-based DNS configuration.
|
||||
|
||||
+42
-6
@@ -77,6 +77,16 @@ func isNoConfigStart(cmd *cobra.Command) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// cliName is this client's identity: the command name Cobra prints in help and
|
||||
// in "--version" output, the file the release pipeline publishes
|
||||
// (scripts/build.sh executable_name), and therefore the file the self-upgrader
|
||||
// downloads.
|
||||
//
|
||||
// Referenced rather than repeated because "--version" output is parsed as well as
|
||||
// printed: binaryVersion builds its expected prefix from this, and a literal on
|
||||
// either side would let a rename break rollback silently.
|
||||
const cliName = "ctrld-client"
|
||||
|
||||
const rootShortDesc = `
|
||||
__ .__ .___
|
||||
_____/ |________| | __| _/
|
||||
@@ -96,7 +106,7 @@ func curVersion() string {
|
||||
// Return version directly if it's not empty and not a dev build
|
||||
// This avoids unnecessary commit hash concatenation for release versions
|
||||
if version != "" && version != "dev" {
|
||||
return version
|
||||
return displayVersion(version)
|
||||
}
|
||||
// Truncate commit hash to 7 characters for readability
|
||||
// Git commit hashes are typically 40 characters, but 7 is sufficient for identification
|
||||
@@ -106,6 +116,28 @@ func curVersion() string {
|
||||
return fmt.Sprintf("%s-%s", version, commit)
|
||||
}
|
||||
|
||||
// displayVersion converts the build tag into the version the client reports.
|
||||
//
|
||||
// master carries v2.x.x tags so its releases can be tracked alongside the v1.x.x
|
||||
// line still cut from the v1.0 branch, but the client presents itself as v1.x.x:
|
||||
// a v2.0.0 tag reads as v1.0.0, v2.3.1 as v1.3.1. Only master ever carries a
|
||||
// major >= 2, so the split tag spaces scope this to master by themselves and the
|
||||
// binary needs no build-time signal for which branch produced it - the v1.0
|
||||
// branch's tags have major 1 and pass through untouched, as does anything that is
|
||||
// not a semantic version ("dev", commit-suffixed builds).
|
||||
//
|
||||
// Minor, patch, prerelease and build metadata are preserved, so a v2.1.0-rc1 tag
|
||||
// stays a release candidate at v1.1.0-rc1 and isStableVersion still classifies it
|
||||
// the same way.
|
||||
func displayVersion(v string) string {
|
||||
sv, err := semver.NewVersion(v)
|
||||
if err != nil || sv.Major() < 2 {
|
||||
return v
|
||||
}
|
||||
shifted := semver.New(sv.Major()-1, sv.Minor(), sv.Patch(), sv.Prerelease(), sv.Metadata())
|
||||
return "v" + shifted.String()
|
||||
}
|
||||
|
||||
func initCLI() *cobra.Command {
|
||||
// Enable opening via explorer.exe on Windows.
|
||||
// See: https://github.com/spf13/cobra/issues/844.
|
||||
@@ -113,7 +145,7 @@ func initCLI() *cobra.Command {
|
||||
cobra.EnableCommandSorting = false
|
||||
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "ctrld",
|
||||
Use: cliName,
|
||||
Short: strings.TrimLeft(rootShortDesc, "\n"),
|
||||
Version: appVersion,
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
@@ -2233,16 +2265,20 @@ func goArm() string {
|
||||
return "5"
|
||||
}
|
||||
|
||||
// upgradeUrl returns the url for downloading new ctrld binary.
|
||||
// upgradeUrl builds the download URL for this platform's binary.
|
||||
//
|
||||
// The path carries no version segment: the v1 line publishes as "ctrld" and this
|
||||
// one as "ctrld-client", so the file name distinguishes them and they can share
|
||||
// one path.
|
||||
func upgradeUrl(baseUrl string) string {
|
||||
dlPath := fmt.Sprintf("v2/%s-%s/ctrld", runtime.GOOS, runtime.GOARCH)
|
||||
dlPath := fmt.Sprintf("%s-%s/%s", runtime.GOOS, runtime.GOARCH, cliName)
|
||||
// Use arm version set during build time, v5 binary can be run on higher arm version system.
|
||||
if armVersion := goArm(); armVersion != "" {
|
||||
dlPath = fmt.Sprintf("%s-%sv%s/ctrld", runtime.GOOS, runtime.GOARCH, armVersion)
|
||||
dlPath = fmt.Sprintf("%s-%sv%s/%s", runtime.GOOS, runtime.GOARCH, armVersion, cliName)
|
||||
}
|
||||
// linux/amd64 has nocgo version, to support systems that missing some libc (like openwrt).
|
||||
if !cgoEnabled && runtime.GOOS == "linux" && runtime.GOARCH == "amd64" {
|
||||
dlPath = fmt.Sprintf("%s-%s-nocgo/ctrld", runtime.GOOS, runtime.GOARCH)
|
||||
dlPath = fmt.Sprintf("%s-%s-nocgo/%s", runtime.GOOS, runtime.GOARCH, cliName)
|
||||
}
|
||||
dlUrl := fmt.Sprintf("%s/%s", baseUrl, dlPath)
|
||||
if runtime.GOOS == "windows" {
|
||||
|
||||
@@ -44,3 +44,55 @@ func Test_isStableVersion(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test_displayVersion pins the tag-to-display transform. master carries v2.x.x
|
||||
// tags so its releases can be tracked next to the v1.x.x line still cut from the
|
||||
// v1.0 branch, and the client reports the tag with its major decremented.
|
||||
//
|
||||
// The v1.0-branch cases are the ones that make the split safe without a
|
||||
// build-time branch signal: a major of 1 has to pass through untouched, or the
|
||||
// v1.0 line would start reporting v0.x.x.
|
||||
func Test_displayVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{"master tag", "v2.0.0", "v1.0.0"},
|
||||
{"master tag with minor and patch", "v2.3.1", "v1.3.1"},
|
||||
{"master prerelease keeps its suffix", "v2.1.0-rc1", "v1.1.0-rc1"},
|
||||
{"master tag with build metadata", "v2.1.0+build.5", "v1.1.0+build.5"},
|
||||
{"a later major still decrements by one", "v3.2.1", "v2.2.1"},
|
||||
// v1.0 branch: untouched, which is what scopes the transform to master.
|
||||
{"v1.0 branch tag", "v1.3.5", "v1.3.5"},
|
||||
{"v1.0 branch prerelease", "v1.3.5-next", "v1.3.5-next"},
|
||||
// Not semantic versions: dev and commit-suffixed builds pass through.
|
||||
{"dev", "dev", "dev"},
|
||||
{"dev with commit", "dev-abc1234", "dev-abc1234"},
|
||||
{"empty", "", ""},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := displayVersion(tc.in); got != tc.want {
|
||||
t.Errorf("displayVersion(%q) = %q, want %q", tc.in, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test_displayVersionKeepsStabilityClassification guards the coupling between the
|
||||
// transform and isStableVersion, which selects the self-upgrade channel: a tag
|
||||
// must not change from prerelease to stable (or back) by being renumbered.
|
||||
func Test_displayVersionKeepsStabilityClassification(t *testing.T) {
|
||||
for _, ver := range []string{"v2.0.0", "v2.1.0-rc1", "v1.3.5", "v1.3.5-next", "dev"} {
|
||||
ver := ver
|
||||
t.Run(ver, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got, want := isStableVersion(displayVersion(ver)), isStableVersion(ver); got != want {
|
||||
t.Errorf("isStableVersion(displayVersion(%q)) = %v, want %v", ver, got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ func NewServiceCommand() *ServiceCommand {
|
||||
func (sc *ServiceCommand) createServiceConfig() *service.Config {
|
||||
return &service.Config{
|
||||
Name: ctrldServiceName,
|
||||
DisplayName: "Control-D Helper Service",
|
||||
DisplayName: ctrldServiceDisplayName,
|
||||
Description: "A highly configurable, multi-protocol DNS forwarding proxy",
|
||||
Option: service.KeyValue{},
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ func NewServiceManager() (*ServiceManager, error) {
|
||||
// Create a proper service configuration
|
||||
svcConfig := &service.Config{
|
||||
Name: ctrldServiceName,
|
||||
DisplayName: "Control-D Helper Service",
|
||||
DisplayName: ctrldServiceDisplayName,
|
||||
Description: "A highly configurable, multi-protocol DNS forwarding proxy",
|
||||
Option: service.KeyValue{},
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ func TestBasicCommandStructure(t *testing.T) {
|
||||
rootCmd := initCLI()
|
||||
|
||||
// Test that root command has basic properties
|
||||
assert.Equal(t, "ctrld", rootCmd.Use)
|
||||
assert.Equal(t, "ctrld-client", rootCmd.Use)
|
||||
assert.NotEmpty(t, rootCmd.Short, "Root command should have a short description")
|
||||
|
||||
// Test that root command has subcommands
|
||||
@@ -46,14 +46,19 @@ func TestServiceCommandCreation(t *testing.T) {
|
||||
config := sc.createServiceConfig()
|
||||
require.NotNil(t, config, "Service config should be created")
|
||||
assert.Equal(t, ctrldServiceName, config.Name)
|
||||
assert.Equal(t, "Control-D Helper Service", config.DisplayName)
|
||||
assert.Equal(t, ctrldServiceDisplayName, config.DisplayName)
|
||||
// Windows requires service display names to be unique and rejects a second
|
||||
// registration with ERROR_DUPLICATE_SERVICE_NAME. Reusing the v1 service's
|
||||
// display name would make "ctrld-client start" unable to install on hosts
|
||||
// that still have v1 installed, so pin that they stay distinct (#565).
|
||||
assert.NotEqual(t, "Control-D Helper Service", config.DisplayName)
|
||||
assert.Equal(t, "A highly configurable, multi-protocol DNS forwarding proxy", config.Description)
|
||||
}
|
||||
|
||||
// TestServiceCommandSubCommands tests service command sub commands
|
||||
func TestServiceCommandSubCommands(t *testing.T) {
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "ctrld",
|
||||
Use: "ctrld-client",
|
||||
Short: "DNS forwarding proxy",
|
||||
}
|
||||
|
||||
|
||||
@@ -329,11 +329,11 @@ var binaryVersionFn = binaryVersion
|
||||
// answers "can this binary actually run on this host", which is what rollback needs
|
||||
// to know before making a file the installed ctrld.
|
||||
//
|
||||
// On Windows path is ctrld.exe_previous, whose extension is not in PATHEXT. That
|
||||
// On Windows path is ctrld-client.exe_previous, whose extension is not in PATHEXT. That
|
||||
// resolves because os/exec only falls back to appending PATHEXT entries when the path
|
||||
// has no extension at all (lp_windows.go findExecutable): with one present and the
|
||||
// file on disk, it is used as-is. A suffix that left no extension - renaming
|
||||
// oldBinSuffix such that the result is "ctrld_previous" - would break this probe with
|
||||
// oldBinSuffix such that the result is "ctrld-client_previous" - would break this probe with
|
||||
// "executable file not found in %PATH%", and rollback would then refuse to restore a
|
||||
// perfectly good binary.
|
||||
func binaryVersion(path string) (string, error) {
|
||||
@@ -343,13 +343,32 @@ func binaryVersion(path string) (string, error) {
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("running %s --version: %w", path, err)
|
||||
}
|
||||
ver, found := strings.CutPrefix(strings.TrimSpace(string(out)), "ctrld version ")
|
||||
if !found {
|
||||
ver, ok := parseVersionOutput(string(out))
|
||||
if !ok {
|
||||
return "", fmt.Errorf("unexpected --version output from %s: %q", path, strings.TrimSpace(string(out)))
|
||||
}
|
||||
return ver, nil
|
||||
}
|
||||
|
||||
// parseVersionOutput extracts the version from a binary's "--version" output.
|
||||
//
|
||||
// The expected prefix is built from cliName, which is also what the root command
|
||||
// is named, because Cobra renders "--version" as "<name> version <version>" from
|
||||
// that same name. Spelling the prefix out here instead would make a rename of the
|
||||
// client silently break this parser - and with it rollback, which refuses to
|
||||
// restore a previous binary whose version it cannot read. That failure mode is
|
||||
// the worst one this code has: the service is already stopped, so a wrongly
|
||||
// rejected previous binary leaves the host with no ctrld enforcement at all.
|
||||
func parseVersionOutput(out string) (string, bool) {
|
||||
// Not CutPrefix's own return: on a miss it hands back the whole input, which a
|
||||
// caller that forgot to check the bool would store as if it were a version.
|
||||
ver, ok := strings.CutPrefix(strings.TrimSpace(out), cliName+" version ")
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
return ver, true
|
||||
}
|
||||
|
||||
// InitUpgradeCmd creates the upgrade command with proper logic
|
||||
func InitUpgradeCmd(rootCmd *cobra.Command) *cobra.Command {
|
||||
upgradeCmd := &cobra.Command{
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -137,7 +139,7 @@ func TestRemoveBinaryWithRetry(t *testing.T) {
|
||||
|
||||
func TestBinaryVersion(t *testing.T) {
|
||||
t.Run("reports the version", func(t *testing.T) {
|
||||
t.Setenv(envFakeVersionOutput, "ctrld version dev-94fbd3f")
|
||||
t.Setenv(envFakeVersionOutput, cliName+" version dev-94fbd3f")
|
||||
got, err := binaryVersion(os.Args[0])
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
@@ -287,3 +289,77 @@ func TestRollbackToPreviousBinaryAbortsWhenStopFails(t *testing.T) {
|
||||
t.Errorf("binary was modified even though the stop failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestVersionOutputParsesThroughRollbackProbe ties the "--version" output the root
|
||||
// command actually produces to the parser rollback reads it with.
|
||||
//
|
||||
// These are two halves of one contract that live in different files: Cobra renders
|
||||
// "<Use> version <Version>", and binaryVersion cuts a prefix off it. Renaming the
|
||||
// client moved the first half; if the second half had kept its literal, every
|
||||
// upgrade would have logged "unknown version" and - the part that matters -
|
||||
// rollbackToPreviousBinary would have judged a perfectly good previous binary
|
||||
// "not usable" and left the host stopped with the broken one installed.
|
||||
//
|
||||
// The version output is taken from the real root command rather than assembled
|
||||
// here, so a future change to the name, the template, or the parser has to keep
|
||||
// them agreeing.
|
||||
func TestVersionOutputParsesThroughRollbackProbe(t *testing.T) {
|
||||
rootCmd := initCLI()
|
||||
rootCmd.SetVersionTemplate(rootCmd.VersionTemplate())
|
||||
|
||||
var out bytes.Buffer
|
||||
rootCmd.SetOut(&out)
|
||||
rootCmd.SetErr(&out)
|
||||
rootCmd.SetArgs([]string{"--version"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("running --version: %v", err)
|
||||
}
|
||||
|
||||
got := out.String()
|
||||
if strings.TrimSpace(got) == "" {
|
||||
t.Fatal("--version produced no output")
|
||||
}
|
||||
ver, ok := parseVersionOutput(got)
|
||||
if !ok {
|
||||
t.Fatalf("the version probe cannot parse the root command's own --version output %q; "+
|
||||
"rollback would reject a working previous binary as unusable", strings.TrimSpace(got))
|
||||
}
|
||||
if ver != appVersion {
|
||||
t.Errorf("parsed version = %q, want %q", ver, appVersion)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseVersionOutput covers the shapes the probe must accept and reject. The
|
||||
// rejected ones are what a genuinely broken previous binary produces - the
|
||||
// incident's ctrld.exe_previous printed nothing at all - and rollback depends on
|
||||
// telling those apart from a healthy binary under a new name.
|
||||
func TestParseVersionOutput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
out string
|
||||
want string
|
||||
ok bool
|
||||
}{
|
||||
{"current identity", cliName + " version v1.0.0", "v1.0.0", true},
|
||||
{"trailing newline", cliName + " version v1.0.0\n", "v1.0.0", true},
|
||||
{"dev build", cliName + " version dev-94fbd3f", "dev-94fbd3f", true},
|
||||
// The pre-rename identity: a v1-line binary is not a valid rollback target
|
||||
// for this client, and must not be read as one.
|
||||
{"previous identity", "ctrld version v1.3.5", "", false},
|
||||
{"no output", "", "", false},
|
||||
{"unrelated output", "some other program", "", false},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got, ok := parseVersionOutput(tc.out)
|
||||
if ok != tc.ok {
|
||||
t.Fatalf("parseVersionOutput(%q) ok = %v, want %v", tc.out, ok, tc.ok)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Errorf("parseVersionOutput(%q) = %q, want %q", tc.out, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ func (rt *denyingRoundTripper) RoundTrip(req *http.Request) (*http.Response, err
|
||||
|
||||
func TestDoWithRetryPreservesHostnameError(t *testing.T) {
|
||||
const hostname = "dl.controld.dev"
|
||||
req, err := http.NewRequest(http.MethodGet, "https://"+hostname+"/v2/windows-amd64/ctrld.exe", nil)
|
||||
req, err := http.NewRequest(http.MethodGet, "https://"+hostname+"/windows-amd64/ctrld-client.exe", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -219,7 +219,7 @@ func TestDoWithFallbackClassificationEndToEnd(t *testing.T) {
|
||||
// and not only in the hand-built shape.
|
||||
func TestDoWithRetryComposesHostnameAttemptFirst(t *testing.T) {
|
||||
const hostname = "dl.controld.dev"
|
||||
req, err := http.NewRequest(http.MethodGet, "https://"+hostname+"/v2/windows-amd64/ctrld.exe", nil)
|
||||
req, err := http.NewRequest(http.MethodGet, "https://"+hostname+"/windows-amd64/ctrld-client.exe", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
+17
-1
@@ -48,7 +48,23 @@ const (
|
||||
upstreamOS = upstreamPrefix + "os"
|
||||
upstreamOSLocal = upstreamOS + ".local"
|
||||
dnsWatchdogDefaultInterval = 20 * time.Second
|
||||
ctrldServiceName = "ctrld"
|
||||
ctrldServiceName = "ctrld-client"
|
||||
// ctrldServiceDisplayName must differ from the v1 service's display name
|
||||
// ("Control-D Helper Service"). Windows requires display names to be unique
|
||||
// across all installed services and fails registration with
|
||||
// ERROR_DUPLICATE_SERVICE_NAME otherwise, so reusing v1's name would make
|
||||
// "ctrld-client start" unable to install on any host that still has the v1
|
||||
// service. It moves with ctrldServiceName: both identify this service.
|
||||
ctrldServiceDisplayName = "Control-D Client Service"
|
||||
)
|
||||
|
||||
// Service-manager paths derived from ctrldServiceName. Every init system names
|
||||
// its unit after the service identifier, so these must move with it: a rename
|
||||
// that missed one would leave ctrld managing a unit it no longer installs.
|
||||
const (
|
||||
systemdUnitFile = "/etc/systemd/system/" + ctrldServiceName + ".service"
|
||||
sysVInitScript = "/etc/init.d/" + ctrldServiceName
|
||||
launchdPlistFile = "/Library/LaunchDaemons/" + ctrldServiceName + ".plist"
|
||||
)
|
||||
|
||||
// RecoveryReason provides context for why we are waiting for recovery.
|
||||
|
||||
+5
-6
@@ -41,7 +41,7 @@ type sysV struct {
|
||||
}
|
||||
|
||||
func (s *sysV) installed() bool {
|
||||
fi, err := os.Stat("/etc/init.d/ctrld")
|
||||
fi, err := os.Stat(sysVInitScript)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -53,7 +53,7 @@ func (s *sysV) Start() error {
|
||||
if !s.installed() {
|
||||
return service.ErrNotInstalled
|
||||
}
|
||||
_, err := exec.Command("/etc/init.d/ctrld", "start").CombinedOutput()
|
||||
_, err := exec.Command(sysVInitScript, "start").CombinedOutput()
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ func (s *sysV) Stop() error {
|
||||
if !s.installed() {
|
||||
return service.ErrNotInstalled
|
||||
}
|
||||
_, err := exec.Command("/etc/init.d/ctrld", "stop").CombinedOutput()
|
||||
_, err := exec.Command(sysVInitScript, "stop").CombinedOutput()
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ type systemd struct {
|
||||
}
|
||||
|
||||
func (s *systemd) Status() (service.Status, error) {
|
||||
out, _ := exec.Command("systemctl", "status", "ctrld").CombinedOutput()
|
||||
out, _ := exec.Command("systemctl", "status", ctrldServiceName).CombinedOutput()
|
||||
if bytes.Contains(out, []byte("/FAILURE)")) {
|
||||
return service.StatusStopped, nil
|
||||
}
|
||||
@@ -97,7 +97,6 @@ func (s *systemd) Status() (service.Status, error) {
|
||||
}
|
||||
|
||||
func (s *systemd) Start() error {
|
||||
const systemdUnitFile = "/etc/systemd/system/ctrld.service"
|
||||
f, err := os.Open(systemdUnitFile)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -223,7 +222,7 @@ func checkHasElevatedPrivilege() {
|
||||
|
||||
// unixSystemVServiceStatus checks the status of a Unix System V service
|
||||
func unixSystemVServiceStatus() (service.Status, error) {
|
||||
out, err := exec.Command("/etc/init.d/ctrld", "status").CombinedOutput()
|
||||
out, err := exec.Command(sysVInitScript, "status").CombinedOutput()
|
||||
if err != nil {
|
||||
return service.StatusUnknown, nil
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const launchdPlistPath = "/Library/LaunchDaemons/ctrld.plist"
|
||||
const launchdPlistPath = launchdPlistFile
|
||||
|
||||
// serviceConfigFileExists returns true if the launchd plist for ctrld exists on disk.
|
||||
// This is more reliable than checking launchctl status, which may report "not found"
|
||||
@@ -23,7 +23,7 @@ func serviceConfigFileExists() bool {
|
||||
// service's launch arguments. This is used when upgrading an existing installation
|
||||
// to intercept mode without losing the existing --cd flag and other arguments.
|
||||
//
|
||||
// On macOS, this modifies the launchd plist at /Library/LaunchDaemons/ctrld.plist
|
||||
// On macOS, this modifies the launchd plist named after ctrldServiceName
|
||||
// using PlistBuddy for exact array reads and writes.
|
||||
//
|
||||
// The function is idempotent: if the flag already exists, it's a no-op.
|
||||
|
||||
@@ -14,11 +14,11 @@ var errServiceFlagsUnsupported = errors.New("modifying service flags is not supp
|
||||
// serviceConfigFileExists checks common service config file locations on Linux.
|
||||
func serviceConfigFileExists() bool {
|
||||
// systemd unit file
|
||||
if _, err := os.Stat("/etc/systemd/system/ctrld.service"); err == nil {
|
||||
if _, err := os.Stat(systemdUnitFile); err == nil {
|
||||
return true
|
||||
}
|
||||
// SysV init script
|
||||
if _, err := os.Stat("/etc/init.d/ctrld"); err == nil {
|
||||
if _, err := os.Stat(sysVInitScript); err == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -6,7 +6,7 @@ import "strings"
|
||||
// ImagePath value, which carries the command line rather than a bare path: it may be
|
||||
// quoted and is usually followed by arguments, e.g.
|
||||
//
|
||||
// "C:\Program Files\Control D\ctrld.exe" run --config C:\...\ctrld.toml
|
||||
// "C:\Program Files\Control D\ctrld-client.exe" run --config C:\...\ctrld.toml
|
||||
//
|
||||
// It returns "" when no path can be read, which callers must treat as "cannot tell"
|
||||
// rather than "does not match".
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
// installedServiceDirMatches reports whether this executable is the installed service
|
||||
// binary, by comparing its directory with the one in the service's registered ImagePath.
|
||||
//
|
||||
// socketDir() on Windows is relative to the running executable, so a ctrld.exe run from
|
||||
// socketDir() on Windows is relative to the running executable, so a ctrld-client.exe run from
|
||||
// somewhere else - a download directory, a build tree - looks for the control socket in
|
||||
// its own directory and never finds the installed daemon's. A failed probe from there
|
||||
// says nothing about the service's health, and reporting "not ready" for it would tell
|
||||
|
||||
+13
-4
@@ -5,9 +5,9 @@
|
||||
# - Timezone info file.
|
||||
# - CA certs file.
|
||||
# - /etc/{passwd,group} file.
|
||||
# - Non-cgo ctrld binary.
|
||||
# - Non-cgo ctrld-client binary.
|
||||
#
|
||||
# CI_COMMIT_TAG is used to set the version of ctrld binary.
|
||||
# CI_COMMIT_TAG is used to set the version of the ctrld-client binary.
|
||||
FROM golang:1.25-bookworm AS base
|
||||
|
||||
WORKDIR /app
|
||||
@@ -21,6 +21,15 @@ ARG tag=master
|
||||
ENV CI_COMMIT_TAG=$tag
|
||||
RUN CTRLD_NO_QF=yes CGO_ENABLED=0 ./scripts/build.sh
|
||||
|
||||
# Resolve the arch-suffixed build artifact to one fixed path, and fail the build
|
||||
# if it is not there. A COPY glob that matches nothing does not reliably fail,
|
||||
# which is how a rename can produce an image whose ENTRYPOINT names a binary the
|
||||
# image does not contain.
|
||||
RUN set -eu; \
|
||||
set -- ctrld-client-linux-*-nocgo; \
|
||||
[ "$#" -eq 1 ] && [ -x "$1" ] || { echo >&2 "no single executable build artifact: $*"; exit 1; }; \
|
||||
mv -- "$1" /ctrld-client
|
||||
|
||||
FROM scratch
|
||||
|
||||
COPY --from=base /usr/share/zoneinfo /usr/share/zoneinfo
|
||||
@@ -28,6 +37,6 @@ COPY --from=base /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=base /etc/passwd /etc/passwd
|
||||
COPY --from=base /etc/group /etc/group
|
||||
|
||||
COPY --from=base /app/ctrld-linux-*-nocgo ctrld
|
||||
COPY --from=base /ctrld-client ctrld-client
|
||||
|
||||
ENTRYPOINT ["./ctrld", "run"]
|
||||
ENTRYPOINT ["./ctrld-client", "run"]
|
||||
|
||||
+13
-4
@@ -5,9 +5,9 @@
|
||||
# - Timezone info file.
|
||||
# - CA certs file.
|
||||
# - /etc/{passwd,group} file.
|
||||
# - Non-cgo ctrld binary.
|
||||
# - Non-cgo ctrld-client binary.
|
||||
#
|
||||
# CI_COMMIT_TAG is used to set the version of ctrld binary.
|
||||
# CI_COMMIT_TAG is used to set the version of the ctrld-client binary.
|
||||
FROM golang:1.25-bookworm AS base
|
||||
|
||||
WORKDIR /app
|
||||
@@ -21,6 +21,15 @@ ARG tag=master
|
||||
ENV CI_COMMIT_TAG=$tag
|
||||
RUN CTRLD_NO_QF=yes CGO_ENABLED=0 ./scripts/build.sh
|
||||
|
||||
# Resolve the arch-suffixed build artifact to one fixed path, and fail the build
|
||||
# if it is not there. A COPY glob that matches nothing does not reliably fail,
|
||||
# which is how a rename can produce an image whose ENTRYPOINT names a binary the
|
||||
# image does not contain.
|
||||
RUN set -eu; \
|
||||
set -- ctrld-client-linux-*-nocgo; \
|
||||
[ "$#" -eq 1 ] && [ -x "$1" ] || { echo >&2 "no single executable build artifact: $*"; exit 1; }; \
|
||||
mv -- "$1" /ctrld-client
|
||||
|
||||
FROM alpine
|
||||
|
||||
COPY --from=base /usr/share/zoneinfo /usr/share/zoneinfo
|
||||
@@ -28,6 +37,6 @@ COPY --from=base /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=base /etc/passwd /etc/passwd
|
||||
COPY --from=base /etc/group /etc/group
|
||||
|
||||
COPY --from=base /app/ctrld-linux-*-nocgo ctrld
|
||||
COPY --from=base /ctrld-client ctrld-client
|
||||
|
||||
ENTRYPOINT ["./ctrld", "run"]
|
||||
ENTRYPOINT ["./ctrld-client", "run"]
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ The config file allows for advanced configuration of the `ctrld` utility to cove
|
||||
The user can choose to override default value using command line `--config` or `-c`:
|
||||
|
||||
```shell
|
||||
ctrld run --config /path/to/myconfig.toml
|
||||
ctrld-client run --config /path/to/myconfig.toml
|
||||
```
|
||||
|
||||
If no configuration files found, a default `ctrld.toml` file will be created in the current directory.
|
||||
|
||||
@@ -26,17 +26,17 @@ DNS intercept mode works at a lower level than interface settings:
|
||||
|
||||
```bash
|
||||
# Start ctrld with DNS intercept mode (auto-detects VPN search domains)
|
||||
ctrld start --intercept-mode dns --cd <resolver-uid>
|
||||
ctrld-client start --intercept-mode dns --cd <resolver-uid>
|
||||
|
||||
# Hard intercept: all DNS through ctrld, no VPN split routing
|
||||
ctrld start --intercept-mode hard --cd <resolver-uid>
|
||||
ctrld-client start --intercept-mode hard --cd <resolver-uid>
|
||||
|
||||
# Or with a config file
|
||||
ctrld start --intercept-mode dns -c /path/to/ctrld.toml
|
||||
ctrld-client start --intercept-mode dns -c /path/to/ctrld.toml
|
||||
|
||||
# Run in foreground (debug)
|
||||
ctrld run --intercept-mode dns --cd <resolver-uid>
|
||||
ctrld run --intercept-mode hard --cd <resolver-uid>
|
||||
ctrld-client run --intercept-mode dns --cd <resolver-uid>
|
||||
ctrld-client run --intercept-mode hard --cd <resolver-uid>
|
||||
```
|
||||
|
||||
### Intercept Modes
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
|
||||
## Launch arguments
|
||||
```shell
|
||||
$ ctrld run --help
|
||||
$ ctrld-client run --help
|
||||
Run the DNS proxy server
|
||||
|
||||
Usage:
|
||||
ctrld run [flags]
|
||||
ctrld-client run [flags]
|
||||
|
||||
Flags:
|
||||
--base64_config string base64 encoded config
|
||||
@@ -29,7 +29,7 @@ Global Flags:
|
||||
For example:
|
||||
|
||||
```shell
|
||||
ctrld run --listen=127.0.0.1:53 --primary_upstream=https://freedns.controld.com/p2 --secondary_upstream=8.8.8.8:53 --domains=*.company.int,*.net --log /path/to/log.log
|
||||
ctrld-client run --listen=127.0.0.1:53 --primary_upstream=https://freedns.controld.com/p2 --secondary_upstream=8.8.8.8:53 --domains=*.company.int,*.net --log /path/to/log.log
|
||||
```
|
||||
|
||||
Above command will be translated roughly to this config:
|
||||
@@ -70,5 +70,5 @@ Only `listen` and `primary_upstream` flags are required.
|
||||
`ctrld` can read a complete base64 encoded config via command line flag. This allows you to supply complex configurations.
|
||||
|
||||
```shell
|
||||
ctrld run --base64_config="CltsaXN0ZW5lcl0KCiAgW2xpc3RlbmVyLjBdCiAgICBpcCA9ICIxMjcuMC4wLjEiCiAgICBwb3J0ID0gNTMKICAgIHJlc3RyaWN0ZWQgPSBmYWxzZQoKW25ldHdvcmtdCgogIFtuZXR3b3JrLjBdCiAgICBjaWRycyA9IFsiMC4wLjAuMC8wIl0KICAgIG5hbWUgPSAiTmV0d29yayAwIgoKW3Vwc3RyZWFtXQoKICBbdXBzdHJlYW0uMF0KICAgIGJvb3RzdHJhcF9pcCA9ICI3Ni43Ni4yLjExIgogICAgZW5kcG9pbnQgPSAiaHR0cHM6Ly9mcmVlZG5zLmNvbnRyb2xkLmNvbS9wMSIKICAgIG5hbWUgPSAiQ29udHJvbCBEIC0gQW50aS1NYWx3YXJlIgogICAgdGltZW91dCA9IDUwMDAKICAgIHR5cGUgPSAiZG9oIgoKICBbdXBzdHJlYW0uMV0KICAgIGJvb3RzdHJhcF9pcCA9ICI3Ni43Ni4yLjExIgogICAgZW5kcG9pbnQgPSAicDIuZnJlZWRucy5jb250cm9sZC5jb20iCiAgICBuYW1lID0gIkNvbnRyb2wgRCAtIE5vIEFkcyIKICAgIHRpbWVvdXQgPSAzMDAwCiAgICB0eXBlID0gImRvcSIK"
|
||||
ctrld-client run --base64_config="CltsaXN0ZW5lcl0KCiAgW2xpc3RlbmVyLjBdCiAgICBpcCA9ICIxMjcuMC4wLjEiCiAgICBwb3J0ID0gNTMKICAgIHJlc3RyaWN0ZWQgPSBmYWxzZQoKW25ldHdvcmtdCgogIFtuZXR3b3JrLjBdCiAgICBjaWRycyA9IFsiMC4wLjAuMC8wIl0KICAgIG5hbWUgPSAiTmV0d29yayAwIgoKW3Vwc3RyZWFtXQoKICBbdXBzdHJlYW0uMF0KICAgIGJvb3RzdHJhcF9pcCA9ICI3Ni43Ni4yLjExIgogICAgZW5kcG9pbnQgPSAiaHR0cHM6Ly9mcmVlZG5zLmNvbnRyb2xkLmNvbS9wMSIKICAgIG5hbWUgPSAiQ29udHJvbCBEIC0gQW50aS1NYWx3YXJlIgogICAgdGltZW91dCA9IDUwMDAKICAgIHR5cGUgPSAiZG9oIgoKICBbdXBzdHJlYW0uMV0KICAgIGJvb3RzdHJhcF9pcCA9ICI3Ni43Ni4yLjExIgogICAgZW5kcG9pbnQgPSAicDIuZnJlZWRucy5jb250cm9sZC5jb20iCiAgICBuYW1lID0gIkNvbnRyb2wgRCAtIE5vIEFkcyIKICAgIHRpbWVvdXQgPSAzMDAwCiAgICB0eXBlID0gImRvcSIK"
|
||||
```
|
||||
|
||||
@@ -34,7 +34,7 @@ IPs, direct-IP fallbacks, or alternative DNS resolvers to bypass DNS-based filte
|
||||
### CLI Flag
|
||||
|
||||
```bash
|
||||
ctrld start --firewall-mode on --intercept-mode hard
|
||||
ctrld-client start --firewall-mode on --intercept-mode hard
|
||||
```
|
||||
|
||||
### Remote API
|
||||
|
||||
@@ -14,7 +14,7 @@ This document outlines known issues with ctrld and their current status, workaro
|
||||
|
||||
**Workarounds**:
|
||||
1. **Recommended**: Upgrade your macOS system to Darwin 15.6 or later, which has been tested and verified to work correctly with ctrld self-upgrade functionality.
|
||||
2. **Alternative**: Run `ctrld upgrade prod` directly to manually upgrade ctrld to the latest version on Darwin 15.5.
|
||||
2. **Alternative**: Run `ctrld-client upgrade prod` directly to manually upgrade ctrld to the latest version on Darwin 15.5.
|
||||
|
||||
**Affected Versions**: ctrld v1.4.2 and later on macOS Darwin 15.5
|
||||
|
||||
|
||||
@@ -217,7 +217,7 @@ Both anchors appended. This is a degenerate case that shouldn't occur in practic
|
||||
### What happens if ctrld crashes (SIGKILL)?
|
||||
- pf anchor rules persist in kernel memory
|
||||
- DNS is redirected to 127.0.0.1:53 but nothing is listening → DNS breaks
|
||||
- On next `ctrld start`, we detect the stale anchor file, flush the anchor, and start fresh
|
||||
- On next `ctrld-client start`, we detect the stale anchor file, flush the anchor, and start fresh
|
||||
- Without ctrld restart: `sudo pfctl -a com.controld.ctrld -F all` manually clears it
|
||||
|
||||
### What if another program flushes all pf rules?
|
||||
|
||||
@@ -16,7 +16,7 @@ stable code on three surfaces:
|
||||
risking token leakage from other output.
|
||||
- **Exit code** — stage-scoped: bootstrap 30–39, listener 40–49,
|
||||
service 50–59. Unrelated existing contracts are unchanged
|
||||
(`ctrld status` exits 0–3; invalid deactivation pin exits 126).
|
||||
(`ctrld-client status` exits 0–3; invalid deactivation pin exits 126).
|
||||
|
||||
A customer or administrator only needs to report the code (or the whole
|
||||
output line). The table below is the maintained support mapping; it must
|
||||
@@ -31,9 +31,9 @@ stay in sync with `cmd/cli/provision_result.go` and changes in the same MR.
|
||||
| `API_DEVICE_INVALID` | bootstrap | 32 | The API reports the device/resolver no longer exists (error code 40402). ctrld self-uninstalls its service because the identity is gone server-side. | Confirm the device was deleted or re-provisioned in the dashboard; re-provision with a current token. No local evidence needed beyond the code. |
|
||||
| `LISTENER_BIND_FAILED` | listener | 41 | No listen address could be bound after all fallbacks (configured address, 0.0.0.0:53, localhost:53, port 5354, random) were exhausted. `detail.attempts` records each tried address with the UDP/TCP OS error, e.g. `address already in use` (another DNS service owns the port) or `can't assign requested address` (address not on any interface). | Read `detail.attempts`: `address already in use` → find the process owning the port (`sudo lsof -i :53 -nP`); `can't assign requested address` → the configured IP is not present on the device. Then fix the conflict or the listener config. |
|
||||
| `LISTENER_CONFIGURED_ADDR_UNAVAILABLE` | listener | 42 | An explicitly configured listener address could not be bound and configuration checks forbid falling back to another address, or (macOS intercept mode) the required explicit address is unavailable. | The configured `ip:port` in the listener config is wrong for this device or occupied. Verify the address exists on an interface and nothing else binds it; correct the config rather than expecting fallback. |
|
||||
| `SERVICE_INSTALL_FAILED` | service | 51 | The OS service manager refused to install the service (launchd/systemd/SCM registration failed). | Check OS-level constraints: permissions/elevation, MDM policy blocking daemon installation, corrupted previous install. Evidence: result file `message` (service manager error), plus `launchctl print system/ctrld` / `systemctl status ctrld` / SCM state. |
|
||||
| `SERVICE_INSTALL_FAILED` | service | 51 | The OS service manager refused to install the service (launchd/systemd/SCM registration failed). | Check OS-level constraints: permissions/elevation, MDM policy blocking daemon installation, corrupted previous install. Evidence: result file `message` (service manager error), plus `launchctl print system/ctrld-client` / `systemctl status ctrld-client` / SCM state. |
|
||||
| `SERVICE_START_FAILED` | service | 52 | The service installed but the service manager could not start it. | Check the service manager's own log for the start error, then the ctrld home dir `ctrld.log`. Often permissions or a binary quarantined by security tooling. |
|
||||
| `SERVICE_SELFCHECK_FAILED` | service | 53 | The service started but never became healthy: no fresher failure was reported by the daemon, and the post-install DNS self-check failed. The just-installed service is rolled back (uninstalled). If the daemon itself recorded a more specific failure (e.g. a listener code), that code is reported instead of this one. | Ask for the drained service log printed by `ctrld start` and the result file. If the service was running but unreachable, check host firewall rules intercepting DNS to the listener. |
|
||||
| `SERVICE_SELFCHECK_FAILED` | service | 53 | The service started but never became healthy: no fresher failure was reported by the daemon, and the post-install DNS self-check failed. The just-installed service is rolled back (uninstalled). If the daemon itself recorded a more specific failure (e.g. a listener code), that code is reported instead of this one. | Ask for the drained service log printed by `ctrld-client start` and the result file. If the service was running but unreachable, check host firewall rules intercepting DNS to the listener. |
|
||||
|
||||
## Reading the result file
|
||||
|
||||
@@ -43,7 +43,7 @@ macOS and Linux (default service home is `/etc/controld`):
|
||||
sudo cat /etc/controld/provision_result.json
|
||||
```
|
||||
|
||||
On Windows the file sits next to `ctrld.exe` in the install directory. A
|
||||
On Windows the file sits next to `ctrld-client.exe` in the install directory. A
|
||||
custom `homedir` config moves it accordingly; routers and mobile use their
|
||||
platform home directory.
|
||||
|
||||
|
||||
@@ -97,8 +97,8 @@ System metadata (OS, chassis, username, domain) is sent to the Control D API via
|
||||
|
||||
| Scenario | Metadata sent? | Username included? |
|
||||
|---|---|---|
|
||||
| `ctrld start` with `--cd-org` (provisioning via `cdUIDFromProvToken`) | ✅ Full | ✅ Yes |
|
||||
| `ctrld run` startup (config validation / processCDFlags) | ✅ Lightweight | ❌ No |
|
||||
| `ctrld-client start` with `--cd-org` (provisioning via `cdUIDFromProvToken`) | ✅ Full | ✅ Yes |
|
||||
| `ctrld-client run` startup (config validation / processCDFlags) | ✅ Lightweight | ❌ No |
|
||||
| Runtime config reload (`doReloadApiConfig`) | ✅ Lightweight | ❌ No |
|
||||
| Runtime self-uninstall check | ✅ Lightweight | ❌ No |
|
||||
| Runtime deactivation pin refresh | ✅ Lightweight | ❌ No |
|
||||
|
||||
@@ -38,9 +38,9 @@ ctrld v1.x.x will continue to be supported for router and server platforms:
|
||||
|
||||
If you're currently using ctrld v1.x.x on router or server platforms, you need to follow these steps to migrate to v2.0.0:
|
||||
|
||||
### Step 1: Downloading ctrld v2 binary
|
||||
### Step 1: Downloading the ctrld-client binary
|
||||
|
||||
To download ctrld v2.0.0, follow these steps:
|
||||
To download the new client, follow these steps:
|
||||
|
||||
Stop the current ctrld service:
|
||||
|
||||
@@ -54,24 +54,24 @@ Or uninstall the current version:
|
||||
ctrld uninstall
|
||||
```
|
||||
|
||||
Download the appropriate binary for your platform: https://dl.controld.com/v2/linux-amd64/ctrld
|
||||
Download the appropriate binary for your platform: https://dl.controld.com/linux-amd64/ctrld-client
|
||||
|
||||
> **Note**: Replace `amd64` with your platform architecture as needed.
|
||||
|
||||
Verify that the binary was updated correctly:
|
||||
|
||||
```sh
|
||||
ctrld --version
|
||||
ctrld-client --version
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
ctrld version v2.0.0
|
||||
ctrld-client version v1.0.0
|
||||
```
|
||||
|
||||
### Step 2: Start ctrld without self-checking
|
||||
### Step 2: Start ctrld-client without self-checking
|
||||
|
||||
You have two ways to start ctrld:
|
||||
You have two ways to start ctrld-client:
|
||||
|
||||
**Option A: Use Remote Configuration (Recommended)**
|
||||
1. **Export your current configuration:**
|
||||
@@ -81,22 +81,22 @@ You have two ways to start ctrld:
|
||||
- Log into your Control D dashboard
|
||||
- Use the remote configuration feature to upload your configuration
|
||||
|
||||
3. **Start ctrld with remote config:**
|
||||
3. **Start ctrld-client with remote config:**
|
||||
```bash
|
||||
sudo ctrld service start --cd=<your_uid> --skip_self_checks
|
||||
sudo ctrld-client service start --cd=<your_uid> --skip_self_checks
|
||||
```
|
||||
|
||||
> **Note**: You must use `ctrld service start` to prevent DNS being set automatically by ctrld.
|
||||
> **Note**: You must use `ctrld-client service start` to prevent DNS being set automatically by ctrld-client.
|
||||
|
||||
**Option B: Use Local Configuration**
|
||||
```bash
|
||||
sudo ctrld service start --skip_self_checks
|
||||
sudo ctrld-client service start --skip_self_checks
|
||||
```
|
||||
|
||||
### Step 3: Configure DNS Software to Use ctrld as Upstream
|
||||
### Step 3: Configure DNS Software to Use ctrld-client as Upstream
|
||||
|
||||
**For dnsmasq users:**
|
||||
1. Configure dnsmasq to use ctrld as upstream:
|
||||
1. Configure dnsmasq to use ctrld-client as upstream:
|
||||
```bash
|
||||
# Add to dnsmasq.conf
|
||||
no-resolv
|
||||
@@ -118,7 +118,7 @@ sudo ctrld service start --skip_self_checks
|
||||
- Open DNS Manager
|
||||
- Right-click on your server name
|
||||
- Select "Properties" → "Forwarders" tab
|
||||
- Add `<ctrld listener IP>` as a forwarder
|
||||
- Add `<ctrld-client listener IP>` as a forwarder
|
||||
|
||||
## Getting Help
|
||||
|
||||
|
||||
@@ -452,7 +452,7 @@ nslookup example.com 127.0.0.1
|
||||
gpupdate /target:computer /force
|
||||
|
||||
# Verify service registration
|
||||
sc qc ctrld
|
||||
sc qc ctrld-client
|
||||
```
|
||||
|
||||
### Service Verification
|
||||
@@ -461,10 +461,10 @@ After install, verify the Windows service is correctly registered:
|
||||
|
||||
```powershell
|
||||
# Check binary path and start type
|
||||
sc qc ctrld
|
||||
sc qc ctrld-client
|
||||
|
||||
# Should show:
|
||||
# BINARY_PATH_NAME: "C:\...\ctrld.exe" run --cd xxxxx --intercept-mode dns
|
||||
# BINARY_PATH_NAME: "C:\...\ctrld-client.exe" run --cd xxxxx --intercept-mode dns
|
||||
# START_TYPE: AUTO_START
|
||||
```
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
go=${GOBIN:-go}
|
||||
executable_name="ctrld"
|
||||
executable_name="ctrld-client"
|
||||
os_archs=(
|
||||
darwin/arm64
|
||||
darwin/amd64
|
||||
|
||||
Executable
+53
@@ -0,0 +1,53 @@
|
||||
#!/bin/bash
|
||||
# Build a Docker image and prove the binary its ENTRYPOINT names is actually in it.
|
||||
#
|
||||
# The container path has no compile-time link to the build: the image copies an
|
||||
# artifact whose name comes from scripts/build.sh and starts it by a name spelled
|
||||
# out in the Dockerfile. Renaming the binary once left both Dockerfiles copying a
|
||||
# glob that matched nothing and starting a file that did not exist - the image
|
||||
# built clean and only failed at "docker run". Nothing in the test suite covers
|
||||
# that, so this does.
|
||||
#
|
||||
# Usage: docker-smoke.sh [dockerfile ...] (default: both docker/Dockerfile*)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
dockerfiles=("$@")
|
||||
if [ ${#dockerfiles[@]} -eq 0 ]; then
|
||||
dockerfiles=(docker/Dockerfile docker/Dockerfile.debug)
|
||||
fi
|
||||
|
||||
repo_root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd -- "$repo_root"
|
||||
|
||||
status=0
|
||||
for dockerfile in "${dockerfiles[@]}"; do
|
||||
tag="ctrld-client-smoke:$(basename "$dockerfile" | tr '[:upper:]' '[:lower:]')"
|
||||
echo ">>> building $dockerfile as $tag"
|
||||
docker build -q -t "$tag" -f "$dockerfile" .
|
||||
|
||||
# Override the entrypoint rather than appending an argument to it: the image's
|
||||
# ENTRYPOINT is "<binary> run", so a bare "docker run <image> --version" asks the
|
||||
# run subcommand for a version and starts the proxy instead of exiting. Naming
|
||||
# the binary explicitly is also the stronger check - it fails if the file is
|
||||
# absent or not executable, which is the regression this guards.
|
||||
echo ">>> running $tag entrypoint binary with --version"
|
||||
if ! out=$(docker run --rm --entrypoint ./ctrld-client "$tag" --version 2>&1); then
|
||||
echo >&2 "FAIL: $dockerfile: could not run the image's entrypoint: $out"
|
||||
status=1
|
||||
docker image rm -f "$tag" >/dev/null 2>&1 || :
|
||||
continue
|
||||
fi
|
||||
|
||||
echo " $out"
|
||||
case "$out" in
|
||||
"ctrld-client version "*) echo " OK: $dockerfile" ;;
|
||||
*)
|
||||
echo >&2 "FAIL: $dockerfile: unexpected --version output: $out"
|
||||
status=1
|
||||
;;
|
||||
esac
|
||||
docker image rm -f "$tag" >/dev/null 2>&1 || :
|
||||
done
|
||||
|
||||
exit "$status"
|
||||
@@ -36,9 +36,9 @@ printf 'launchctl %s\n' "$*" >>"$CALLS"
|
||||
exit 0
|
||||
EOF
|
||||
|
||||
cat >"$bin/ctrld" <<'EOF'
|
||||
cat >"$bin/ctrld-client" <<'EOF'
|
||||
#!/bin/sh
|
||||
printf 'ctrld %s\n' "$*" >>"$CALLS"
|
||||
printf 'ctrld-client %s\n' "$*" >>"$CALLS"
|
||||
if [ "${FAKE_CTRLD_EXIT:-0}" = "0" ]; then
|
||||
case " $* " in
|
||||
*" --cd-org="*) : >"$FAKE_PLIST" ;;
|
||||
@@ -47,7 +47,7 @@ fi
|
||||
exit "${FAKE_CTRLD_EXIT:-0}"
|
||||
EOF
|
||||
|
||||
chmod +x "$bin/defaults" "$bin/launchctl" "$bin/ctrld"
|
||||
chmod +x "$bin/defaults" "$bin/launchctl" "$bin/ctrld-client"
|
||||
|
||||
assert_contains() {
|
||||
expected=$1
|
||||
@@ -78,7 +78,7 @@ run_case() {
|
||||
expected_status=${6:-0}
|
||||
case_dir="$fixture/$name"
|
||||
mkdir -p "$case_dir"
|
||||
plist="$case_dir/ctrld.plist"
|
||||
plist="$case_dir/ctrld-client.plist"
|
||||
prefs="$case_dir/preferences"
|
||||
calls="$case_dir/calls"
|
||||
output="$case_dir/output"
|
||||
@@ -89,12 +89,30 @@ run_case() {
|
||||
fi
|
||||
|
||||
sed \
|
||||
-e "s|^PLIST=\"/Library/LaunchDaemons/ctrld.plist\"$|PLIST=\"$plist\"|" \
|
||||
-e "s|^CTRLD=\"/usr/local/bin/ctrld\"$|CTRLD=\"$bin/ctrld\"|" \
|
||||
-e "s|^PLIST=\"/Library/LaunchDaemons/ctrld-client.plist\"$|PLIST=\"$plist\"|" \
|
||||
-e "s|^CTRLD=\"/usr/local/bin/ctrld-client\"$|CTRLD=\"$bin/ctrld-client\"|" \
|
||||
-e "s|^PREFS=\"/Library/Managed Preferences/com.controld.ctrld\"$|PREFS=\"$prefs\"|" \
|
||||
"$postinstall_source" >"$postinstall"
|
||||
chmod +x "$postinstall"
|
||||
|
||||
# A substitution that stops matching is the dangerous failure here, not a
|
||||
# noisy one: the generated script would keep the real /usr/local/bin and
|
||||
# /Library paths and the case would exercise the installed system instead of
|
||||
# this fixture. Renaming the client once already did exactly that, so check
|
||||
# that every redirection actually landed.
|
||||
for expected in "PLIST=\"$plist\"" "CTRLD=\"$bin/ctrld-client\"" "PREFS=\"$prefs\""; do
|
||||
if ! grep -Fq -- "$expected" "$postinstall"; then
|
||||
printf 'FAIL: %s: fixture substitution did not apply (%s); postinstall paths changed?\n' \
|
||||
"$name" "$expected" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
if grep -Eq '^(PLIST|CTRLD|PREFS)="(/Library|/usr/local)' "$postinstall"; then
|
||||
printf 'FAIL: %s: a real system path survived substitution\n' "$name" >&2
|
||||
grep -E '^(PLIST|CTRLD|PREFS)=' "$postinstall" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
status=0
|
||||
PATH="$bin:$PATH" \
|
||||
CALLS="$calls" \
|
||||
@@ -118,26 +136,26 @@ run_case() {
|
||||
assert_not_contains 'CTRLD_POSTINSTALL_' "$postinstall_source"
|
||||
|
||||
case_dir=$(run_case fresh-legacy 0 0 '')
|
||||
assert_contains 'ctrld start --cd-org=test-token' "$case_dir/calls"
|
||||
assert_contains 'ctrld-client start --cd-org=test-token' "$case_dir/calls"
|
||||
assert_not_contains '--intercept-mode' "$case_dir/calls"
|
||||
|
||||
case_dir=$(run_case fresh-standard 0 1 standard)
|
||||
assert_contains 'ctrld start --cd-org=test-token' "$case_dir/calls"
|
||||
assert_contains 'ctrld-client start --cd-org=test-token' "$case_dir/calls"
|
||||
assert_not_contains '--intercept-mode' "$case_dir/calls"
|
||||
|
||||
case_dir=$(run_case fresh-intercept 0 1 intercept-dns)
|
||||
assert_contains 'ctrld start --cd-org=test-token --intercept-mode dns' "$case_dir/calls"
|
||||
assert_contains 'ctrld-client start --cd-org=test-token --intercept-mode dns' "$case_dir/calls"
|
||||
|
||||
case_dir=$(run_case upgrade-legacy 1 0 '')
|
||||
assert_contains 'launchctl load' "$case_dir/calls"
|
||||
assert_not_contains 'ctrld start' "$case_dir/calls"
|
||||
assert_not_contains 'ctrld-client start' "$case_dir/calls"
|
||||
|
||||
case_dir=$(run_case upgrade-standard 1 1 standard)
|
||||
assert_contains 'ctrld start --intercept-mode off' "$case_dir/calls"
|
||||
assert_contains 'ctrld-client start --intercept-mode off' "$case_dir/calls"
|
||||
assert_not_contains 'launchctl load' "$case_dir/calls"
|
||||
|
||||
case_dir=$(run_case upgrade-intercept 1 1 intercept-dns)
|
||||
assert_contains 'ctrld start --intercept-mode dns' "$case_dir/calls"
|
||||
assert_contains 'ctrld-client start --intercept-mode dns' "$case_dir/calls"
|
||||
assert_not_contains 'launchctl load' "$case_dir/calls"
|
||||
|
||||
case_dir=$(run_case fresh-invalid 0 1 invalid)
|
||||
@@ -147,10 +165,10 @@ assert_not_contains '--intercept-mode' "$case_dir/calls"
|
||||
case_dir=$(run_case upgrade-invalid 1 1 invalid)
|
||||
assert_contains 'WARNING: unsupported InterceptMode in managed preferences; preserving existing service mode' "$case_dir/output"
|
||||
assert_contains 'launchctl load' "$case_dir/calls"
|
||||
assert_not_contains 'ctrld start' "$case_dir/calls"
|
||||
assert_not_contains 'ctrld-client start' "$case_dir/calls"
|
||||
|
||||
case_dir=$(run_case upgrade-standard-failure 1 1 standard 1 1)
|
||||
assert_contains 'ctrld start --intercept-mode off' "$case_dir/calls"
|
||||
assert_contains 'ctrld-client start --intercept-mode off' "$case_dir/calls"
|
||||
assert_contains 'ERROR: upgrade installed but managed InterceptMode could not be applied' "$case_dir/output"
|
||||
assert_not_contains 'launchctl load' "$case_dir/calls"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user