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:
|
The simplest way to download and install `ctrld` is to use the following installer command on any UNIX-like platform:
|
||||||
|
|
||||||
```shell
|
```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:
|
Windows user and prefer Powershell (who doesn't)? No problem, execute this command instead in administrative PowerShell:
|
||||||
```shell
|
```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)
|
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 \/
|
\/ dns forwarding proxy \/
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
ctrld [command]
|
ctrld-client [command]
|
||||||
|
|
||||||
Available Commands:
|
Available Commands:
|
||||||
run Run the DNS proxy server
|
run Run the DNS proxy server
|
||||||
@@ -109,12 +109,12 @@ Available Commands:
|
|||||||
log Manage runtime debug logs
|
log Manage runtime debug logs
|
||||||
|
|
||||||
Flags:
|
Flags:
|
||||||
-h, --help help for ctrld
|
-h, --help help for ctrld-client
|
||||||
-s, --silent do not write any log output
|
-s, --silent do not write any log output
|
||||||
-v, --verbose count verbose log output, "-v" basic logging, "-vv" debug logging
|
-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
|
## 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)
|
Windows (Admin Shell)
|
||||||
```shell
|
```shell
|
||||||
ctrld.exe run
|
ctrld-client.exe run
|
||||||
```
|
```
|
||||||
|
|
||||||
Linux or Macos
|
Linux or Macos
|
||||||
```shell
|
```shell
|
||||||
sudo ctrld run
|
sudo ctrld-client run
|
||||||
```
|
```
|
||||||
|
|
||||||
You can then run a test query using a DNS client, for example, `dig`:
|
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)
|
Windows (Admin Shell)
|
||||||
```shell
|
```shell
|
||||||
ctrld.exe start
|
ctrld-client.exe start
|
||||||
```
|
```
|
||||||
|
|
||||||
Linux or Macos
|
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
|
## 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.
|
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)
|
Windows (Admin Shell)
|
||||||
```shell
|
```shell
|
||||||
ctrld.exe service start
|
ctrld-client.exe service start
|
||||||
```
|
```
|
||||||
|
|
||||||
Linux or Macos
|
Linux or Macos
|
||||||
```shell
|
```shell
|
||||||
sudo ctrld service start
|
sudo ctrld-client service start
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
@@ -185,12 +185,12 @@ The following command will use your own personal Control D Device resolver, and
|
|||||||
|
|
||||||
Windows (Admin Shell)
|
Windows (Admin Shell)
|
||||||
```shell
|
```shell
|
||||||
ctrld.exe start --cd abcd1234
|
ctrld-client.exe start --cd abcd1234
|
||||||
```
|
```
|
||||||
|
|
||||||
Linux or Macos
|
Linux or Macos
|
||||||
```shell
|
```shell
|
||||||
sudo ctrld start --cd abcd1234
|
sudo ctrld-client start --cd abcd1234
|
||||||
```
|
```
|
||||||
|
|
||||||
Once you run the above command, the following things will happen:
|
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
|
### 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:
|
The above will start a foreground process and:
|
||||||
@@ -260,12 +260,12 @@ Enable DNS Intercept Mode if you:
|
|||||||
|
|
||||||
Windows (Admin Shell)
|
Windows (Admin Shell)
|
||||||
```shell
|
```shell
|
||||||
ctrld.exe start --intercept-mode dns --cd RESOLVER_ID_HERE
|
ctrld-client.exe start --intercept-mode dns --cd RESOLVER_ID_HERE
|
||||||
```
|
```
|
||||||
|
|
||||||
macOS
|
macOS
|
||||||
```shell
|
```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.
|
`--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)
|
Windows (Admin Shell)
|
||||||
```shell
|
```shell
|
||||||
ctrld.exe start --intercept-mode off
|
ctrld-client.exe start --intercept-mode off
|
||||||
```
|
```
|
||||||
|
|
||||||
macOS
|
macOS
|
||||||
```shell
|
```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.
|
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
|
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 = `
|
const rootShortDesc = `
|
||||||
__ .__ .___
|
__ .__ .___
|
||||||
_____/ |________| | __| _/
|
_____/ |________| | __| _/
|
||||||
@@ -96,7 +106,7 @@ func curVersion() string {
|
|||||||
// Return version directly if it's not empty and not a dev build
|
// Return version directly if it's not empty and not a dev build
|
||||||
// This avoids unnecessary commit hash concatenation for release versions
|
// This avoids unnecessary commit hash concatenation for release versions
|
||||||
if version != "" && version != "dev" {
|
if version != "" && version != "dev" {
|
||||||
return version
|
return displayVersion(version)
|
||||||
}
|
}
|
||||||
// Truncate commit hash to 7 characters for readability
|
// Truncate commit hash to 7 characters for readability
|
||||||
// Git commit hashes are typically 40 characters, but 7 is sufficient for identification
|
// 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)
|
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 {
|
func initCLI() *cobra.Command {
|
||||||
// Enable opening via explorer.exe on Windows.
|
// Enable opening via explorer.exe on Windows.
|
||||||
// See: https://github.com/spf13/cobra/issues/844.
|
// See: https://github.com/spf13/cobra/issues/844.
|
||||||
@@ -113,7 +145,7 @@ func initCLI() *cobra.Command {
|
|||||||
cobra.EnableCommandSorting = false
|
cobra.EnableCommandSorting = false
|
||||||
|
|
||||||
rootCmd := &cobra.Command{
|
rootCmd := &cobra.Command{
|
||||||
Use: "ctrld",
|
Use: cliName,
|
||||||
Short: strings.TrimLeft(rootShortDesc, "\n"),
|
Short: strings.TrimLeft(rootShortDesc, "\n"),
|
||||||
Version: appVersion,
|
Version: appVersion,
|
||||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||||
@@ -2233,16 +2265,20 @@ func goArm() string {
|
|||||||
return "5"
|
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 {
|
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.
|
// Use arm version set during build time, v5 binary can be run on higher arm version system.
|
||||||
if armVersion := goArm(); armVersion != "" {
|
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).
|
// linux/amd64 has nocgo version, to support systems that missing some libc (like openwrt).
|
||||||
if !cgoEnabled && runtime.GOOS == "linux" && runtime.GOARCH == "amd64" {
|
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)
|
dlUrl := fmt.Sprintf("%s/%s", baseUrl, dlPath)
|
||||||
if runtime.GOOS == "windows" {
|
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 {
|
func (sc *ServiceCommand) createServiceConfig() *service.Config {
|
||||||
return &service.Config{
|
return &service.Config{
|
||||||
Name: ctrldServiceName,
|
Name: ctrldServiceName,
|
||||||
DisplayName: "Control-D Helper Service",
|
DisplayName: ctrldServiceDisplayName,
|
||||||
Description: "A highly configurable, multi-protocol DNS forwarding proxy",
|
Description: "A highly configurable, multi-protocol DNS forwarding proxy",
|
||||||
Option: service.KeyValue{},
|
Option: service.KeyValue{},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func NewServiceManager() (*ServiceManager, error) {
|
|||||||
// Create a proper service configuration
|
// Create a proper service configuration
|
||||||
svcConfig := &service.Config{
|
svcConfig := &service.Config{
|
||||||
Name: ctrldServiceName,
|
Name: ctrldServiceName,
|
||||||
DisplayName: "Control-D Helper Service",
|
DisplayName: ctrldServiceDisplayName,
|
||||||
Description: "A highly configurable, multi-protocol DNS forwarding proxy",
|
Description: "A highly configurable, multi-protocol DNS forwarding proxy",
|
||||||
Option: service.KeyValue{},
|
Option: service.KeyValue{},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ func TestBasicCommandStructure(t *testing.T) {
|
|||||||
rootCmd := initCLI()
|
rootCmd := initCLI()
|
||||||
|
|
||||||
// Test that root command has basic properties
|
// 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")
|
assert.NotEmpty(t, rootCmd.Short, "Root command should have a short description")
|
||||||
|
|
||||||
// Test that root command has subcommands
|
// Test that root command has subcommands
|
||||||
@@ -46,14 +46,19 @@ func TestServiceCommandCreation(t *testing.T) {
|
|||||||
config := sc.createServiceConfig()
|
config := sc.createServiceConfig()
|
||||||
require.NotNil(t, config, "Service config should be created")
|
require.NotNil(t, config, "Service config should be created")
|
||||||
assert.Equal(t, ctrldServiceName, config.Name)
|
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)
|
assert.Equal(t, "A highly configurable, multi-protocol DNS forwarding proxy", config.Description)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestServiceCommandSubCommands tests service command sub commands
|
// TestServiceCommandSubCommands tests service command sub commands
|
||||||
func TestServiceCommandSubCommands(t *testing.T) {
|
func TestServiceCommandSubCommands(t *testing.T) {
|
||||||
rootCmd := &cobra.Command{
|
rootCmd := &cobra.Command{
|
||||||
Use: "ctrld",
|
Use: "ctrld-client",
|
||||||
Short: "DNS forwarding proxy",
|
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
|
// answers "can this binary actually run on this host", which is what rollback needs
|
||||||
// to know before making a file the installed ctrld.
|
// 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
|
// 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
|
// 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
|
// 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
|
// "executable file not found in %PATH%", and rollback would then refuse to restore a
|
||||||
// perfectly good binary.
|
// perfectly good binary.
|
||||||
func binaryVersion(path string) (string, error) {
|
func binaryVersion(path string) (string, error) {
|
||||||
@@ -343,13 +343,32 @@ func binaryVersion(path string) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("running %s --version: %w", path, err)
|
return "", fmt.Errorf("running %s --version: %w", path, err)
|
||||||
}
|
}
|
||||||
ver, found := strings.CutPrefix(strings.TrimSpace(string(out)), "ctrld version ")
|
ver, ok := parseVersionOutput(string(out))
|
||||||
if !found {
|
if !ok {
|
||||||
return "", fmt.Errorf("unexpected --version output from %s: %q", path, strings.TrimSpace(string(out)))
|
return "", fmt.Errorf("unexpected --version output from %s: %q", path, strings.TrimSpace(string(out)))
|
||||||
}
|
}
|
||||||
return ver, nil
|
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
|
// InitUpgradeCmd creates the upgrade command with proper logic
|
||||||
func InitUpgradeCmd(rootCmd *cobra.Command) *cobra.Command {
|
func InitUpgradeCmd(rootCmd *cobra.Command) *cobra.Command {
|
||||||
upgradeCmd := &cobra.Command{
|
upgradeCmd := &cobra.Command{
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -137,7 +139,7 @@ func TestRemoveBinaryWithRetry(t *testing.T) {
|
|||||||
|
|
||||||
func TestBinaryVersion(t *testing.T) {
|
func TestBinaryVersion(t *testing.T) {
|
||||||
t.Run("reports the version", func(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])
|
got, err := binaryVersion(os.Args[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
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)
|
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) {
|
func TestDoWithRetryPreservesHostnameError(t *testing.T) {
|
||||||
const hostname = "dl.controld.dev"
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -219,7 +219,7 @@ func TestDoWithFallbackClassificationEndToEnd(t *testing.T) {
|
|||||||
// and not only in the hand-built shape.
|
// and not only in the hand-built shape.
|
||||||
func TestDoWithRetryComposesHostnameAttemptFirst(t *testing.T) {
|
func TestDoWithRetryComposesHostnameAttemptFirst(t *testing.T) {
|
||||||
const hostname = "dl.controld.dev"
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-1
@@ -48,7 +48,23 @@ const (
|
|||||||
upstreamOS = upstreamPrefix + "os"
|
upstreamOS = upstreamPrefix + "os"
|
||||||
upstreamOSLocal = upstreamOS + ".local"
|
upstreamOSLocal = upstreamOS + ".local"
|
||||||
dnsWatchdogDefaultInterval = 20 * time.Second
|
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.
|
// RecoveryReason provides context for why we are waiting for recovery.
|
||||||
|
|||||||
+5
-6
@@ -41,7 +41,7 @@ type sysV struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *sysV) installed() bool {
|
func (s *sysV) installed() bool {
|
||||||
fi, err := os.Stat("/etc/init.d/ctrld")
|
fi, err := os.Stat(sysVInitScript)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,7 @@ func (s *sysV) Start() error {
|
|||||||
if !s.installed() {
|
if !s.installed() {
|
||||||
return service.ErrNotInstalled
|
return service.ErrNotInstalled
|
||||||
}
|
}
|
||||||
_, err := exec.Command("/etc/init.d/ctrld", "start").CombinedOutput()
|
_, err := exec.Command(sysVInitScript, "start").CombinedOutput()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ func (s *sysV) Stop() error {
|
|||||||
if !s.installed() {
|
if !s.installed() {
|
||||||
return service.ErrNotInstalled
|
return service.ErrNotInstalled
|
||||||
}
|
}
|
||||||
_, err := exec.Command("/etc/init.d/ctrld", "stop").CombinedOutput()
|
_, err := exec.Command(sysVInitScript, "stop").CombinedOutput()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ type systemd struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *systemd) Status() (service.Status, error) {
|
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)")) {
|
if bytes.Contains(out, []byte("/FAILURE)")) {
|
||||||
return service.StatusStopped, nil
|
return service.StatusStopped, nil
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,6 @@ func (s *systemd) Status() (service.Status, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *systemd) Start() error {
|
func (s *systemd) Start() error {
|
||||||
const systemdUnitFile = "/etc/systemd/system/ctrld.service"
|
|
||||||
f, err := os.Open(systemdUnitFile)
|
f, err := os.Open(systemdUnitFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -223,7 +222,7 @@ func checkHasElevatedPrivilege() {
|
|||||||
|
|
||||||
// unixSystemVServiceStatus checks the status of a Unix System V service
|
// unixSystemVServiceStatus checks the status of a Unix System V service
|
||||||
func unixSystemVServiceStatus() (service.Status, error) {
|
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 {
|
if err != nil {
|
||||||
return service.StatusUnknown, nil
|
return service.StatusUnknown, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const launchdPlistPath = "/Library/LaunchDaemons/ctrld.plist"
|
const launchdPlistPath = launchdPlistFile
|
||||||
|
|
||||||
// serviceConfigFileExists returns true if the launchd plist for ctrld exists on disk.
|
// 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"
|
// 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
|
// service's launch arguments. This is used when upgrading an existing installation
|
||||||
// to intercept mode without losing the existing --cd flag and other arguments.
|
// 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.
|
// using PlistBuddy for exact array reads and writes.
|
||||||
//
|
//
|
||||||
// The function is idempotent: if the flag already exists, it's a no-op.
|
// 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.
|
// serviceConfigFileExists checks common service config file locations on Linux.
|
||||||
func serviceConfigFileExists() bool {
|
func serviceConfigFileExists() bool {
|
||||||
// systemd unit file
|
// systemd unit file
|
||||||
if _, err := os.Stat("/etc/systemd/system/ctrld.service"); err == nil {
|
if _, err := os.Stat(systemdUnitFile); err == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
// SysV init script
|
// SysV init script
|
||||||
if _, err := os.Stat("/etc/init.d/ctrld"); err == nil {
|
if _, err := os.Stat(sysVInitScript); err == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import "strings"
|
|||||||
// ImagePath value, which carries the command line rather than a bare path: it may be
|
// ImagePath value, which carries the command line rather than a bare path: it may be
|
||||||
// quoted and is usually followed by arguments, e.g.
|
// 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"
|
// It returns "" when no path can be read, which callers must treat as "cannot tell"
|
||||||
// rather than "does not match".
|
// rather than "does not match".
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
// installedServiceDirMatches reports whether this executable is the installed service
|
// installedServiceDirMatches reports whether this executable is the installed service
|
||||||
// binary, by comparing its directory with the one in the service's registered ImagePath.
|
// 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
|
// 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
|
// 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
|
// says nothing about the service's health, and reporting "not ready" for it would tell
|
||||||
|
|||||||
+13
-4
@@ -5,9 +5,9 @@
|
|||||||
# - Timezone info file.
|
# - Timezone info file.
|
||||||
# - CA certs file.
|
# - CA certs file.
|
||||||
# - /etc/{passwd,group} 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
|
FROM golang:1.25-bookworm AS base
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -21,6 +21,15 @@ ARG tag=master
|
|||||||
ENV CI_COMMIT_TAG=$tag
|
ENV CI_COMMIT_TAG=$tag
|
||||||
RUN CTRLD_NO_QF=yes CGO_ENABLED=0 ./scripts/build.sh
|
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
|
FROM scratch
|
||||||
|
|
||||||
COPY --from=base /usr/share/zoneinfo /usr/share/zoneinfo
|
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/passwd /etc/passwd
|
||||||
COPY --from=base /etc/group /etc/group
|
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.
|
# - Timezone info file.
|
||||||
# - CA certs file.
|
# - CA certs file.
|
||||||
# - /etc/{passwd,group} 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
|
FROM golang:1.25-bookworm AS base
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -21,6 +21,15 @@ ARG tag=master
|
|||||||
ENV CI_COMMIT_TAG=$tag
|
ENV CI_COMMIT_TAG=$tag
|
||||||
RUN CTRLD_NO_QF=yes CGO_ENABLED=0 ./scripts/build.sh
|
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
|
FROM alpine
|
||||||
|
|
||||||
COPY --from=base /usr/share/zoneinfo /usr/share/zoneinfo
|
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/passwd /etc/passwd
|
||||||
COPY --from=base /etc/group /etc/group
|
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`:
|
The user can choose to override default value using command line `--config` or `-c`:
|
||||||
|
|
||||||
```shell
|
```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.
|
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
|
```bash
|
||||||
# Start ctrld with DNS intercept mode (auto-detects VPN search domains)
|
# 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
|
# 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
|
# 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)
|
# Run in foreground (debug)
|
||||||
ctrld run --intercept-mode dns --cd <resolver-uid>
|
ctrld-client run --intercept-mode dns --cd <resolver-uid>
|
||||||
ctrld run --intercept-mode hard --cd <resolver-uid>
|
ctrld-client run --intercept-mode hard --cd <resolver-uid>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Intercept Modes
|
### Intercept Modes
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
|
|
||||||
## Launch arguments
|
## Launch arguments
|
||||||
```shell
|
```shell
|
||||||
$ ctrld run --help
|
$ ctrld-client run --help
|
||||||
Run the DNS proxy server
|
Run the DNS proxy server
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
ctrld run [flags]
|
ctrld-client run [flags]
|
||||||
|
|
||||||
Flags:
|
Flags:
|
||||||
--base64_config string base64 encoded config
|
--base64_config string base64 encoded config
|
||||||
@@ -29,7 +29,7 @@ Global Flags:
|
|||||||
For example:
|
For example:
|
||||||
|
|
||||||
```shell
|
```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:
|
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.
|
`ctrld` can read a complete base64 encoded config via command line flag. This allows you to supply complex configurations.
|
||||||
|
|
||||||
```shell
|
```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
|
### CLI Flag
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ctrld start --firewall-mode on --intercept-mode hard
|
ctrld-client start --firewall-mode on --intercept-mode hard
|
||||||
```
|
```
|
||||||
|
|
||||||
### Remote API
|
### Remote API
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ This document outlines known issues with ctrld and their current status, workaro
|
|||||||
|
|
||||||
**Workarounds**:
|
**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.
|
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
|
**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)?
|
### What happens if ctrld crashes (SIGKILL)?
|
||||||
- pf anchor rules persist in kernel memory
|
- pf anchor rules persist in kernel memory
|
||||||
- DNS is redirected to 127.0.0.1:53 but nothing is listening → DNS breaks
|
- 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
|
- Without ctrld restart: `sudo pfctl -a com.controld.ctrld -F all` manually clears it
|
||||||
|
|
||||||
### What if another program flushes all pf rules?
|
### What if another program flushes all pf rules?
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ stable code on three surfaces:
|
|||||||
risking token leakage from other output.
|
risking token leakage from other output.
|
||||||
- **Exit code** — stage-scoped: bootstrap 30–39, listener 40–49,
|
- **Exit code** — stage-scoped: bootstrap 30–39, listener 40–49,
|
||||||
service 50–59. Unrelated existing contracts are unchanged
|
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
|
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
|
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. |
|
| `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_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. |
|
| `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_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
|
## Reading the result file
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ macOS and Linux (default service home is `/etc/controld`):
|
|||||||
sudo cat /etc/controld/provision_result.json
|
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
|
custom `homedir` config moves it accordingly; routers and mobile use their
|
||||||
platform home directory.
|
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? |
|
| Scenario | Metadata sent? | Username included? |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `ctrld start` with `--cd-org` (provisioning via `cdUIDFromProvToken`) | ✅ Full | ✅ Yes |
|
| `ctrld-client start` with `--cd-org` (provisioning via `cdUIDFromProvToken`) | ✅ Full | ✅ Yes |
|
||||||
| `ctrld run` startup (config validation / processCDFlags) | ✅ Lightweight | ❌ No |
|
| `ctrld-client run` startup (config validation / processCDFlags) | ✅ Lightweight | ❌ No |
|
||||||
| Runtime config reload (`doReloadApiConfig`) | ✅ Lightweight | ❌ No |
|
| Runtime config reload (`doReloadApiConfig`) | ✅ Lightweight | ❌ No |
|
||||||
| Runtime self-uninstall check | ✅ Lightweight | ❌ No |
|
| Runtime self-uninstall check | ✅ Lightweight | ❌ No |
|
||||||
| Runtime deactivation pin refresh | ✅ 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:
|
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:
|
Stop the current ctrld service:
|
||||||
|
|
||||||
@@ -54,24 +54,24 @@ Or uninstall the current version:
|
|||||||
ctrld uninstall
|
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.
|
> **Note**: Replace `amd64` with your platform architecture as needed.
|
||||||
|
|
||||||
Verify that the binary was updated correctly:
|
Verify that the binary was updated correctly:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
ctrld --version
|
ctrld-client --version
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected output:
|
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)**
|
**Option A: Use Remote Configuration (Recommended)**
|
||||||
1. **Export your current configuration:**
|
1. **Export your current configuration:**
|
||||||
@@ -81,22 +81,22 @@ You have two ways to start ctrld:
|
|||||||
- Log into your Control D dashboard
|
- Log into your Control D dashboard
|
||||||
- Use the remote configuration feature to upload your configuration
|
- Use the remote configuration feature to upload your configuration
|
||||||
|
|
||||||
3. **Start ctrld with remote config:**
|
3. **Start ctrld-client with remote config:**
|
||||||
```bash
|
```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**
|
**Option B: Use Local Configuration**
|
||||||
```bash
|
```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:**
|
**For dnsmasq users:**
|
||||||
1. Configure dnsmasq to use ctrld as upstream:
|
1. Configure dnsmasq to use ctrld-client as upstream:
|
||||||
```bash
|
```bash
|
||||||
# Add to dnsmasq.conf
|
# Add to dnsmasq.conf
|
||||||
no-resolv
|
no-resolv
|
||||||
@@ -118,7 +118,7 @@ sudo ctrld service start --skip_self_checks
|
|||||||
- Open DNS Manager
|
- Open DNS Manager
|
||||||
- Right-click on your server name
|
- Right-click on your server name
|
||||||
- Select "Properties" → "Forwarders" tab
|
- Select "Properties" → "Forwarders" tab
|
||||||
- Add `<ctrld listener IP>` as a forwarder
|
- Add `<ctrld-client listener IP>` as a forwarder
|
||||||
|
|
||||||
## Getting Help
|
## Getting Help
|
||||||
|
|
||||||
|
|||||||
@@ -452,7 +452,7 @@ nslookup example.com 127.0.0.1
|
|||||||
gpupdate /target:computer /force
|
gpupdate /target:computer /force
|
||||||
|
|
||||||
# Verify service registration
|
# Verify service registration
|
||||||
sc qc ctrld
|
sc qc ctrld-client
|
||||||
```
|
```
|
||||||
|
|
||||||
### Service Verification
|
### Service Verification
|
||||||
@@ -461,10 +461,10 @@ After install, verify the Windows service is correctly registered:
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Check binary path and start type
|
# Check binary path and start type
|
||||||
sc qc ctrld
|
sc qc ctrld-client
|
||||||
|
|
||||||
# Should show:
|
# 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
|
# START_TYPE: AUTO_START
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
go=${GOBIN:-go}
|
go=${GOBIN:-go}
|
||||||
executable_name="ctrld"
|
executable_name="ctrld-client"
|
||||||
os_archs=(
|
os_archs=(
|
||||||
darwin/arm64
|
darwin/arm64
|
||||||
darwin/amd64
|
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
|
exit 0
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
cat >"$bin/ctrld" <<'EOF'
|
cat >"$bin/ctrld-client" <<'EOF'
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
printf 'ctrld %s\n' "$*" >>"$CALLS"
|
printf 'ctrld-client %s\n' "$*" >>"$CALLS"
|
||||||
if [ "${FAKE_CTRLD_EXIT:-0}" = "0" ]; then
|
if [ "${FAKE_CTRLD_EXIT:-0}" = "0" ]; then
|
||||||
case " $* " in
|
case " $* " in
|
||||||
*" --cd-org="*) : >"$FAKE_PLIST" ;;
|
*" --cd-org="*) : >"$FAKE_PLIST" ;;
|
||||||
@@ -47,7 +47,7 @@ fi
|
|||||||
exit "${FAKE_CTRLD_EXIT:-0}"
|
exit "${FAKE_CTRLD_EXIT:-0}"
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
chmod +x "$bin/defaults" "$bin/launchctl" "$bin/ctrld"
|
chmod +x "$bin/defaults" "$bin/launchctl" "$bin/ctrld-client"
|
||||||
|
|
||||||
assert_contains() {
|
assert_contains() {
|
||||||
expected=$1
|
expected=$1
|
||||||
@@ -78,7 +78,7 @@ run_case() {
|
|||||||
expected_status=${6:-0}
|
expected_status=${6:-0}
|
||||||
case_dir="$fixture/$name"
|
case_dir="$fixture/$name"
|
||||||
mkdir -p "$case_dir"
|
mkdir -p "$case_dir"
|
||||||
plist="$case_dir/ctrld.plist"
|
plist="$case_dir/ctrld-client.plist"
|
||||||
prefs="$case_dir/preferences"
|
prefs="$case_dir/preferences"
|
||||||
calls="$case_dir/calls"
|
calls="$case_dir/calls"
|
||||||
output="$case_dir/output"
|
output="$case_dir/output"
|
||||||
@@ -89,12 +89,30 @@ run_case() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
sed \
|
sed \
|
||||||
-e "s|^PLIST=\"/Library/LaunchDaemons/ctrld.plist\"$|PLIST=\"$plist\"|" \
|
-e "s|^PLIST=\"/Library/LaunchDaemons/ctrld-client.plist\"$|PLIST=\"$plist\"|" \
|
||||||
-e "s|^CTRLD=\"/usr/local/bin/ctrld\"$|CTRLD=\"$bin/ctrld\"|" \
|
-e "s|^CTRLD=\"/usr/local/bin/ctrld-client\"$|CTRLD=\"$bin/ctrld-client\"|" \
|
||||||
-e "s|^PREFS=\"/Library/Managed Preferences/com.controld.ctrld\"$|PREFS=\"$prefs\"|" \
|
-e "s|^PREFS=\"/Library/Managed Preferences/com.controld.ctrld\"$|PREFS=\"$prefs\"|" \
|
||||||
"$postinstall_source" >"$postinstall"
|
"$postinstall_source" >"$postinstall"
|
||||||
chmod +x "$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
|
status=0
|
||||||
PATH="$bin:$PATH" \
|
PATH="$bin:$PATH" \
|
||||||
CALLS="$calls" \
|
CALLS="$calls" \
|
||||||
@@ -118,26 +136,26 @@ run_case() {
|
|||||||
assert_not_contains 'CTRLD_POSTINSTALL_' "$postinstall_source"
|
assert_not_contains 'CTRLD_POSTINSTALL_' "$postinstall_source"
|
||||||
|
|
||||||
case_dir=$(run_case fresh-legacy 0 0 '')
|
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"
|
assert_not_contains '--intercept-mode' "$case_dir/calls"
|
||||||
|
|
||||||
case_dir=$(run_case fresh-standard 0 1 standard)
|
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"
|
assert_not_contains '--intercept-mode' "$case_dir/calls"
|
||||||
|
|
||||||
case_dir=$(run_case fresh-intercept 0 1 intercept-dns)
|
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 '')
|
case_dir=$(run_case upgrade-legacy 1 0 '')
|
||||||
assert_contains 'launchctl load' "$case_dir/calls"
|
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)
|
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"
|
assert_not_contains 'launchctl load' "$case_dir/calls"
|
||||||
|
|
||||||
case_dir=$(run_case upgrade-intercept 1 1 intercept-dns)
|
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"
|
assert_not_contains 'launchctl load' "$case_dir/calls"
|
||||||
|
|
||||||
case_dir=$(run_case fresh-invalid 0 1 invalid)
|
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)
|
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 'WARNING: unsupported InterceptMode in managed preferences; preserving existing service mode' "$case_dir/output"
|
||||||
assert_contains 'launchctl load' "$case_dir/calls"
|
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)
|
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_contains 'ERROR: upgrade installed but managed InterceptMode could not be applied' "$case_dir/output"
|
||||||
assert_not_contains 'launchctl load' "$case_dir/calls"
|
assert_not_contains 'launchctl load' "$case_dir/calls"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user