mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-02-03 22:18:39 +00:00
Preparing for v2.0.0 branch merge
This commit reverts changes from v1.4.5 to v1.4.7, to prepare for v2.0.0 branch codes. Changes includes in these releases have been included in v2.0.0 branch already. Details: Revert "feat: add --rfc1918 flag for explicit LAN client support" This reverts commit0e3f764299. Revert "Upgrade quic-go to v0.54.0" This reverts commite52402eb0c. Revert "docs: add known issues documentation for Darwin 15.5 upgrade issue" This reverts commit2133f31854. Revert "start mobile library with provision id and custom hostname." This reverts commita198a5cd65. Revert "Add OPNsense new lease file" This reverts commit7af29cfbc0. Revert ".github/workflows: bump go version to 1.24.x" This reverts commitce1a165348. Revert "fix: ensure upstream health checks can handle large DNS responses" This reverts commitfd48e6d795. Revert "refactor(prog): move network monitoring outside listener loop" This reverts commitd71d1341b6. Revert "fix: correct Windows API constants to fix domain join detection" This reverts commit21855df4af. Revert "refactor: move network monitoring to separate goroutine" This reverts commit66e2d3a40a. Revert "refactor: extract empty string filtering to reusable function" This reverts commit36a7423634. Revert "cmd/cli: ignore empty positional argument for start command" This reverts commite616091249. Revert "Avoiding Windows runners file locking issue" This reverts commit0948161529. Revert "refactor: split selfUpgradeCheck into version check and upgrade execution" This reverts commitce29b5d217. Revert "internal/router: support Ubios 4.3+" This reverts commitde24fa293e. Revert "internal/router: support Merlin Guest Network Pro VLAN" This reverts commit6663925c4d.
This commit is contained in:
committed by
Cuong Manh Le
parent
3ca559e5a4
commit
51e58b64a5
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: ["windows-latest", "ubuntu-latest", "macOS-latest"]
|
||||
go: ["1.24.x"]
|
||||
go: ["1.23.x"]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -21,6 +21,6 @@ jobs:
|
||||
- run: "go test -race ./..."
|
||||
- uses: dominikh/staticcheck-action@v1.3.1
|
||||
with:
|
||||
version: "2025.1"
|
||||
version: "2024.1.1"
|
||||
install-go: false
|
||||
cache-key: ${{ matrix.go }}
|
||||
|
||||
@@ -178,15 +178,7 @@ func RunMobile(appConfig *AppConfig, appCallback *AppCallback, stopCh chan struc
|
||||
noConfigStart = false
|
||||
homedir = appConfig.HomeDir
|
||||
verbose = appConfig.Verbose
|
||||
if appConfig.ProvisionID != "" {
|
||||
cdOrg = appConfig.ProvisionID
|
||||
}
|
||||
if appConfig.CustomHostname != "" {
|
||||
customHostname = appConfig.CustomHostname
|
||||
}
|
||||
if appConfig.CdUID != "" {
|
||||
cdUID = appConfig.CdUID
|
||||
}
|
||||
cdUID = appConfig.CdUID
|
||||
cdUpstreamProto = appConfig.UpstreamProto
|
||||
logPath = appConfig.LogPath
|
||||
run(appCallback, stopCh)
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -189,7 +188,6 @@ func initRunCmd() *cobra.Command {
|
||||
runCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`)
|
||||
_ = runCmd.Flags().MarkHidden("iface")
|
||||
runCmd.Flags().StringVarP(&cdUpstreamProto, "proto", "", ctrld.ResolverTypeDOH, `Control D upstream type, either "doh" or "doh3"`)
|
||||
runCmd.Flags().BoolVarP(&rfc1918, "rfc1918", "", false, "Listen on RFC1918 addresses when 127.0.0.1 is the only listener")
|
||||
|
||||
runCmd.FParseErrWhitelist = cobra.FParseErrWhitelist{UnknownFlags: true}
|
||||
rootCmd.AddCommand(runCmd)
|
||||
@@ -208,7 +206,6 @@ func initStartCmd() *cobra.Command {
|
||||
|
||||
NOTE: running "ctrld start" without any arguments will start already installed ctrld service.`,
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
args = filterEmptyStrings(args)
|
||||
if len(args) > 0 {
|
||||
return fmt.Errorf("'ctrld start' doesn't accept positional arguments\n" +
|
||||
"Use flags instead (e.g. --cd, --iface) or see 'ctrld start --help' for all options")
|
||||
@@ -222,7 +219,6 @@ NOTE: running "ctrld start" without any arguments will start already installed c
|
||||
sc := &service.Config{}
|
||||
*sc = *svcConfig
|
||||
osArgs := os.Args[2:]
|
||||
osArgs = filterEmptyStrings(osArgs)
|
||||
if os.Args[1] == "service" {
|
||||
osArgs = os.Args[3:]
|
||||
}
|
||||
@@ -532,7 +528,6 @@ NOTE: running "ctrld start" without any arguments will start already installed c
|
||||
startCmd.Flags().BoolVarP(&skipSelfChecks, "skip_self_checks", "", false, `Skip self checks after installing ctrld service`)
|
||||
startCmd.Flags().BoolVarP(&startOnly, "start_only", "", false, "Do not install new service")
|
||||
_ = startCmd.Flags().MarkHidden("start_only")
|
||||
startCmd.Flags().BoolVarP(&rfc1918, "rfc1918", "", false, "Listen on RFC1918 addresses when 127.0.0.1 is the only listener")
|
||||
|
||||
routerCmd := &cobra.Command{
|
||||
Use: "setup",
|
||||
@@ -571,7 +566,6 @@ NOTE: running "ctrld start" without any arguments will start already installed c
|
||||
|
||||
NOTE: running "ctrld start" without any arguments will start already installed ctrld service.`,
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
args = filterEmptyStrings(args)
|
||||
if len(args) > 0 {
|
||||
return fmt.Errorf("'ctrld start' doesn't accept positional arguments\n" +
|
||||
"Use flags instead (e.g. --cd, --iface) or see 'ctrld start --help' for all options")
|
||||
@@ -1387,11 +1381,3 @@ func initServicesCmd(commands ...*cobra.Command) *cobra.Command {
|
||||
|
||||
return serviceCmd
|
||||
}
|
||||
|
||||
// filterEmptyStrings removes empty strings from a slice of strings.
|
||||
// It returns a new slice containing only non-empty strings.
|
||||
func filterEmptyStrings(slice []string) []string {
|
||||
return slices.DeleteFunc(slice, func(s string) bool {
|
||||
return s == ""
|
||||
})
|
||||
}
|
||||
|
||||
@@ -84,7 +84,13 @@ type upstreamForResult struct {
|
||||
srcAddr string
|
||||
}
|
||||
|
||||
func (p *prog) serveDNS(listenerNum string) error {
|
||||
func (p *prog) serveDNS(mainCtx context.Context, listenerNum string) error {
|
||||
// Start network monitoring
|
||||
if err := p.monitorNetworkChanges(mainCtx); err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("Failed to start network monitoring")
|
||||
// Don't return here as we still want DNS service to run
|
||||
}
|
||||
|
||||
listenerConfig := p.cfg.Listener[listenerNum]
|
||||
// make sure ip is allocated
|
||||
if allocErr := p.allocateIP(listenerConfig.IP); allocErr != nil {
|
||||
@@ -207,8 +213,8 @@ func (p *prog) serveDNS(listenerNum string) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
// When we spawn a listener on 127.0.0.1, also spawn listeners on the RFC1918 addresses of the machine
|
||||
// if explicitly set via setting rfc1918 flag, so ctrld could receive queries from LAN clients.
|
||||
// When we spawn a listener on 127.0.0.1, also spawn listeners on the RFC1918
|
||||
// addresses of the machine. So ctrld could receive queries from LAN clients.
|
||||
if needRFC1918Listeners(listenerConfig) {
|
||||
g.Go(func() error {
|
||||
for _, addr := range ctrld.Rfc1918Addresses() {
|
||||
@@ -1039,7 +1045,7 @@ func (p *prog) queryFromSelf(ip string) bool {
|
||||
// needRFC1918Listeners reports whether ctrld need to spawn listener for RFC 1918 addresses.
|
||||
// This is helpful for non-desktop platforms to receive queries from LAN clients.
|
||||
func needRFC1918Listeners(lc *ctrld.ListenerConfig) bool {
|
||||
return rfc1918 && lc.IP == "127.0.0.1" && lc.Port == 53
|
||||
return lc.IP == "127.0.0.1" && lc.Port == 53 && !ctrld.IsDesktopPlatform()
|
||||
}
|
||||
|
||||
// ipFromARPA parses a FQDN arpa domain and return the IP address if valid.
|
||||
@@ -1181,7 +1187,7 @@ func FlushDNSCache() error {
|
||||
}
|
||||
|
||||
// monitorNetworkChanges starts monitoring for network interface changes
|
||||
func (p *prog) monitorNetworkChanges() error {
|
||||
func (p *prog) monitorNetworkChanges(ctx context.Context) error {
|
||||
mon, err := netmon.New(func(format string, args ...any) {
|
||||
// Always fetch the latest logger (and inject the prefix)
|
||||
mainLog.Load().Printf("netmon: "+format, args...)
|
||||
@@ -1400,6 +1406,9 @@ func (p *prog) checkUpstreamOnce(upstream string, uc *ctrld.UpstreamConfig) erro
|
||||
return err
|
||||
}
|
||||
|
||||
msg := new(dns.Msg)
|
||||
msg.SetQuestion(".", dns.TypeNS)
|
||||
|
||||
timeout := 1000 * time.Millisecond
|
||||
if uc.Timeout > 0 {
|
||||
timeout = time.Millisecond * time.Duration(uc.Timeout)
|
||||
@@ -1413,7 +1422,6 @@ func (p *prog) checkUpstreamOnce(upstream string, uc *ctrld.UpstreamConfig) erro
|
||||
mainLog.Load().Debug().Msgf("Rebootstrapping resolver for upstream: %s", upstream)
|
||||
|
||||
start := time.Now()
|
||||
msg := uc.VerifyMsg()
|
||||
_, err = resolver.Resolve(ctx, msg)
|
||||
duration := time.Since(start)
|
||||
|
||||
|
||||
@@ -18,13 +18,11 @@ type AppCallback struct {
|
||||
|
||||
// AppConfig allows overwriting ctrld cli flags from mobile platforms.
|
||||
type AppConfig struct {
|
||||
CdUID string
|
||||
ProvisionID string
|
||||
CustomHostname string
|
||||
HomeDir string
|
||||
UpstreamProto string
|
||||
Verbose int
|
||||
LogPath string
|
||||
CdUID string
|
||||
HomeDir string
|
||||
UpstreamProto string
|
||||
Verbose int
|
||||
LogPath string
|
||||
}
|
||||
|
||||
const (
|
||||
|
||||
@@ -39,7 +39,6 @@ var (
|
||||
skipSelfChecks bool
|
||||
cleanup bool
|
||||
startOnly bool
|
||||
rfc1918 bool
|
||||
|
||||
mainLog atomic.Pointer[zerolog.Logger]
|
||||
consoleWriter zerolog.ConsoleWriter
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package ctrld
|
||||
package cli
|
||||
|
||||
import (
|
||||
"maps"
|
||||
@@ -35,7 +35,6 @@ import (
|
||||
"github.com/Control-D-Inc/ctrld/internal/controld"
|
||||
"github.com/Control-D-Inc/ctrld/internal/dnscache"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router"
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/dnsmasq"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -329,7 +328,7 @@ func (p *prog) apiConfigReload() {
|
||||
|
||||
// Performing self-upgrade check for production version.
|
||||
if isStable {
|
||||
_ = selfUpgradeCheck(resolverConfig.Ctrld.VersionTarget, curVer, &logger)
|
||||
selfUpgradeCheck(resolverConfig.Ctrld.VersionTarget, curVer, &logger)
|
||||
}
|
||||
|
||||
if resolverConfig.DeactivationPin != nil {
|
||||
@@ -530,15 +529,6 @@ func (p *prog) run(reload bool, reloadCh chan struct{}) {
|
||||
go p.watchLinkState(ctx)
|
||||
}
|
||||
|
||||
if !reload {
|
||||
go func() {
|
||||
// Start network monitoring
|
||||
if err := p.monitorNetworkChanges(); err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("Failed to start network monitoring")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
for listenerNum := range p.cfg.Listener {
|
||||
p.cfg.Listener[listenerNum].Init()
|
||||
if !reload {
|
||||
@@ -550,7 +540,7 @@ func (p *prog) run(reload bool, reloadCh chan struct{}) {
|
||||
}
|
||||
addr := net.JoinHostPort(listenerConfig.IP, strconv.Itoa(listenerConfig.Port))
|
||||
mainLog.Load().Info().Msgf("starting DNS server on listener.%s: %s", listenerNum, addr)
|
||||
if err := p.serveDNS(listenerNum); err != nil {
|
||||
if err := p.serveDNS(ctx, listenerNum); err != nil {
|
||||
mainLog.Load().Fatal().Err(err).Msgf("unable to start dns proxy on listener.%s", listenerNum)
|
||||
}
|
||||
mainLog.Load().Debug().Msgf("end of serveDNS listener.%s: %s", listenerNum, addr)
|
||||
@@ -617,12 +607,6 @@ func (p *prog) setupClientInfoDiscover(selfIP string) {
|
||||
format := ctrld.LeaseFileFormat(p.cfg.Service.DHCPLeaseFileFormat)
|
||||
p.ciTable.AddLeaseFile(leaseFile, format)
|
||||
}
|
||||
if leaseFiles := dnsmasq.AdditionalLeaseFiles(); len(leaseFiles) > 0 {
|
||||
mainLog.Load().Debug().Msgf("watching additional lease files: %v", leaseFiles)
|
||||
for _, leaseFile := range leaseFiles {
|
||||
p.ciTable.AddLeaseFile(leaseFile, ctrld.Dnsmasq)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// runClientInfoDiscover runs the client info discover.
|
||||
@@ -1483,15 +1467,14 @@ func selfUninstallCheck(uninstallErr error, p *prog, logger zerolog.Logger) {
|
||||
}
|
||||
}
|
||||
|
||||
// shouldUpgrade checks if the version target vt is greater than the current one cv.
|
||||
// Major version upgrades are not allowed to prevent breaking changes.
|
||||
// selfUpgradeCheck checks if the version target vt is greater
|
||||
// than the current one cv, perform self-upgrade then.
|
||||
//
|
||||
// The callers must ensure curVer and logger are non-nil.
|
||||
// Returns true if upgrade is allowed, false otherwise.
|
||||
func shouldUpgrade(vt string, cv *semver.Version, logger *zerolog.Logger) bool {
|
||||
func selfUpgradeCheck(vt string, cv *semver.Version, logger *zerolog.Logger) {
|
||||
if vt == "" {
|
||||
logger.Debug().Msg("no version target set, skipped checking self-upgrade")
|
||||
return false
|
||||
return
|
||||
}
|
||||
vts := vt
|
||||
if !strings.HasPrefix(vts, "v") {
|
||||
@@ -1500,58 +1483,28 @@ func shouldUpgrade(vt string, cv *semver.Version, logger *zerolog.Logger) bool {
|
||||
targetVer, err := semver.NewVersion(vts)
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msgf("invalid target version, skipped self-upgrade: %s", vt)
|
||||
return false
|
||||
return
|
||||
}
|
||||
|
||||
// Prevent major version upgrades to avoid breaking changes
|
||||
if targetVer.Major() != cv.Major() {
|
||||
logger.Warn().
|
||||
Str("target", vt).
|
||||
Str("current", cv.String()).
|
||||
Msgf("major version upgrade not allowed (target: %d, current: %d), skipped self-upgrade", targetVer.Major(), cv.Major())
|
||||
return false
|
||||
}
|
||||
|
||||
if !targetVer.GreaterThan(cv) {
|
||||
logger.Debug().
|
||||
Str("target", vt).
|
||||
Str("current", cv.String()).
|
||||
Msgf("target version is not greater than current one, skipped self-upgrade")
|
||||
return false
|
||||
return
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// performUpgrade executes the self-upgrade command.
|
||||
// Returns true if upgrade was initiated successfully, false otherwise.
|
||||
func performUpgrade(vt string) bool {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("failed to get executable path, skipped self-upgrade")
|
||||
return false
|
||||
return
|
||||
}
|
||||
cmd := exec.Command(exe, "upgrade", "prod", "-vv")
|
||||
cmd.SysProcAttr = sysProcAttrForDetachedChildProcess()
|
||||
if err := cmd.Start(); err != nil {
|
||||
mainLog.Load().Error().Err(err).Msg("failed to start self-upgrade")
|
||||
return false
|
||||
return
|
||||
}
|
||||
mainLog.Load().Debug().Msgf("self-upgrade triggered, version target: %s", vt)
|
||||
return true
|
||||
}
|
||||
|
||||
// selfUpgradeCheck checks if the version target vt is greater
|
||||
// than the current one cv, perform self-upgrade then.
|
||||
// Major version upgrades are not allowed to prevent breaking changes.
|
||||
//
|
||||
// The callers must ensure curVer and logger are non-nil.
|
||||
// Returns true if upgrade is allowed and should proceed, false otherwise.
|
||||
func selfUpgradeCheck(vt string, cv *semver.Version, logger *zerolog.Logger) bool {
|
||||
if shouldUpgrade(vt, cv, logger) {
|
||||
return performUpgrade(vt)
|
||||
}
|
||||
return false
|
||||
mainLog.Load().Debug().Msgf("self-upgrade triggered, version target: %s", vts)
|
||||
}
|
||||
|
||||
// leakOnUpstreamFailure reports whether ctrld should initiate a recovery flow
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_prog_dnsWatchdogEnabled(t *testing.T) {
|
||||
@@ -59,215 +55,3 @@ func Test_prog_dnsWatchdogInterval(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_shouldUpgrade(t *testing.T) {
|
||||
// Helper function to create a version
|
||||
makeVersion := func(v string) *semver.Version {
|
||||
ver, err := semver.NewVersion(v)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create version %s: %v", v, err)
|
||||
}
|
||||
return ver
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
versionTarget string
|
||||
currentVersion *semver.Version
|
||||
shouldUpgrade bool
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "empty version target",
|
||||
versionTarget: "",
|
||||
currentVersion: makeVersion("v1.0.0"),
|
||||
shouldUpgrade: false,
|
||||
description: "should skip upgrade when version target is empty",
|
||||
},
|
||||
{
|
||||
name: "invalid version target",
|
||||
versionTarget: "invalid-version",
|
||||
currentVersion: makeVersion("v1.0.0"),
|
||||
shouldUpgrade: false,
|
||||
description: "should skip upgrade when version target is invalid",
|
||||
},
|
||||
{
|
||||
name: "same version",
|
||||
versionTarget: "v1.0.0",
|
||||
currentVersion: makeVersion("v1.0.0"),
|
||||
shouldUpgrade: false,
|
||||
description: "should skip upgrade when target version equals current version",
|
||||
},
|
||||
{
|
||||
name: "older version",
|
||||
versionTarget: "v1.0.0",
|
||||
currentVersion: makeVersion("v1.1.0"),
|
||||
shouldUpgrade: false,
|
||||
description: "should skip upgrade when target version is older than current version",
|
||||
},
|
||||
{
|
||||
name: "patch upgrade allowed",
|
||||
versionTarget: "v1.0.1",
|
||||
currentVersion: makeVersion("v1.0.0"),
|
||||
shouldUpgrade: true,
|
||||
description: "should allow patch version upgrade within same major version",
|
||||
},
|
||||
{
|
||||
name: "minor upgrade allowed",
|
||||
versionTarget: "v1.1.0",
|
||||
currentVersion: makeVersion("v1.0.0"),
|
||||
shouldUpgrade: true,
|
||||
description: "should allow minor version upgrade within same major version",
|
||||
},
|
||||
{
|
||||
name: "major upgrade blocked",
|
||||
versionTarget: "v2.0.0",
|
||||
currentVersion: makeVersion("v1.0.0"),
|
||||
shouldUpgrade: false,
|
||||
description: "should block major version upgrade",
|
||||
},
|
||||
{
|
||||
name: "major downgrade blocked",
|
||||
versionTarget: "v1.0.0",
|
||||
currentVersion: makeVersion("v2.0.0"),
|
||||
shouldUpgrade: false,
|
||||
description: "should block major version downgrade",
|
||||
},
|
||||
{
|
||||
name: "version without v prefix",
|
||||
versionTarget: "1.0.1",
|
||||
currentVersion: makeVersion("v1.0.0"),
|
||||
shouldUpgrade: true,
|
||||
description: "should handle version target without v prefix",
|
||||
},
|
||||
{
|
||||
name: "complex version upgrade allowed",
|
||||
versionTarget: "v1.5.3",
|
||||
currentVersion: makeVersion("v1.4.2"),
|
||||
shouldUpgrade: true,
|
||||
description: "should allow complex version upgrade within same major version",
|
||||
},
|
||||
{
|
||||
name: "complex major upgrade blocked",
|
||||
versionTarget: "v3.1.0",
|
||||
currentVersion: makeVersion("v2.5.3"),
|
||||
shouldUpgrade: false,
|
||||
description: "should block complex major version upgrade",
|
||||
},
|
||||
{
|
||||
name: "pre-release version upgrade allowed",
|
||||
versionTarget: "v1.0.1-beta.1",
|
||||
currentVersion: makeVersion("v1.0.0"),
|
||||
shouldUpgrade: true,
|
||||
description: "should allow pre-release version upgrade within same major version",
|
||||
},
|
||||
{
|
||||
name: "pre-release major upgrade blocked",
|
||||
versionTarget: "v2.0.0-alpha.1",
|
||||
currentVersion: makeVersion("v1.0.0"),
|
||||
shouldUpgrade: false,
|
||||
description: "should block pre-release major version upgrade",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Create test logger
|
||||
testLogger := zerolog.New(zerolog.NewTestWriter(t)).With().Logger()
|
||||
|
||||
// Call the function and capture the result
|
||||
result := shouldUpgrade(tc.versionTarget, tc.currentVersion, &testLogger)
|
||||
|
||||
// Assert the expected result
|
||||
assert.Equal(t, tc.shouldUpgrade, result, tc.description)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_selfUpgradeCheck(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipped due to Windows file locking issue on Github Action runners")
|
||||
}
|
||||
|
||||
// Helper function to create a version
|
||||
makeVersion := func(v string) *semver.Version {
|
||||
ver, err := semver.NewVersion(v)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create version %s: %v", v, err)
|
||||
}
|
||||
return ver
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
versionTarget string
|
||||
currentVersion *semver.Version
|
||||
shouldUpgrade bool
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "upgrade allowed",
|
||||
versionTarget: "v1.0.1",
|
||||
currentVersion: makeVersion("v1.0.0"),
|
||||
shouldUpgrade: true,
|
||||
description: "should allow upgrade and attempt to perform it",
|
||||
},
|
||||
{
|
||||
name: "upgrade blocked",
|
||||
versionTarget: "v2.0.0",
|
||||
currentVersion: makeVersion("v1.0.0"),
|
||||
shouldUpgrade: false,
|
||||
description: "should block upgrade and not attempt to perform it",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Create test logger
|
||||
testLogger := zerolog.New(zerolog.NewTestWriter(t)).With().Logger()
|
||||
|
||||
// Call the function and capture the result
|
||||
result := selfUpgradeCheck(tc.versionTarget, tc.currentVersion, &testLogger)
|
||||
|
||||
// Assert the expected result
|
||||
assert.Equal(t, tc.shouldUpgrade, result, tc.description)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_performUpgrade(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipped due to Windows file locking issue on Github Action runners")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
versionTarget string
|
||||
expectedResult bool
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "valid version target",
|
||||
versionTarget: "v1.0.1",
|
||||
expectedResult: true,
|
||||
description: "should attempt to perform upgrade with valid version target",
|
||||
},
|
||||
{
|
||||
name: "empty version target",
|
||||
versionTarget: "",
|
||||
expectedResult: true,
|
||||
description: "should attempt to perform upgrade even with empty version target",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Call the function and capture the result
|
||||
result := performUpgrade(tc.versionTarget)
|
||||
assert.Equal(t, tc.expectedResult, result, tc.description)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,17 +28,15 @@ type AppCallback interface {
|
||||
// Start configures utility with config.toml from provided directory.
|
||||
// This function will block until Stop is called
|
||||
// Check port availability prior to calling it.
|
||||
func (c *Controller) Start(CdUID string, ProvisionID string, CustomHostname string, HomeDir string, UpstreamProto string, logLevel int, logPath string) {
|
||||
func (c *Controller) Start(CdUID string, HomeDir string, UpstreamProto string, logLevel int, logPath string) {
|
||||
if c.stopCh == nil {
|
||||
c.stopCh = make(chan struct{})
|
||||
c.Config = cli.AppConfig{
|
||||
CdUID: CdUID,
|
||||
ProvisionID: ProvisionID,
|
||||
CustomHostname: CustomHostname,
|
||||
HomeDir: HomeDir,
|
||||
UpstreamProto: UpstreamProto,
|
||||
Verbose: logLevel,
|
||||
LogPath: logPath,
|
||||
CdUID: CdUID,
|
||||
HomeDir: HomeDir,
|
||||
UpstreamProto: UpstreamProto,
|
||||
Verbose: logLevel,
|
||||
LogPath: logPath,
|
||||
}
|
||||
appCallback := mapCallback(c.AppCallback)
|
||||
cli.RunMobile(&c.Config, &appCallback, c.stopCh)
|
||||
|
||||
@@ -358,15 +358,6 @@ func (uc *UpstreamConfig) Init() {
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyMsg creates and returns a new DNS message could be used for testing upstream health.
|
||||
func (uc *UpstreamConfig) VerifyMsg() *dns.Msg {
|
||||
msg := new(dns.Msg)
|
||||
msg.RecursionDesired = true
|
||||
msg.SetQuestion(".", dns.TypeNS)
|
||||
msg.SetEdns0(4096, false) // ensure handling of large DNS response
|
||||
return msg
|
||||
}
|
||||
|
||||
// VerifyDomain returns the domain name that could be resolved by the upstream endpoint.
|
||||
// It returns empty for non-ControlD upstream endpoint.
|
||||
func (uc *UpstreamConfig) VerifyDomain() string {
|
||||
|
||||
@@ -36,7 +36,7 @@ func (uc *UpstreamConfig) setupDOH3Transport() {
|
||||
func (uc *UpstreamConfig) newDOH3Transport(addrs []string) http.RoundTripper {
|
||||
rt := &http3.Transport{}
|
||||
rt.TLSClientConfig = &tls.Config{RootCAs: uc.certPool}
|
||||
rt.Dial = func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) {
|
||||
rt.Dial = func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
||||
_, port, _ := net.SplitHostPort(addr)
|
||||
// if we have a bootstrap ip set, use it to avoid DNS lookup
|
||||
if uc.BootstrapIP != "" {
|
||||
@@ -96,14 +96,14 @@ func (uc *UpstreamConfig) doh3Transport(dnsType uint16) http.RoundTripper {
|
||||
// - quic dialer is different with net.Dialer
|
||||
// - simplification for quic free version
|
||||
type parallelDialerResult struct {
|
||||
conn *quic.Conn
|
||||
conn quic.EarlyConnection
|
||||
err error
|
||||
}
|
||||
|
||||
type quicParallelDialer struct{}
|
||||
|
||||
// Dial performs parallel dialing to the given address list.
|
||||
func (d *quicParallelDialer) Dial(ctx context.Context, addrs []string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) {
|
||||
func (d *quicParallelDialer) Dial(ctx context.Context, addrs []string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
||||
if len(addrs) == 0 {
|
||||
return nil, errors.New("empty addresses")
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
# Known Issues
|
||||
|
||||
This document outlines known issues with ctrld and their current status, workarounds, and recommendations.
|
||||
|
||||
## macOS (Darwin) Issues
|
||||
|
||||
### Self-Upgrade Issue on Darwin 15.5
|
||||
|
||||
**Issue**: ctrld self-upgrading functionality may not work on macOS Darwin 15.5.
|
||||
|
||||
**Status**: Under investigation
|
||||
|
||||
**Description**: Users on macOS Darwin 15.5 may experience issues when ctrld attempts to perform automatic self-upgrades. The upgrade process would be triggered, but ctrld won't be upgraded.
|
||||
|
||||
**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.
|
||||
|
||||
**Affected Versions**: ctrld v1.4.2 and later on macOS Darwin 15.5
|
||||
|
||||
**Last Updated**: 05/09/2025
|
||||
|
||||
---
|
||||
|
||||
## Contributing to Known Issues
|
||||
|
||||
If you encounter an issue not listed here, please:
|
||||
|
||||
1. Check the [GitHub Issues](https://github.com/Control-D-Inc/ctrld/issues) to see if it's already reported
|
||||
2. If not reported, create a new issue with:
|
||||
- Detailed description of the problem
|
||||
- Steps to reproduce
|
||||
- Expected vs actual behavior
|
||||
- System information (OS, version, architecture)
|
||||
- ctrld version
|
||||
|
||||
## Issue Status Legend
|
||||
|
||||
- **Under investigation**: Issue is confirmed and being analyzed
|
||||
- **Workaround available**: Temporary solution exists while permanent fix is developed
|
||||
- **Fixed**: Issue has been resolved in a specific version
|
||||
- **Won't fix**: Issue is acknowledged but will not be addressed due to technical limitations or design decisions
|
||||
@@ -142,7 +142,7 @@ func (s *testQUICServer) serve(t *testing.T) {
|
||||
}
|
||||
|
||||
// handleConnection manages an individual QUIC connection by accepting and handling incoming streams in separate goroutines.
|
||||
func (s *testQUICServer) handleConnection(t *testing.T, conn *quic.Conn) {
|
||||
func (s *testQUICServer) handleConnection(t *testing.T, conn quic.Connection) {
|
||||
for {
|
||||
stream, err := conn.AcceptStream(context.Background())
|
||||
if err != nil {
|
||||
@@ -154,7 +154,7 @@ func (s *testQUICServer) handleConnection(t *testing.T, conn *quic.Conn) {
|
||||
}
|
||||
|
||||
// handleStream processes a single QUIC stream, reads DNS messages, generates a response, and sends it back to the client.
|
||||
func (s *testQUICServer) handleStream(t *testing.T, stream *quic.Stream) {
|
||||
func (s *testQUICServer) handleStream(t *testing.T, stream quic.Stream) {
|
||||
defer stream.Close()
|
||||
|
||||
// Read length (2 bytes)
|
||||
|
||||
7
go.mod
7
go.mod
@@ -29,7 +29,7 @@ require (
|
||||
github.com/prometheus/client_golang v1.19.1
|
||||
github.com/prometheus/client_model v0.5.0
|
||||
github.com/prometheus/prom2json v1.3.3
|
||||
github.com/quic-go/quic-go v0.54.0
|
||||
github.com/quic-go/quic-go v0.48.2
|
||||
github.com/rs/zerolog v1.28.0
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
@@ -54,8 +54,10 @@ require (
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
@@ -72,6 +74,7 @@ require (
|
||||
github.com/mdlayher/packet v1.1.2 // indirect
|
||||
github.com/mdlayher/socket v0.5.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
@@ -86,7 +89,7 @@ require (
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e // indirect
|
||||
github.com/vishvananda/netns v0.0.4 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
go.uber.org/mock v0.4.0 // indirect
|
||||
go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
|
||||
20
go.sum
20
go.sum
@@ -91,6 +91,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 h1:ymLjT4f35nQbASLnvxEde4XOBL+Sn7rFuV+FOJqkljg=
|
||||
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0/go.mod h1:6daplAwHHGbUGib4990V3Il26O0OC4aRyvewaaAihaA=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
@@ -101,6 +103,8 @@ github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/j
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ=
|
||||
github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg=
|
||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU=
|
||||
@@ -158,6 +162,8 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf
|
||||
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
@@ -236,6 +242,10 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
|
||||
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
|
||||
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
|
||||
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||
github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
@@ -261,8 +271,8 @@ github.com/prometheus/prom2json v1.3.3 h1:IYfSMiZ7sSOfliBoo89PcufjWO4eAR0gznGcET
|
||||
github.com/prometheus/prom2json v1.3.3/go.mod h1:Pv4yIPktEkK7btWsrUTWDDDrnpUrAELaOCj+oFwlgmc=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||
github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE=
|
||||
github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
@@ -320,8 +330,8 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H9rmp5nkDTAst8=
|
||||
go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||
@@ -495,6 +505,8 @@ golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
|
||||
@@ -16,5 +16,4 @@ var clientInfoFiles = map[string]ctrld.LeaseFileFormat{
|
||||
"/var/dhcpd/var/db/dhcpd.leases": ctrld.IscDhcpd, // Pfsense
|
||||
"/home/pi/.router/run/dhcp/dnsmasq.leases": ctrld.Dnsmasq, // Firewalla
|
||||
"/var/lib/kea/dhcp4.leases": ctrld.KeaDHCP4, // Pfsense
|
||||
"/var/db/dnsmasq.leases": ctrld.Dnsmasq, // OPNsense
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ func (m *mdns) lookupIPByHostname(name string, v6 bool) string {
|
||||
if value == name {
|
||||
if addr, err := netip.ParseAddr(key.(string)); err == nil && addr.Is6() == v6 {
|
||||
ip = addr.String()
|
||||
//lint:ignore S1008 This is used for readable.
|
||||
if addr.IsLoopback() { // Continue searching if this is loopback address.
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -104,6 +104,7 @@ func (p *ptrDiscover) lookupIPByHostname(name string, v6 bool) string {
|
||||
if value == name {
|
||||
if addr, err := netip.ParseAddr(key.(string)); err == nil && addr.Is6() == v6 {
|
||||
ip = addr.String()
|
||||
//lint:ignore S1008 This is used for readable.
|
||||
if addr.IsLoopback() { // Continue searching if this is loopback address.
|
||||
return true
|
||||
}
|
||||
@@ -119,7 +120,8 @@ func (p *ptrDiscover) lookupIPByHostname(name string, v6 bool) string {
|
||||
// is reachable, set p.serverDown to false, so p.lookupHostname can continue working.
|
||||
func (p *ptrDiscover) checkServer() {
|
||||
bo := backoff.NewBackoff("ptrDiscover", func(format string, args ...any) {}, time.Minute*5)
|
||||
m := (&ctrld.UpstreamConfig{}).VerifyMsg()
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion(".", dns.TypeNS)
|
||||
ping := func() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -29,62 +28,3 @@ func interfaceNameFromReader(r io.Reader) (string, error) {
|
||||
}
|
||||
return "", errors.New("not found")
|
||||
}
|
||||
|
||||
// AdditionalConfigFiles returns a list of Dnsmasq configuration files found in the "/tmp/etc" directory.
|
||||
func AdditionalConfigFiles() []string {
|
||||
if paths, err := filepath.Glob("/tmp/etc/dnsmasq-*.conf"); err == nil {
|
||||
return paths
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AdditionalLeaseFiles returns a list of lease file paths corresponding to the Dnsmasq configuration files.
|
||||
func AdditionalLeaseFiles() []string {
|
||||
cfgFiles := AdditionalConfigFiles()
|
||||
if len(cfgFiles) == 0 {
|
||||
return nil
|
||||
}
|
||||
leaseFiles := make([]string, 0, len(cfgFiles))
|
||||
for _, cfgFile := range cfgFiles {
|
||||
if leaseFile := leaseFileFromConfigFileName(cfgFile); leaseFile != "" {
|
||||
leaseFiles = append(leaseFiles, leaseFile)
|
||||
|
||||
} else {
|
||||
leaseFiles = append(leaseFiles, defaultLeaseFileFromConfigPath(cfgFile))
|
||||
}
|
||||
}
|
||||
return leaseFiles
|
||||
}
|
||||
|
||||
// leaseFileFromConfigFileName retrieves the DHCP lease file path by reading and parsing the provided configuration file.
|
||||
func leaseFileFromConfigFileName(cfgFile string) string {
|
||||
if f, err := os.Open(cfgFile); err == nil {
|
||||
return leaseFileFromReader(f)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// leaseFileFromReader parses the given io.Reader for the "dhcp-leasefile" configuration and returns its value as a string.
|
||||
func leaseFileFromReader(r io.Reader) string {
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
before, after, found := strings.Cut(line, "=")
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
if before == "dhcp-leasefile" {
|
||||
return after
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// defaultLeaseFileFromConfigPath generates the default lease file path based on the provided configuration file path.
|
||||
func defaultLeaseFileFromConfigPath(path string) string {
|
||||
name := filepath.Base(path)
|
||||
return filepath.Join("/var/lib/misc", strings.TrimSuffix(name, ".conf")+".leases")
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package dnsmasq
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -45,49 +44,3 @@ interface=eth0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_leaseFileFromReader(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in io.Reader
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
"default",
|
||||
strings.NewReader(`
|
||||
dhcp-script=/sbin/dhcpc_lease
|
||||
dhcp-leasefile=/var/lib/misc/dnsmasq-1.leases
|
||||
script-arp
|
||||
`),
|
||||
"/var/lib/misc/dnsmasq-1.leases",
|
||||
},
|
||||
{
|
||||
"non-default",
|
||||
strings.NewReader(`
|
||||
dhcp-script=/sbin/dhcpc_lease
|
||||
dhcp-leasefile=/tmp/var/lib/misc/dnsmasq-1.leases
|
||||
script-arp
|
||||
`),
|
||||
"/tmp/var/lib/misc/dnsmasq-1.leases",
|
||||
},
|
||||
{
|
||||
"missing",
|
||||
strings.NewReader(`
|
||||
dhcp-script=/sbin/dhcpc_lease
|
||||
script-arp
|
||||
`),
|
||||
"",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := leaseFileFromReader(tc.in); got != tc.expected {
|
||||
t.Errorf("leaseFileFromReader() = %v, want %v", got, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"errors"
|
||||
"html/template"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
@@ -27,13 +26,9 @@ max-cache-ttl=0
|
||||
{{- end}}
|
||||
`
|
||||
|
||||
const (
|
||||
MerlinConfPath = "/tmp/etc/dnsmasq.conf"
|
||||
MerlinJffsConfDir = "/jffs/configs"
|
||||
MerlinJffsConfPath = "/jffs/configs/dnsmasq.conf"
|
||||
MerlinPostConfPath = "/jffs/scripts/dnsmasq.postconf"
|
||||
)
|
||||
|
||||
const MerlinConfPath = "/tmp/etc/dnsmasq.conf"
|
||||
const MerlinJffsConfPath = "/jffs/configs/dnsmasq.conf"
|
||||
const MerlinPostConfPath = "/jffs/scripts/dnsmasq.postconf"
|
||||
const MerlinPostConfMarker = `# GENERATED BY ctrld - EOF`
|
||||
const MerlinPostConfTmpl = `# GENERATED BY ctrld - DO NOT MODIFY
|
||||
|
||||
@@ -164,27 +159,3 @@ func FirewallaSelfInterfaces() []*net.Interface {
|
||||
}
|
||||
return ifaces
|
||||
}
|
||||
|
||||
const (
|
||||
ubios43ConfPath = "/run/dnsmasq.dhcp.conf.d"
|
||||
ubios42ConfPath = "/run/dnsmasq.conf.d"
|
||||
ubios43PidFile = "/run/dnsmasq-main.pid"
|
||||
ubios42PidFile = "/run/dnsmasq.pid"
|
||||
UbiosConfName = "zzzctrld.conf"
|
||||
)
|
||||
|
||||
// UbiosConfPath returns the appropriate configuration path based on the system's directory structure.
|
||||
func UbiosConfPath() string {
|
||||
if st, _ := os.Stat(ubios43ConfPath); st != nil && st.IsDir() {
|
||||
return ubios43ConfPath
|
||||
}
|
||||
return ubios42ConfPath
|
||||
}
|
||||
|
||||
// UbiosPidFile returns the appropriate dnsmasq pid file based on the system's directory structure.
|
||||
func UbiosPidFile() string {
|
||||
if st, _ := os.Stat(ubios43PidFile); st != nil && !st.IsDir() {
|
||||
return ubios43PidFile
|
||||
}
|
||||
return ubios42PidFile
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/kardianos/service"
|
||||
@@ -182,7 +181,7 @@ func ContentFilteringEnabled() bool {
|
||||
// DnsShieldEnabled reports whether DNS Shield is enabled.
|
||||
// See: https://community.ui.com/releases/UniFi-OS-Dream-Machines-3-2-7/251dfc1e-f4dd-4264-a080-3be9d8b9e02b
|
||||
func DnsShieldEnabled() bool {
|
||||
buf, err := os.ReadFile(filepath.Join(dnsmasq.UbiosConfPath(), "dns.conf"))
|
||||
buf, err := os.ReadFile("/var/run/dnsmasq.conf.d/dns.conf")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
@@ -21,18 +20,10 @@ import (
|
||||
|
||||
const Name = "merlin"
|
||||
|
||||
// nvramKvMap is a map of NVRAM key-value pairs used to configure and manage Merlin-specific settings.
|
||||
var nvramKvMap = map[string]string{
|
||||
"dnspriv_enable": "0", // Ensure Merlin native DoT disabled.
|
||||
}
|
||||
|
||||
// dnsmasqConfig represents configuration paths for dnsmasq operations in Merlin firmware.
|
||||
type dnsmasqConfig struct {
|
||||
confPath string
|
||||
jffsConfPath string
|
||||
}
|
||||
|
||||
// Merlin represents a configuration handler for setting up and managing ctrld on Merlin routers.
|
||||
type Merlin struct {
|
||||
cfg *ctrld.Config
|
||||
}
|
||||
@@ -42,22 +33,18 @@ func New(cfg *ctrld.Config) *Merlin {
|
||||
return &Merlin{cfg: cfg}
|
||||
}
|
||||
|
||||
// ConfigureService configures the service based on the provided configuration. It returns an error if the configuration fails.
|
||||
func (m *Merlin) ConfigureService(config *service.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Install sets up the necessary configurations and services required for the Merlin instance to function properly.
|
||||
func (m *Merlin) Install(_ *service.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Uninstall removes the ctrld-related configurations and services from the Merlin router and reverts to the original state.
|
||||
func (m *Merlin) Uninstall(_ *service.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// PreRun prepares the Merlin instance for operation by waiting for essential services and directories to become available.
|
||||
func (m *Merlin) PreRun() error {
|
||||
// Wait NTP ready.
|
||||
_ = m.Cleanup()
|
||||
@@ -79,7 +66,6 @@ func (m *Merlin) PreRun() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Setup initializes and configures the Merlin instance for use, including setting up dnsmasq and necessary nvram settings.
|
||||
func (m *Merlin) Setup() error {
|
||||
if m.cfg.FirstListener().IsDirectDnsListener() {
|
||||
return nil
|
||||
@@ -93,10 +79,35 @@ func (m *Merlin) Setup() error {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, cfg := range getDnsmasqConfigs() {
|
||||
if err := m.setupDnsmasq(cfg); err != nil {
|
||||
return fmt.Errorf("failed to setup dnsmasq: config: %s, error: %w", cfg.confPath, err)
|
||||
}
|
||||
// Copy current dnsmasq config to /jffs/configs/dnsmasq.conf,
|
||||
// Then we will run postconf script on this file.
|
||||
//
|
||||
// Normally, adding postconf script is enough. However, we see
|
||||
// reports on some Merlin devices that postconf scripts does not
|
||||
// work, but manipulating the config directly via /jffs/configs does.
|
||||
src, err := os.Open(dnsmasq.MerlinConfPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open dnsmasq config: %w", err)
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
dst, err := os.Create(dnsmasq.MerlinJffsConfPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create %s: %w", dnsmasq.MerlinJffsConfPath, err)
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
if _, err := io.Copy(dst, src); err != nil {
|
||||
return fmt.Errorf("failed to copy current dnsmasq config: %w", err)
|
||||
}
|
||||
if err := dst.Close(); err != nil {
|
||||
return fmt.Errorf("failed to save %s: %w", dnsmasq.MerlinJffsConfPath, err)
|
||||
}
|
||||
|
||||
// Run postconf script on /jffs/configs/dnsmasq.conf directly.
|
||||
cmd := exec.Command("/bin/sh", dnsmasq.MerlinPostConfPath, dnsmasq.MerlinJffsConfPath)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to run post conf: %s: %w", string(out), err)
|
||||
}
|
||||
|
||||
// Restart dnsmasq service.
|
||||
@@ -111,7 +122,6 @@ func (m *Merlin) Setup() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cleanup restores the original dnsmasq and nvram configurations and restarts dnsmasq if necessary.
|
||||
func (m *Merlin) Cleanup() error {
|
||||
if m.cfg.FirstListener().IsDirectDnsListener() {
|
||||
return nil
|
||||
@@ -133,11 +143,9 @@ func (m *Merlin) Cleanup() error {
|
||||
if err := os.WriteFile(dnsmasq.MerlinPostConfPath, merlinParsePostConf(buf), 0750); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, cfg := range getDnsmasqConfigs() {
|
||||
if err := m.cleanupDnsmasqJffs(cfg); err != nil {
|
||||
return fmt.Errorf("failed to cleanup jffs dnsmasq: config: %s, error: %w", cfg.confPath, err)
|
||||
}
|
||||
// Remove /jffs/configs/dnsmasq.conf file.
|
||||
if err := os.Remove(dnsmasq.MerlinJffsConfPath); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
// Restart dnsmasq service.
|
||||
if err := restartDNSMasq(); err != nil {
|
||||
@@ -146,54 +154,6 @@ func (m *Merlin) Cleanup() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// setupDnsmasq sets up dnsmasq configuration by writing postconf, copying configuration, and running a postconf script.
|
||||
func (m *Merlin) setupDnsmasq(cfg *dnsmasqConfig) error {
|
||||
src, err := os.Open(cfg.confPath)
|
||||
if os.IsNotExist(err) {
|
||||
return nil // nothing to do if conf file does not exist.
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open dnsmasq config: %w", err)
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// Copy current dnsmasq config to cfg.jffsConfPath,
|
||||
// Then we will run postconf script on this file.
|
||||
//
|
||||
// Normally, adding postconf script is enough. However, we see
|
||||
// reports on some Merlin devices that postconf scripts does not
|
||||
// work, but manipulating the config directly via /jffs/configs does.
|
||||
dst, err := os.Create(cfg.jffsConfPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create %s: %w", cfg.jffsConfPath, err)
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
if _, err := io.Copy(dst, src); err != nil {
|
||||
return fmt.Errorf("failed to copy current dnsmasq config: %w", err)
|
||||
}
|
||||
if err := dst.Close(); err != nil {
|
||||
return fmt.Errorf("failed to save %s: %w", cfg.jffsConfPath, err)
|
||||
}
|
||||
|
||||
// Run postconf script on cfg.jffsConfPath directly.
|
||||
cmd := exec.Command("/bin/sh", dnsmasq.MerlinPostConfPath, cfg.jffsConfPath)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to run post conf: %s: %w", string(out), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// cleanupDnsmasqJffs removes the JFFS configuration file specified in the given dnsmasqConfig, if it exists.
|
||||
func (m *Merlin) cleanupDnsmasqJffs(cfg *dnsmasqConfig) error {
|
||||
// Remove cfg.jffsConfPath file.
|
||||
if err := os.Remove(cfg.jffsConfPath); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeDnsmasqPostconf writes the requireddnsmasqConfigs post-configuration for dnsmasq to enable custom DNS settings with ctrld.
|
||||
func (m *Merlin) writeDnsmasqPostconf() error {
|
||||
buf, err := os.ReadFile(dnsmasq.MerlinPostConfPath)
|
||||
// Already setup.
|
||||
@@ -219,8 +179,6 @@ func (m *Merlin) writeDnsmasqPostconf() error {
|
||||
return os.WriteFile(dnsmasq.MerlinPostConfPath, []byte(data), 0750)
|
||||
}
|
||||
|
||||
// restartDNSMasq restarts the dnsmasq service by executing the appropriate system command using "service".
|
||||
// Returns an error if the command fails or if there is an issue processing the command output.
|
||||
func restartDNSMasq() error {
|
||||
if out, err := exec.Command("service", "restart_dnsmasq").CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("restart_dnsmasq: %s, %w", string(out), err)
|
||||
@@ -228,22 +186,6 @@ func restartDNSMasq() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// getDnsmasqConfigs retrieves a list of dnsmasqConfig containing configuration and JFFS paths for dnsmasq operations.
|
||||
func getDnsmasqConfigs() []*dnsmasqConfig {
|
||||
cfgs := []*dnsmasqConfig{
|
||||
{dnsmasq.MerlinConfPath, dnsmasq.MerlinJffsConfPath},
|
||||
}
|
||||
for _, path := range dnsmasq.AdditionalConfigFiles() {
|
||||
jffsConfPath := filepath.Join(dnsmasq.MerlinJffsConfDir, filepath.Base(path))
|
||||
cfgs = append(cfgs, &dnsmasqConfig{path, jffsConfPath})
|
||||
}
|
||||
|
||||
return cfgs
|
||||
}
|
||||
|
||||
// merlinParsePostConf parses the dnsmasq post configuration by removing content after the MerlinPostConfMarker, if present.
|
||||
// If no marker is found, the original buffer is returned unmodified.
|
||||
// Returns nil if the input buffer is empty.
|
||||
func merlinParsePostConf(buf []byte) []byte {
|
||||
if len(buf) == 0 {
|
||||
return nil
|
||||
@@ -255,7 +197,6 @@ func merlinParsePostConf(buf []byte) []byte {
|
||||
return buf
|
||||
}
|
||||
|
||||
// waitDirExists waits until the specified directory exists, polling its existence every second.
|
||||
func waitDirExists(dir string) {
|
||||
for {
|
||||
if _, err := os.Stat(dir); !os.IsNotExist(err) {
|
||||
|
||||
@@ -13,13 +13,14 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/kardianos/service"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/dnsmasq"
|
||||
)
|
||||
|
||||
// This is a copy of https://github.com/kardianos/service/blob/v1.2.1/service_sysv_linux.go,
|
||||
// with modification for supporting ubios v1 init system.
|
||||
|
||||
// Keep in sync with ubios.ubiosDNSMasqConfigPath
|
||||
const ubiosDNSMasqConfigPath = "/run/dnsmasq.conf.d/zzzctrld.conf"
|
||||
|
||||
type ubiosSvc struct {
|
||||
i service.Interface
|
||||
platform string
|
||||
@@ -85,7 +86,7 @@ func (s *ubiosSvc) Install() error {
|
||||
}{
|
||||
s.Config,
|
||||
path,
|
||||
filepath.Join(dnsmasq.UbiosConfPath(), dnsmasq.UbiosConfName),
|
||||
ubiosDNSMasqConfigPath,
|
||||
}
|
||||
|
||||
if err := s.template().Execute(f, to); err != nil {
|
||||
|
||||
@@ -3,7 +3,6 @@ package ubios
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/kardianos/service"
|
||||
@@ -13,19 +12,19 @@ import (
|
||||
"github.com/Control-D-Inc/ctrld/internal/router/edgeos"
|
||||
)
|
||||
|
||||
const Name = "ubios"
|
||||
const (
|
||||
Name = "ubios"
|
||||
ubiosDNSMasqConfigPath = "/run/dnsmasq.conf.d/zzzctrld.conf"
|
||||
ubiosDNSMasqDnsConfigPath = "/run/dnsmasq.conf.d/dns.conf"
|
||||
)
|
||||
|
||||
type Ubios struct {
|
||||
cfg *ctrld.Config
|
||||
dnsmasqConfPath string
|
||||
cfg *ctrld.Config
|
||||
}
|
||||
|
||||
// New returns a router.Router for configuring/setup/run ctrld on Ubios routers.
|
||||
func New(cfg *ctrld.Config) *Ubios {
|
||||
return &Ubios{
|
||||
cfg: cfg,
|
||||
dnsmasqConfPath: filepath.Join(dnsmasq.UbiosConfPath(), dnsmasq.UbiosConfName),
|
||||
}
|
||||
return &Ubios{cfg: cfg}
|
||||
}
|
||||
|
||||
func (u *Ubios) ConfigureService(config *service.Config) error {
|
||||
@@ -60,7 +59,7 @@ func (u *Ubios) Setup() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(u.dnsmasqConfPath, []byte(data), 0600); err != nil {
|
||||
if err := os.WriteFile(ubiosDNSMasqConfigPath, []byte(data), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
// Restart dnsmasq service.
|
||||
@@ -75,7 +74,7 @@ func (u *Ubios) Cleanup() error {
|
||||
return nil
|
||||
}
|
||||
// Remove the custom dnsmasq config
|
||||
if err := os.Remove(u.dnsmasqConfPath); err != nil {
|
||||
if err := os.Remove(ubiosDNSMasqConfigPath); err != nil {
|
||||
return err
|
||||
}
|
||||
// Restart dnsmasq service.
|
||||
@@ -86,7 +85,7 @@ func (u *Ubios) Cleanup() error {
|
||||
}
|
||||
|
||||
func restartDNSMasq() error {
|
||||
buf, err := os.ReadFile(dnsmasq.UbiosPidFile())
|
||||
buf, err := os.ReadFile("/run/dnsmasq.pid")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -5,12 +5,9 @@ import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/net/netmon"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld/internal/dns/resolvconffile"
|
||||
)
|
||||
|
||||
@@ -131,25 +128,3 @@ func virtualInterfaces() set {
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// validInterfacesMap returns a set containing non virtual interfaces.
|
||||
// TODO: deduplicated with cmd/cli/net_linux.go in v2.
|
||||
func validInterfaces() set {
|
||||
m := make(map[string]struct{})
|
||||
vis := virtualInterfaces()
|
||||
netmon.ForeachInterface(func(i netmon.Interface, prefixes []netip.Prefix) {
|
||||
if _, existed := vis[i.Name]; existed {
|
||||
return
|
||||
}
|
||||
m[i.Name] = struct{}{}
|
||||
})
|
||||
// Fallback to default route interface if found nothing.
|
||||
if len(m) == 0 {
|
||||
defaultRoute, err := netmon.DefaultRoute()
|
||||
if err != nil {
|
||||
return m
|
||||
}
|
||||
m[defaultRoute.InterfaceName] = struct{}{}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
@@ -23,17 +23,20 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
maxDNSAdapterRetries = 5
|
||||
retryDelayDNSAdapter = 1 * time.Second
|
||||
defaultDNSAdapterTimeout = 10 * time.Second
|
||||
minDNSServers = 1 // Minimum number of DNS servers we want to find
|
||||
|
||||
DS_FORCE_REDISCOVERY = 0x00000001
|
||||
DS_DIRECTORY_SERVICE_REQUIRED = 0x00000010
|
||||
DS_BACKGROUND_ONLY = 0x00000100
|
||||
DS_IP_REQUIRED = 0x00000200
|
||||
DS_IS_DNS_NAME = 0x00020000
|
||||
DS_RETURN_DNS_NAME = 0x40000000
|
||||
maxDNSAdapterRetries = 5
|
||||
retryDelayDNSAdapter = 1 * time.Second
|
||||
defaultDNSAdapterTimeout = 10 * time.Second
|
||||
minDNSServers = 1 // Minimum number of DNS servers we want to find
|
||||
NetSetupUnknown uint32 = 0
|
||||
NetSetupWorkgroup uint32 = 1
|
||||
NetSetupDomain uint32 = 2
|
||||
NetSetupCloudDomain uint32 = 3
|
||||
DS_FORCE_REDISCOVERY = 0x00000001
|
||||
DS_DIRECTORY_SERVICE_REQUIRED = 0x00000010
|
||||
DS_BACKGROUND_ONLY = 0x00000100
|
||||
DS_IP_REQUIRED = 0x00000200
|
||||
DS_IS_DNS_NAME = 0x00020000
|
||||
DS_RETURN_DNS_NAME = 0x40000000
|
||||
)
|
||||
|
||||
type DomainControllerInfo struct {
|
||||
@@ -155,7 +158,7 @@ func getDNSServers(ctx context.Context) ([]string, error) {
|
||||
0, // DomainGuid - not needed
|
||||
0, // SiteName - not needed
|
||||
uintptr(flags), // Flags
|
||||
uintptr(unsafe.Pointer(&info))) // DomainControllerInfo - output
|
||||
uintptr(unsafe.Pointer(&info))) // DomainControllerInfo - output
|
||||
|
||||
if ret != 0 {
|
||||
switch ret {
|
||||
@@ -340,28 +343,27 @@ func checkDomainJoined() bool {
|
||||
var domain *uint16
|
||||
var status uint32
|
||||
|
||||
if err := windows.NetGetJoinInformation(nil, &domain, &status); err != nil {
|
||||
Log(context.Background(), logger.Debug(), "Failed to get domain join status: %v", err)
|
||||
err := windows.NetGetJoinInformation(nil, &domain, &status)
|
||||
if err != nil {
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"Failed to get domain join status: %v", err)
|
||||
return false
|
||||
}
|
||||
defer windows.NetApiBufferFree((*byte)(unsafe.Pointer(domain)))
|
||||
|
||||
// NETSETUP_JOIN_STATUS constants from Microsoft Windows API
|
||||
// See: https://learn.microsoft.com/en-us/windows/win32/api/lmjoin/ne-lmjoin-netsetup_join_status
|
||||
//
|
||||
// NetSetupUnknownStatus uint32 = 0 // The status is unknown
|
||||
// NetSetupUnjoined uint32 = 1 // The computer is not joined to a domain or workgroup
|
||||
// NetSetupWorkgroupName uint32 = 2 // The computer is joined to a workgroup
|
||||
// NetSetupDomainName uint32 = 3 // The computer is joined to a domain
|
||||
//
|
||||
// We only care about NetSetupDomainName.
|
||||
domainName := windows.UTF16PtrToString(domain)
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"Domain join status: domain=%s status=%d (UnknownStatus=0, Unjoined=1, WorkgroupName=2, DomainName=3)",
|
||||
"Domain join status: domain=%s status=%d (Unknown=0, Workgroup=1, Domain=2, CloudDomain=3)",
|
||||
domainName, status)
|
||||
|
||||
isDomain := status == syscall.NetSetupDomainName
|
||||
Log(context.Background(), logger.Debug(), "Is domain joined? status=%d, result=%v", status, isDomain)
|
||||
// Consider domain or cloud domain as domain-joined
|
||||
isDomain := status == NetSetupDomain || status == NetSetupCloudDomain
|
||||
Log(context.Background(), logger.Debug(),
|
||||
"Is domain joined? status=%d, traditional=%v, cloud=%v, result=%v",
|
||||
status,
|
||||
status == NetSetupDomain,
|
||||
status == NetSetupCloudDomain,
|
||||
isDomain)
|
||||
|
||||
return isDomain
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package ctrld
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// validInterfaces returns a set of all valid hardware ports.
|
||||
// TODO: deduplicated with cmd/cli/net_darwin.go in v2.
|
||||
func validInterfaces() map[string]struct{} {
|
||||
b, err := exec.Command("networksetup", "-listallhardwareports").Output()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return parseListAllHardwarePorts(bytes.NewReader(b))
|
||||
}
|
||||
|
||||
// parseListAllHardwarePorts parses output of "networksetup -listallhardwareports"
|
||||
// and returns map presents all hardware ports.
|
||||
func parseListAllHardwarePorts(r io.Reader) map[string]struct{} {
|
||||
m := make(map[string]struct{})
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
after, ok := strings.CutPrefix(line, "Device: ")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
m[after] = struct{}{}
|
||||
}
|
||||
return m
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
//go:build !darwin && !windows && !linux
|
||||
|
||||
package ctrld
|
||||
|
||||
import "tailscale.com/net/netmon"
|
||||
|
||||
// validInterfaces returns a set containing only default route interfaces.
|
||||
// TODO: deuplicated with cmd/cli/net_others.go in v2.
|
||||
func validInterfaces() map[string]struct{} {
|
||||
defaultRoute, err := netmon.DefaultRoute()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return map[string]struct{}{defaultRoute.InterfaceName: {}}
|
||||
}
|
||||
@@ -729,15 +729,10 @@ func newResolverWithNameserver(nameservers []string) *osResolver {
|
||||
return r
|
||||
}
|
||||
|
||||
// Rfc1918Addresses returns the list of local physical interfaces private IP addresses
|
||||
// Rfc1918Addresses returns the list of local interfaces private IP addresses
|
||||
func Rfc1918Addresses() []string {
|
||||
vis := validInterfaces()
|
||||
var res []string
|
||||
netmon.ForeachInterface(func(i netmon.Interface, prefixes []netip.Prefix) {
|
||||
// Skip virtual interfaces.
|
||||
if _, existed := vis[i.Name]; !existed {
|
||||
return
|
||||
}
|
||||
addrs, _ := i.Addrs()
|
||||
for _, addr := range addrs {
|
||||
ipNet, ok := addr.(*net.IPNet)
|
||||
|
||||
@@ -282,35 +282,6 @@ func Test_Edns0_CacheReply(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/Control-D-Inc/ctrld/issues/255
|
||||
func Test_legacyResolverWithBigExtraSection(t *testing.T) {
|
||||
lanPC, err := net.ListenPacket("udp", "127.0.0.1:0") // 127.0.0.1 is considered LAN (loopback)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to listen on LAN address: %v", err)
|
||||
}
|
||||
lanServer, lanAddr, err := runLocalPacketConnTestServer(t, lanPC, bigExtraSectionHandler())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to run LAN test server: %v", err)
|
||||
}
|
||||
defer lanServer.Shutdown()
|
||||
|
||||
uc := &UpstreamConfig{
|
||||
Name: "Legacy",
|
||||
Type: ResolverTypeLegacy,
|
||||
Endpoint: lanAddr,
|
||||
}
|
||||
uc.Init()
|
||||
r, err := NewResolver(uc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = r.Resolve(context.Background(), uc.VerifyMsg())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_upstreamTypeFromEndpoint(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -399,68 +370,6 @@ func countHandler(call *atomic.Int64) dns.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func mustRR(s string) dns.RR {
|
||||
r, err := dns.NewRR(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func bigExtraSectionHandler() dns.HandlerFunc {
|
||||
return func(w dns.ResponseWriter, msg *dns.Msg) {
|
||||
m := &dns.Msg{
|
||||
Answer: []dns.RR{
|
||||
mustRR(". 7149 IN NS m.root-servers.net."),
|
||||
mustRR(". 7149 IN NS c.root-servers.net."),
|
||||
mustRR(". 7149 IN NS e.root-servers.net."),
|
||||
mustRR(". 7149 IN NS j.root-servers.net."),
|
||||
mustRR(". 7149 IN NS g.root-servers.net."),
|
||||
mustRR(". 7149 IN NS k.root-servers.net."),
|
||||
mustRR(". 7149 IN NS l.root-servers.net."),
|
||||
mustRR(". 7149 IN NS d.root-servers.net."),
|
||||
mustRR(". 7149 IN NS h.root-servers.net."),
|
||||
mustRR(". 7149 IN NS b.root-servers.net."),
|
||||
mustRR(". 7149 IN NS a.root-servers.net."),
|
||||
mustRR(". 7149 IN NS f.root-servers.net."),
|
||||
mustRR(". 7149 IN NS i.root-servers.net."),
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
mustRR("m.root-servers.net. 656 IN A 202.12.27.33"),
|
||||
mustRR("m.root-servers.net. 656 IN AAAA 2001:dc3::35"),
|
||||
mustRR("c.root-servers.net. 656 IN A 192.33.4.12"),
|
||||
mustRR("c.root-servers.net. 656 IN AAAA 2001:500:2::c"),
|
||||
mustRR("e.root-servers.net. 656 IN A 192.203.230.10"),
|
||||
mustRR("e.root-servers.net. 656 IN AAAA 2001:500:a8::e"),
|
||||
mustRR("j.root-servers.net. 656 IN A 192.58.128.30"),
|
||||
mustRR("j.root-servers.net. 656 IN AAAA 2001:503:c27::2:30"),
|
||||
mustRR("g.root-servers.net. 656 IN A 192.112.36.4"),
|
||||
mustRR("g.root-servers.net. 656 IN AAAA 2001:500:12::d0d"),
|
||||
mustRR("k.root-servers.net. 656 IN A 193.0.14.129"),
|
||||
mustRR("k.root-servers.net. 656 IN AAAA 2001:7fd::1"),
|
||||
mustRR("l.root-servers.net. 656 IN A 199.7.83.42"),
|
||||
mustRR("l.root-servers.net. 656 IN AAAA 2001:500:9f::42"),
|
||||
mustRR("d.root-servers.net. 656 IN A 199.7.91.13"),
|
||||
mustRR("d.root-servers.net. 656 IN AAAA 2001:500:2d::d"),
|
||||
mustRR("h.root-servers.net. 656 IN A 198.97.190.53"),
|
||||
mustRR("h.root-servers.net. 656 IN AAAA 2001:500:1::53"),
|
||||
mustRR("b.root-servers.net. 656 IN A 170.247.170.2"),
|
||||
mustRR("b.root-servers.net. 656 IN AAAA 2801:1b8:10::b"),
|
||||
mustRR("a.root-servers.net. 656 IN A 198.41.0.4"),
|
||||
mustRR("a.root-servers.net. 656 IN AAAA 2001:503:ba3e::2:30"),
|
||||
mustRR("f.root-servers.net. 656 IN A 192.5.5.241"),
|
||||
mustRR("f.root-servers.net. 656 IN AAAA 2001:500:2f::f"),
|
||||
mustRR("i.root-servers.net. 656 IN A 192.36.148.17"),
|
||||
mustRR("i.root-servers.net. 656 IN AAAA 2001:7fe::53"),
|
||||
},
|
||||
}
|
||||
|
||||
m.Compress = true
|
||||
m.SetReply(msg)
|
||||
w.WriteMsg(m)
|
||||
}
|
||||
}
|
||||
|
||||
func generateEdns0ClientCookie() string {
|
||||
cookie := make([]byte, 8)
|
||||
if _, err := rand.Read(cookie); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user