mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-08-10 13:20:33 +02:00
Merge pull request #93 from Control-D-Inc/release-branch-v1.3.1
Release branch v1.3.1
This commit is contained in:
@@ -76,7 +76,7 @@ $ go install github.com/Control-D-Inc/ctrld/cmd/ctrld@latest
|
|||||||
or
|
or
|
||||||
|
|
||||||
```
|
```
|
||||||
$ docker build -t controldns/ctrld .
|
$ docker build -t controldns/ctrld . -f docker/Dockerfile
|
||||||
$ docker run -d --name=ctrld -p 53:53/tcp -p 53:53/udp controldns/ctrld --cd=RESOLVER_ID_GOES_HERE -vv
|
$ docker run -d --name=ctrld -p 53:53/tcp -p 53:53/udp controldns/ctrld --cd=RESOLVER_ID_GOES_HERE -vv
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -188,8 +188,8 @@ See [Configuration Docs](docs/config.md).
|
|||||||
[listener]
|
[listener]
|
||||||
|
|
||||||
[listener.0]
|
[listener.0]
|
||||||
ip = "127.0.0.1"
|
ip = ""
|
||||||
port = 53
|
port = 0
|
||||||
restricted = false
|
restricted = false
|
||||||
|
|
||||||
[network]
|
[network]
|
||||||
@@ -220,6 +220,8 @@ See [Configuration Docs](docs/config.md).
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`ctrld` will pick a working config for `listener.0` then writing the default config to disk for the first run.
|
||||||
|
|
||||||
## Advanced Configuration
|
## Advanced Configuration
|
||||||
The above is the most basic example, which will work out of the box. If you're looking to do advanced configurations using policies, see [Configuration Docs](docs/config.md) for complete documentation of the config file.
|
The above is the most basic example, which will work out of the box. If you're looking to do advanced configurations using policies, see [Configuration Docs](docs/config.md) for complete documentation of the config file.
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ type ClientInfo struct {
|
|||||||
Mac string
|
Mac string
|
||||||
IP string
|
IP string
|
||||||
Hostname string
|
Hostname string
|
||||||
|
Self bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// LeaseFileFormat specifies the format of DHCP lease file.
|
// LeaseFileFormat specifies the format of DHCP lease file.
|
||||||
|
|||||||
+290
-192
@@ -124,185 +124,7 @@ func initCLI() {
|
|||||||
initConsoleLogging()
|
initConsoleLogging()
|
||||||
},
|
},
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
waitCh := make(chan struct{})
|
RunCobraCommand(cmd)
|
||||||
stopCh := make(chan struct{})
|
|
||||||
p := &prog{
|
|
||||||
waitCh: waitCh,
|
|
||||||
stopCh: stopCh,
|
|
||||||
cfg: &cfg,
|
|
||||||
}
|
|
||||||
if homedir == "" {
|
|
||||||
if dir, err := userHomeDir(); err == nil {
|
|
||||||
homedir = dir
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sockPath := filepath.Join(homedir, ctrldLogUnixSock)
|
|
||||||
if addr, err := net.ResolveUnixAddr("unix", sockPath); err == nil {
|
|
||||||
if conn, err := net.Dial(addr.Network(), addr.String()); err == nil {
|
|
||||||
lc := &logConn{conn: conn}
|
|
||||||
consoleWriter.Out = io.MultiWriter(os.Stdout, lc)
|
|
||||||
p.logConn = lc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if daemon && runtime.GOOS == "windows" {
|
|
||||||
mainLog.Load().Fatal().Msg("Cannot run in daemon mode. Please install a Windows service.")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !daemon {
|
|
||||||
// We need to call s.Run() as soon as possible to response to the OS manager, so it
|
|
||||||
// can see ctrld is running and don't mark ctrld as failed service.
|
|
||||||
go func() {
|
|
||||||
s, err := newService(p, svcConfig)
|
|
||||||
if err != nil {
|
|
||||||
mainLog.Load().Fatal().Err(err).Msg("failed create new service")
|
|
||||||
}
|
|
||||||
if err := s.Run(); err != nil {
|
|
||||||
mainLog.Load().Error().Err(err).Msg("failed to start service")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
noConfigStart := isNoConfigStart(cmd)
|
|
||||||
writeDefaultConfig := !noConfigStart && configBase64 == ""
|
|
||||||
tryReadingConfig(writeDefaultConfig)
|
|
||||||
|
|
||||||
readBase64Config(configBase64)
|
|
||||||
processNoConfigFlags(noConfigStart)
|
|
||||||
if err := v.Unmarshal(&cfg); err != nil {
|
|
||||||
mainLog.Load().Fatal().Msgf("failed to unmarshal config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
processLogAndCacheFlags()
|
|
||||||
|
|
||||||
// Log config do not have thing to validate, so it's safe to init log here,
|
|
||||||
// so it's able to log information in processCDFlags.
|
|
||||||
initLogging()
|
|
||||||
|
|
||||||
mainLog.Load().Info().Msgf("starting ctrld %s", curVersion())
|
|
||||||
mainLog.Load().Info().Msgf("os: %s", osVersion())
|
|
||||||
|
|
||||||
// Wait for network up.
|
|
||||||
if !ctrldnet.Up() {
|
|
||||||
mainLog.Load().Fatal().Msg("network is not up yet")
|
|
||||||
}
|
|
||||||
|
|
||||||
p.router = router.New(&cfg, cdUID != "")
|
|
||||||
cs, err := newControlServer(filepath.Join(homedir, ctrldControlUnixSock))
|
|
||||||
if err != nil {
|
|
||||||
mainLog.Load().Warn().Err(err).Msg("could not create control server")
|
|
||||||
}
|
|
||||||
p.cs = cs
|
|
||||||
|
|
||||||
// Processing --cd flag require connecting to ControlD API, which needs valid
|
|
||||||
// time for validating server certificate. Some routers need NTP synchronization
|
|
||||||
// to set the current time, so this check must happen before processCDFlags.
|
|
||||||
if err := p.router.PreRun(); err != nil {
|
|
||||||
mainLog.Load().Fatal().Err(err).Msg("failed to perform router pre-run check")
|
|
||||||
}
|
|
||||||
|
|
||||||
oldLogPath := cfg.Service.LogPath
|
|
||||||
if uid := cdUIDFromProvToken(); uid != "" {
|
|
||||||
cdUID = uid
|
|
||||||
}
|
|
||||||
if cdUID != "" {
|
|
||||||
processCDFlags()
|
|
||||||
}
|
|
||||||
|
|
||||||
updated := updateListenerConfig()
|
|
||||||
|
|
||||||
if cdUID != "" {
|
|
||||||
processLogAndCacheFlags()
|
|
||||||
}
|
|
||||||
|
|
||||||
if updated {
|
|
||||||
if err := writeConfigFile(); err != nil {
|
|
||||||
mainLog.Load().Fatal().Err(err).Msg("failed to write config file")
|
|
||||||
} else {
|
|
||||||
mainLog.Load().Info().Msg("writing config file to: " + defaultConfigFile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if newLogPath := cfg.Service.LogPath; newLogPath != "" && oldLogPath != newLogPath {
|
|
||||||
// After processCDFlags, log config may change, so reset mainLog and re-init logging.
|
|
||||||
l := zerolog.New(io.Discard)
|
|
||||||
mainLog.Store(&l)
|
|
||||||
|
|
||||||
// Copy logs written so far to new log file if possible.
|
|
||||||
if buf, err := os.ReadFile(oldLogPath); err == nil {
|
|
||||||
if err := os.WriteFile(newLogPath, buf, os.FileMode(0o600)); err != nil {
|
|
||||||
mainLog.Load().Warn().Err(err).Msg("could not copy old log file")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
initLoggingWithBackup(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
validateConfig(&cfg)
|
|
||||||
initCache()
|
|
||||||
|
|
||||||
if daemon {
|
|
||||||
exe, err := os.Executable()
|
|
||||||
if err != nil {
|
|
||||||
mainLog.Load().Error().Err(err).Msg("failed to find the binary")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
curDir, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
mainLog.Load().Error().Err(err).Msg("failed to get current working directory")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
// If running as daemon, re-run the command in background, with daemon off.
|
|
||||||
cmd := exec.Command(exe, append(os.Args[1:], "-d=false")...)
|
|
||||||
cmd.Dir = curDir
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
mainLog.Load().Error().Err(err).Msg("failed to start process as daemon")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
mainLog.Load().Info().Int("pid", cmd.Process.Pid).Msg("DNS proxy started")
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
p.onStarted = append(p.onStarted, func() {
|
|
||||||
for _, lc := range p.cfg.Listener {
|
|
||||||
if shouldAllocateLoopbackIP(lc.IP) {
|
|
||||||
if err := allocateIP(lc.IP); err != nil {
|
|
||||||
mainLog.Load().Error().Err(err).Msgf("could not allocate IP: %s", lc.IP)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
p.onStopped = append(p.onStopped, func() {
|
|
||||||
for _, lc := range p.cfg.Listener {
|
|
||||||
if shouldAllocateLoopbackIP(lc.IP) {
|
|
||||||
if err := deAllocateIP(lc.IP); err != nil {
|
|
||||||
mainLog.Load().Error().Err(err).Msgf("could not de-allocate IP: %s", lc.IP)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if platform := router.Name(); platform != "" {
|
|
||||||
if cp := router.CertPool(); cp != nil {
|
|
||||||
rootCertPool = cp
|
|
||||||
}
|
|
||||||
p.onStarted = append(p.onStarted, func() {
|
|
||||||
mainLog.Load().Debug().Msg("router setup on start")
|
|
||||||
if err := p.router.Setup(); err != nil {
|
|
||||||
mainLog.Load().Error().Err(err).Msg("could not configure router")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
p.onStopped = append(p.onStopped, func() {
|
|
||||||
mainLog.Load().Debug().Msg("router cleanup on stop")
|
|
||||||
if err := p.router.Cleanup(); err != nil {
|
|
||||||
mainLog.Load().Error().Err(err).Msg("could not cleanup router")
|
|
||||||
}
|
|
||||||
p.resetDNS()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
close(waitCh)
|
|
||||||
<-stopCh
|
|
||||||
for _, f := range p.onStopped {
|
|
||||||
f()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
runCmd.Flags().BoolVarP(&daemon, "daemon", "d", false, "Run as daemon")
|
runCmd.Flags().BoolVarP(&daemon, "daemon", "d", false, "Run as daemon")
|
||||||
@@ -314,8 +136,8 @@ func initCLI() {
|
|||||||
runCmd.Flags().StringSliceVarP(&domains, "domains", "", nil, "List of domain to apply in a split DNS policy")
|
runCmd.Flags().StringSliceVarP(&domains, "domains", "", nil, "List of domain to apply in a split DNS policy")
|
||||||
runCmd.Flags().StringVarP(&logPath, "log", "", "", "Path to log file")
|
runCmd.Flags().StringVarP(&logPath, "log", "", "", "Path to log file")
|
||||||
runCmd.Flags().IntVarP(&cacheSize, "cache_size", "", 0, "Enable cache with size items")
|
runCmd.Flags().IntVarP(&cacheSize, "cache_size", "", 0, "Enable cache with size items")
|
||||||
runCmd.Flags().StringVarP(&cdUID, "cd", "", "", "Control D resolver uid")
|
runCmd.Flags().StringVarP(&cdUID, cdUidFlagName, "", "", "Control D resolver uid")
|
||||||
runCmd.Flags().StringVarP(&cdOrg, "cd-org", "", "", "Control D provision token")
|
runCmd.Flags().StringVarP(&cdOrg, cdOrgFlagName, "", "", "Control D provision token")
|
||||||
runCmd.Flags().BoolVarP(&cdDev, "dev", "", false, "Use Control D dev resolver/domain")
|
runCmd.Flags().BoolVarP(&cdDev, "dev", "", false, "Use Control D dev resolver/domain")
|
||||||
_ = runCmd.Flags().MarkHidden("dev")
|
_ = runCmd.Flags().MarkHidden("dev")
|
||||||
runCmd.Flags().StringVarP(&homedir, "homedir", "", "", "")
|
runCmd.Flags().StringVarP(&homedir, "homedir", "", "", "")
|
||||||
@@ -334,6 +156,8 @@ func initCLI() {
|
|||||||
Short: "Install and start the ctrld service",
|
Short: "Install and start the ctrld service",
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
checkStrFlagEmpty(cmd, cdUidFlagName)
|
||||||
|
checkStrFlagEmpty(cmd, cdOrgFlagName)
|
||||||
sc := &service.Config{}
|
sc := &service.Config{}
|
||||||
*sc = *svcConfig
|
*sc = *svcConfig
|
||||||
osArgs := os.Args[2:]
|
osArgs := os.Args[2:]
|
||||||
@@ -466,8 +290,8 @@ func initCLI() {
|
|||||||
startCmd.Flags().StringSliceVarP(&domains, "domains", "", nil, "List of domain to apply in a split DNS policy")
|
startCmd.Flags().StringSliceVarP(&domains, "domains", "", nil, "List of domain to apply in a split DNS policy")
|
||||||
startCmd.Flags().StringVarP(&logPath, "log", "", "", "Path to log file")
|
startCmd.Flags().StringVarP(&logPath, "log", "", "", "Path to log file")
|
||||||
startCmd.Flags().IntVarP(&cacheSize, "cache_size", "", 0, "Enable cache with size items")
|
startCmd.Flags().IntVarP(&cacheSize, "cache_size", "", 0, "Enable cache with size items")
|
||||||
startCmd.Flags().StringVarP(&cdUID, "cd", "", "", "Control D resolver uid")
|
startCmd.Flags().StringVarP(&cdUID, cdUidFlagName, "", "", "Control D resolver uid")
|
||||||
startCmd.Flags().StringVarP(&cdOrg, "cd-org", "", "", "Control D provision token")
|
startCmd.Flags().StringVarP(&cdOrg, cdOrgFlagName, "", "", "Control D provision token")
|
||||||
startCmd.Flags().BoolVarP(&cdDev, "dev", "", false, "Use Control D dev resolver/domain")
|
startCmd.Flags().BoolVarP(&cdDev, "dev", "", false, "Use Control D dev resolver/domain")
|
||||||
_ = startCmd.Flags().MarkHidden("dev")
|
_ = startCmd.Flags().MarkHidden("dev")
|
||||||
startCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`)
|
startCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`)
|
||||||
@@ -804,6 +628,9 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`,
|
|||||||
map2Slice := func(m map[string]struct{}) []string {
|
map2Slice := func(m map[string]struct{}) []string {
|
||||||
s := make([]string, 0, len(m))
|
s := make([]string, 0, len(m))
|
||||||
for k := range m {
|
for k := range m {
|
||||||
|
if k == "" { // skip empty source from output.
|
||||||
|
continue
|
||||||
|
}
|
||||||
s = append(s, k)
|
s = append(s, k)
|
||||||
}
|
}
|
||||||
sort.Strings(s)
|
sort.Strings(s)
|
||||||
@@ -838,6 +665,222 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`,
|
|||||||
rootCmd.AddCommand(clientsCmd)
|
rootCmd.AddCommand(clientsCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isMobile reports whether the current OS is a mobile platform.
|
||||||
|
func isMobile() bool {
|
||||||
|
return runtime.GOOS == "android" || runtime.GOOS == "ios"
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunCobraCommand runs ctrld cli.
|
||||||
|
func RunCobraCommand(cmd *cobra.Command) {
|
||||||
|
noConfigStart = isNoConfigStart(cmd)
|
||||||
|
checkStrFlagEmpty(cmd, cdUidFlagName)
|
||||||
|
checkStrFlagEmpty(cmd, cdOrgFlagName)
|
||||||
|
run(nil, make(chan struct{}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunMobile runs the ctrld cli on mobile platforms.
|
||||||
|
func RunMobile(appConfig *AppConfig, appCallback *AppCallback, stopCh chan struct{}) {
|
||||||
|
if appConfig == nil {
|
||||||
|
panic("appConfig is nil")
|
||||||
|
}
|
||||||
|
initConsoleLogging()
|
||||||
|
noConfigStart = false
|
||||||
|
homedir = appConfig.HomeDir
|
||||||
|
verbose = appConfig.Verbose
|
||||||
|
cdUID = appConfig.CdUID
|
||||||
|
logPath = appConfig.LogPath
|
||||||
|
run(appCallback, stopCh)
|
||||||
|
}
|
||||||
|
|
||||||
|
// run runs ctrld cli with given app callback and stop channel.
|
||||||
|
func run(appCallback *AppCallback, stopCh chan struct{}) {
|
||||||
|
if stopCh == nil {
|
||||||
|
mainLog.Load().Fatal().Msg("stopCh is nil")
|
||||||
|
}
|
||||||
|
waitCh := make(chan struct{})
|
||||||
|
p := &prog{
|
||||||
|
waitCh: waitCh,
|
||||||
|
stopCh: stopCh,
|
||||||
|
cfg: &cfg,
|
||||||
|
appCallback: appCallback,
|
||||||
|
}
|
||||||
|
if homedir == "" {
|
||||||
|
if dir, err := userHomeDir(); err == nil {
|
||||||
|
homedir = dir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sockPath := filepath.Join(homedir, ctrldLogUnixSock)
|
||||||
|
if addr, err := net.ResolveUnixAddr("unix", sockPath); err == nil {
|
||||||
|
if conn, err := net.Dial(addr.Network(), addr.String()); err == nil {
|
||||||
|
lc := &logConn{conn: conn}
|
||||||
|
consoleWriter.Out = io.MultiWriter(os.Stdout, lc)
|
||||||
|
p.logConn = lc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if daemon && runtime.GOOS == "windows" {
|
||||||
|
mainLog.Load().Fatal().Msg("Cannot run in daemon mode. Please install a Windows service.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !daemon {
|
||||||
|
// We need to call s.Run() as soon as possible to response to the OS manager, so it
|
||||||
|
// can see ctrld is running and don't mark ctrld as failed service.
|
||||||
|
go func() {
|
||||||
|
s, err := newService(p, svcConfig)
|
||||||
|
if err != nil {
|
||||||
|
mainLog.Load().Fatal().Err(err).Msg("failed create new service")
|
||||||
|
}
|
||||||
|
if err := s.Run(); err != nil {
|
||||||
|
mainLog.Load().Error().Err(err).Msg("failed to start service")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
writeDefaultConfig := !noConfigStart && configBase64 == ""
|
||||||
|
tryReadingConfig(writeDefaultConfig)
|
||||||
|
|
||||||
|
readBase64Config(configBase64)
|
||||||
|
processNoConfigFlags(noConfigStart)
|
||||||
|
if err := v.Unmarshal(&cfg); err != nil {
|
||||||
|
mainLog.Load().Fatal().Msgf("failed to unmarshal config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
processLogAndCacheFlags()
|
||||||
|
|
||||||
|
// Log config do not have thing to validate, so it's safe to init log here,
|
||||||
|
// so it's able to log information in processCDFlags.
|
||||||
|
initLogging()
|
||||||
|
|
||||||
|
mainLog.Load().Info().Msgf("starting ctrld %s", curVersion())
|
||||||
|
mainLog.Load().Info().Msgf("os: %s", osVersion())
|
||||||
|
|
||||||
|
// Wait for network up.
|
||||||
|
if !ctrldnet.Up() {
|
||||||
|
mainLog.Load().Fatal().Msg("network is not up yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
p.router = router.New(&cfg, cdUID != "")
|
||||||
|
cs, err := newControlServer(filepath.Join(homedir, ctrldControlUnixSock))
|
||||||
|
if err != nil {
|
||||||
|
mainLog.Load().Warn().Err(err).Msg("could not create control server")
|
||||||
|
}
|
||||||
|
p.cs = cs
|
||||||
|
|
||||||
|
// Processing --cd flag require connecting to ControlD API, which needs valid
|
||||||
|
// time for validating server certificate. Some routers need NTP synchronization
|
||||||
|
// to set the current time, so this check must happen before processCDFlags.
|
||||||
|
if err := p.router.PreRun(); err != nil {
|
||||||
|
mainLog.Load().Fatal().Err(err).Msg("failed to perform router pre-run check")
|
||||||
|
}
|
||||||
|
|
||||||
|
oldLogPath := cfg.Service.LogPath
|
||||||
|
if uid := cdUIDFromProvToken(); uid != "" {
|
||||||
|
cdUID = uid
|
||||||
|
}
|
||||||
|
if cdUID != "" {
|
||||||
|
err := processCDFlags()
|
||||||
|
if err != nil {
|
||||||
|
appCallback.Exit(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updated := updateListenerConfig()
|
||||||
|
|
||||||
|
if cdUID != "" {
|
||||||
|
processLogAndCacheFlags()
|
||||||
|
}
|
||||||
|
|
||||||
|
if updated {
|
||||||
|
if err := writeConfigFile(); err != nil {
|
||||||
|
mainLog.Load().Fatal().Err(err).Msg("failed to write config file")
|
||||||
|
} else {
|
||||||
|
mainLog.Load().Info().Msg("writing config file to: " + defaultConfigFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if newLogPath := cfg.Service.LogPath; newLogPath != "" && oldLogPath != newLogPath {
|
||||||
|
// After processCDFlags, log config may change, so reset mainLog and re-init logging.
|
||||||
|
l := zerolog.New(io.Discard)
|
||||||
|
mainLog.Store(&l)
|
||||||
|
|
||||||
|
// Copy logs written so far to new log file if possible.
|
||||||
|
if buf, err := os.ReadFile(oldLogPath); err == nil {
|
||||||
|
if err := os.WriteFile(newLogPath, buf, os.FileMode(0o600)); err != nil {
|
||||||
|
mainLog.Load().Warn().Err(err).Msg("could not copy old log file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
initLoggingWithBackup(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
validateConfig(&cfg)
|
||||||
|
initCache()
|
||||||
|
|
||||||
|
if daemon {
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
mainLog.Load().Error().Err(err).Msg("failed to find the binary")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
curDir, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
mainLog.Load().Error().Err(err).Msg("failed to get current working directory")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
// If running as daemon, re-run the command in background, with daemon off.
|
||||||
|
cmd := exec.Command(exe, append(os.Args[1:], "-d=false")...)
|
||||||
|
cmd.Dir = curDir
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
mainLog.Load().Error().Err(err).Msg("failed to start process as daemon")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
mainLog.Load().Info().Int("pid", cmd.Process.Pid).Msg("DNS proxy started")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.onStarted = append(p.onStarted, func() {
|
||||||
|
for _, lc := range p.cfg.Listener {
|
||||||
|
if shouldAllocateLoopbackIP(lc.IP) {
|
||||||
|
if err := allocateIP(lc.IP); err != nil {
|
||||||
|
mainLog.Load().Error().Err(err).Msgf("could not allocate IP: %s", lc.IP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
p.onStopped = append(p.onStopped, func() {
|
||||||
|
for _, lc := range p.cfg.Listener {
|
||||||
|
if shouldAllocateLoopbackIP(lc.IP) {
|
||||||
|
if err := deAllocateIP(lc.IP); err != nil {
|
||||||
|
mainLog.Load().Error().Err(err).Msgf("could not de-allocate IP: %s", lc.IP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if platform := router.Name(); platform != "" {
|
||||||
|
if cp := router.CertPool(); cp != nil {
|
||||||
|
rootCertPool = cp
|
||||||
|
}
|
||||||
|
p.onStarted = append(p.onStarted, func() {
|
||||||
|
mainLog.Load().Debug().Msg("router setup on start")
|
||||||
|
if err := p.router.Setup(); err != nil {
|
||||||
|
mainLog.Load().Error().Err(err).Msg("could not configure router")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
p.onStopped = append(p.onStopped, func() {
|
||||||
|
mainLog.Load().Debug().Msg("router cleanup on stop")
|
||||||
|
if err := p.router.Cleanup(); err != nil {
|
||||||
|
mainLog.Load().Error().Err(err).Msg("could not cleanup router")
|
||||||
|
}
|
||||||
|
p.resetDNS()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
close(waitCh)
|
||||||
|
<-stopCh
|
||||||
|
for _, f := range p.onStopped {
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func writeConfigFile() error {
|
func writeConfigFile() error {
|
||||||
if cfu := v.ConfigFileUsed(); cfu != "" {
|
if cfu := v.ConfigFileUsed(); cfu != "" {
|
||||||
defaultConfigFile = cfu
|
defaultConfigFile = cfu
|
||||||
@@ -882,6 +925,7 @@ func readConfigFile(writeDefaultConfig bool) bool {
|
|||||||
if err := v.Unmarshal(&cfg); err != nil {
|
if err := v.Unmarshal(&cfg); err != nil {
|
||||||
mainLog.Load().Fatal().Msgf("failed to unmarshal default config: %v", err)
|
mainLog.Load().Fatal().Msgf("failed to unmarshal default config: %v", err)
|
||||||
}
|
}
|
||||||
|
_ = updateListenerConfig()
|
||||||
if err := writeConfigFile(); err != nil {
|
if err := writeConfigFile(); err != nil {
|
||||||
mainLog.Load().Fatal().Msgf("failed to write default config file: %v", err)
|
mainLog.Load().Fatal().Msgf("failed to write default config file: %v", err)
|
||||||
} else {
|
} else {
|
||||||
@@ -971,7 +1015,7 @@ func processNoConfigFlags(noConfigStart bool) {
|
|||||||
v.Set("upstream", upstream)
|
v.Set("upstream", upstream)
|
||||||
}
|
}
|
||||||
|
|
||||||
func processCDFlags() {
|
func processCDFlags() error {
|
||||||
logger := mainLog.Load().With().Str("mode", "cd").Logger()
|
logger := mainLog.Load().With().Str("mode", "cd").Logger()
|
||||||
logger.Info().Msgf("fetching Controld D configuration from API: %s", cdUID)
|
logger.Info().Msgf("fetching Controld D configuration from API: %s", cdUID)
|
||||||
bo := backoff.NewBackoff("processCDFlags", logf, 30*time.Second)
|
bo := backoff.NewBackoff("processCDFlags", logf, 30*time.Second)
|
||||||
@@ -991,12 +1035,12 @@ func processCDFlags() {
|
|||||||
s, err := newService(&prog{}, svcConfig)
|
s, err := newService(&prog{}, svcConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn().Err(err).Msg("failed to create new service")
|
logger.Warn().Err(err).Msg("failed to create new service")
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
if netIface, _ := netInterface(iface); netIface != nil {
|
if netIface, _ := netInterface(iface); netIface != nil {
|
||||||
if err := restoreNetworkManager(); err != nil {
|
if err := restoreNetworkManager(); err != nil {
|
||||||
logger.Error().Err(err).Msg("could not restore NetworkManager")
|
logger.Error().Err(err).Msg("could not restore NetworkManager")
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
logger.Debug().Str("iface", netIface.Name).Msg("Restoring DNS for interface")
|
logger.Debug().Str("iface", netIface.Name).Msg("Restoring DNS for interface")
|
||||||
if err := resetDNS(netIface); err != nil {
|
if err := resetDNS(netIface); err != nil {
|
||||||
@@ -1010,11 +1054,16 @@ func processCDFlags() {
|
|||||||
if doTasks(tasks) {
|
if doTasks(tasks) {
|
||||||
logger.Info().Msg("uninstalled service")
|
logger.Info().Msg("uninstalled service")
|
||||||
}
|
}
|
||||||
logger.Fatal().Err(uer).Msg("failed to fetch resolver config")
|
event := logger.Fatal()
|
||||||
|
if isMobile() {
|
||||||
|
event = logger.Warn()
|
||||||
|
}
|
||||||
|
event.Err(uer).Msg("failed to fetch resolver config")
|
||||||
|
return uer
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn().Err(err).Msg("could not fetch resolver config")
|
logger.Warn().Err(err).Msg("could not fetch resolver config")
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info().Msg("generating ctrld config from Control-D configuration")
|
logger.Info().Msg("generating ctrld config from Control-D configuration")
|
||||||
@@ -1058,6 +1107,7 @@ func processCDFlags() {
|
|||||||
"0": {IP: "", Port: 0},
|
"0": {IP: "", Port: 0},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func processListenFlag() {
|
func processListenFlag() {
|
||||||
@@ -1266,7 +1316,17 @@ func userHomeDir() (string, error) {
|
|||||||
}
|
}
|
||||||
// viper will expand for us.
|
// viper will expand for us.
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
return os.UserHomeDir()
|
// If we're on windows, use the install path for this.
|
||||||
|
exePath, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Dir(exePath), nil
|
||||||
|
}
|
||||||
|
// Mobile platform should provide a rw dir path for this.
|
||||||
|
if isMobile() {
|
||||||
|
return homedir, nil
|
||||||
}
|
}
|
||||||
dir = "/etc/controld"
|
dir = "/etc/controld"
|
||||||
if err := os.MkdirAll(dir, 0750); err != nil {
|
if err := os.MkdirAll(dir, 0750); err != nil {
|
||||||
@@ -1412,6 +1472,14 @@ type listenerConfigCheck struct {
|
|||||||
Port bool
|
Port bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mobileListenerPort returns hardcoded port for mobile platforms.
|
||||||
|
func mobileListenerPort() int {
|
||||||
|
if runtime.GOOS == "ios" {
|
||||||
|
return 53
|
||||||
|
}
|
||||||
|
return 5354
|
||||||
|
}
|
||||||
|
|
||||||
// updateListenerConfig updates the config for listeners if not defined,
|
// updateListenerConfig updates the config for listeners if not defined,
|
||||||
// or defined but invalid to be used, e.g: using loopback address other
|
// or defined but invalid to be used, e.g: using loopback address other
|
||||||
// than 127.0.0.1 with systemd-resolved.
|
// than 127.0.0.1 with systemd-resolved.
|
||||||
@@ -1435,7 +1503,25 @@ func updateListenerConfig() (updated bool) {
|
|||||||
}
|
}
|
||||||
updated = updated || lcc[n].IP || lcc[n].Port
|
updated = updated || lcc[n].IP || lcc[n].Port
|
||||||
}
|
}
|
||||||
|
if isMobile() {
|
||||||
|
// On Mobile, only use first listener, ignore others.
|
||||||
|
firstLn := cfg.FirstListener()
|
||||||
|
for k := range cfg.Listener {
|
||||||
|
if cfg.Listener[k] != firstLn {
|
||||||
|
delete(cfg.Listener, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// In cd mode, always use 127.0.0.1:5354.
|
||||||
|
if cdMode {
|
||||||
|
firstLn.IP = "127.0.0.1" // Mobile platforms allows running listener only on loop back address.
|
||||||
|
firstLn.Port = mobileListenerPort()
|
||||||
|
// TODO: use clear(lcc) once upgrading to go 1.21
|
||||||
|
for k := range lcc {
|
||||||
|
delete(lcc, k)
|
||||||
|
}
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
var closers []io.Closer
|
var closers []io.Closer
|
||||||
defer func() {
|
defer func() {
|
||||||
for _, closer := range closers {
|
for _, closer := range closers {
|
||||||
@@ -1656,12 +1742,12 @@ func removeProvTokenFromArgs(sc *service.Config) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// For "--cd-org XXX", skip it and mark next arg skipped.
|
// For "--cd-org XXX", skip it and mark next arg skipped.
|
||||||
if x == "--cd-org" {
|
if x == cdOrgFlagName {
|
||||||
skip = true
|
skip = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// For "--cd-org=XXX", just skip it.
|
// For "--cd-org=XXX", just skip it.
|
||||||
if strings.HasPrefix(x, "--cd-org=") {
|
if strings.HasPrefix(x, cdOrgFlagName+"=") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
a = append(a, x)
|
a = append(a, x)
|
||||||
@@ -1700,3 +1786,15 @@ func newSocketControlClient(s service.Service, dir string) *controlClient {
|
|||||||
|
|
||||||
return cc
|
return cc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkStrFlagEmpty validates if a string flag was set to an empty string.
|
||||||
|
// If yes, emitting a fatal error message.
|
||||||
|
func checkStrFlagEmpty(cmd *cobra.Command, flagName string) {
|
||||||
|
fl := cmd.Flags().Lookup(flagName)
|
||||||
|
if !fl.Changed || fl.Value.Type() != "string" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if fl.Value.String() == "" {
|
||||||
|
mainLog.Load().Fatal().Msgf(`flag "--%s"" value must be non-empty`, fl.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+92
-67
@@ -5,10 +5,8 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -16,10 +14,9 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
"go4.org/mem"
|
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
"tailscale.com/net/interfaces"
|
"tailscale.com/net/interfaces"
|
||||||
"tailscale.com/util/lineread"
|
"tailscale.com/net/netaddr"
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld"
|
"github.com/Control-D-Inc/ctrld"
|
||||||
"github.com/Control-D-Inc/ctrld/internal/dnscache"
|
"github.com/Control-D-Inc/ctrld/internal/dnscache"
|
||||||
@@ -54,12 +51,12 @@ func (p *prog) serveDNS(listenerNum string) error {
|
|||||||
handler := dns.HandlerFunc(func(w dns.ResponseWriter, m *dns.Msg) {
|
handler := dns.HandlerFunc(func(w dns.ResponseWriter, m *dns.Msg) {
|
||||||
p.sema.acquire()
|
p.sema.acquire()
|
||||||
defer p.sema.release()
|
defer p.sema.release()
|
||||||
|
go p.detectLoop(m)
|
||||||
q := m.Question[0]
|
q := m.Question[0]
|
||||||
domain := canonicalName(q.Name)
|
domain := canonicalName(q.Name)
|
||||||
reqId := requestID()
|
reqId := requestID()
|
||||||
remoteIP, _, _ := net.SplitHostPort(w.RemoteAddr().String())
|
remoteIP, _, _ := net.SplitHostPort(w.RemoteAddr().String())
|
||||||
mac := macFromMsg(m)
|
ci := p.getClientInfo(remoteIP, m)
|
||||||
ci := p.getClientInfo(remoteIP, mac)
|
|
||||||
remoteAddr := spoofRemoteAddr(w.RemoteAddr(), ci)
|
remoteAddr := spoofRemoteAddr(w.RemoteAddr(), ci)
|
||||||
fmtSrcToDest := fmtRemoteToLocal(listenerNum, remoteAddr.String(), w.LocalAddr().String())
|
fmtSrcToDest := fmtRemoteToLocal(listenerNum, remoteAddr.String(), w.LocalAddr().String())
|
||||||
t := time.Now()
|
t := time.Now()
|
||||||
@@ -121,7 +118,8 @@ func (p *prog) serveDNS(listenerNum string) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
s, errCh := runDNSServer(dnsListenAddress(listenerConfig), proto, handler)
|
addr := net.JoinHostPort(listenerConfig.IP, strconv.Itoa(listenerConfig.Port))
|
||||||
|
s, errCh := runDNSServer(addr, proto, handler)
|
||||||
defer s.Shutdown()
|
defer s.Shutdown()
|
||||||
select {
|
select {
|
||||||
case err := <-errCh:
|
case err := <-errCh:
|
||||||
@@ -149,7 +147,7 @@ func (p *prog) serveDNS(listenerNum string) error {
|
|||||||
// processed later, because policy logging want to know whether a network rule
|
// processed later, because policy logging want to know whether a network rule
|
||||||
// is disregarded in favor of the domain level rule.
|
// is disregarded in favor of the domain level rule.
|
||||||
func (p *prog) upstreamFor(ctx context.Context, defaultUpstreamNum string, lc *ctrld.ListenerConfig, addr net.Addr, domain string) ([]string, bool) {
|
func (p *prog) upstreamFor(ctx context.Context, defaultUpstreamNum string, lc *ctrld.ListenerConfig, addr net.Addr, domain string) ([]string, bool) {
|
||||||
upstreams := []string{"upstream." + defaultUpstreamNum}
|
upstreams := []string{upstreamPrefix + defaultUpstreamNum}
|
||||||
matchedPolicy := "no policy"
|
matchedPolicy := "no policy"
|
||||||
matchedNetwork := "no network"
|
matchedNetwork := "no network"
|
||||||
matchedRule := "no rule"
|
matchedRule := "no rule"
|
||||||
@@ -233,7 +231,7 @@ func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []i
|
|||||||
upstreamConfigs := p.upstreamConfigsFromUpstreamNumbers(upstreams)
|
upstreamConfigs := p.upstreamConfigsFromUpstreamNumbers(upstreams)
|
||||||
if len(upstreamConfigs) == 0 {
|
if len(upstreamConfigs) == 0 {
|
||||||
upstreamConfigs = []*ctrld.UpstreamConfig{osUpstreamConfig}
|
upstreamConfigs = []*ctrld.UpstreamConfig{osUpstreamConfig}
|
||||||
upstreams = []string{"upstream.os"}
|
upstreams = []string{upstreamOS}
|
||||||
}
|
}
|
||||||
// Inverse query should not be cached: https://www.rfc-editor.org/rfc/rfc1035#section-7.4
|
// Inverse query should not be cached: https://www.rfc-editor.org/rfc/rfc1035#section-7.4
|
||||||
if p.cache != nil && msg.Question[0].Qtype != dns.TypePTR {
|
if p.cache != nil && msg.Question[0].Qtype != dns.TypePTR {
|
||||||
@@ -277,6 +275,12 @@ func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []i
|
|||||||
answer, err := resolve1(n, upstreamConfig, msg)
|
answer, err := resolve1(n, upstreamConfig, msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctrld.Log(ctx, mainLog.Load().Error().Err(err), "failed to resolve query")
|
ctrld.Log(ctx, mainLog.Load().Error().Err(err), "failed to resolve query")
|
||||||
|
if errNetworkError(err) {
|
||||||
|
p.um.increaseFailureCount(upstreams[n])
|
||||||
|
if p.um.isDown(upstreams[n]) {
|
||||||
|
go p.um.checkUpstream(upstreams[n], upstreamConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return answer
|
return answer
|
||||||
@@ -285,6 +289,14 @@ func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []i
|
|||||||
if upstreamConfig == nil {
|
if upstreamConfig == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if p.isLoop(upstreamConfig) {
|
||||||
|
mainLog.Load().Warn().Msgf("dns loop detected, upstream: %q, endpoint: %q", upstreamConfig.Name, upstreamConfig.Endpoint)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if p.um.isDown(upstreams[n]) {
|
||||||
|
ctrld.Log(ctx, mainLog.Load().Warn(), "%s is down", upstreams[n])
|
||||||
|
continue
|
||||||
|
}
|
||||||
answer := resolve(n, upstreamConfig, msg)
|
answer := resolve(n, upstreamConfig, msg)
|
||||||
if answer == nil {
|
if answer == nil {
|
||||||
if serveStaleCache && staleAnswer != nil {
|
if serveStaleCache && staleAnswer != nil {
|
||||||
@@ -316,7 +328,7 @@ func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []i
|
|||||||
}
|
}
|
||||||
return answer
|
return answer
|
||||||
}
|
}
|
||||||
ctrld.Log(ctx, mainLog.Load().Error(), "all upstreams failed")
|
ctrld.Log(ctx, mainLog.Load().Error(), "all %v endpoints failed", upstreams)
|
||||||
answer := new(dns.Msg)
|
answer := new(dns.Msg)
|
||||||
answer.SetRcode(msg, dns.RcodeServerFailure)
|
answer.SetRcode(msg, dns.RcodeServerFailure)
|
||||||
return answer
|
return answer
|
||||||
@@ -325,7 +337,7 @@ func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []i
|
|||||||
func (p *prog) upstreamConfigsFromUpstreamNumbers(upstreams []string) []*ctrld.UpstreamConfig {
|
func (p *prog) upstreamConfigsFromUpstreamNumbers(upstreams []string) []*ctrld.UpstreamConfig {
|
||||||
upstreamConfigs := make([]*ctrld.UpstreamConfig, 0, len(upstreams))
|
upstreamConfigs := make([]*ctrld.UpstreamConfig, 0, len(upstreams))
|
||||||
for _, upstream := range upstreams {
|
for _, upstream := range upstreams {
|
||||||
upstreamNum := strings.TrimPrefix(upstream, "upstream.")
|
upstreamNum := strings.TrimPrefix(upstream, upstreamPrefix)
|
||||||
upstreamConfigs = append(upstreamConfigs, p.cfg.Upstream[upstreamNum])
|
upstreamConfigs = append(upstreamConfigs, p.cfg.Upstream[upstreamNum])
|
||||||
}
|
}
|
||||||
return upstreamConfigs
|
return upstreamConfigs
|
||||||
@@ -422,29 +434,24 @@ func needLocalIPv6Listener() bool {
|
|||||||
return ctrldnet.SupportsIPv6ListenLocal() && runtime.GOOS == "windows"
|
return ctrldnet.SupportsIPv6ListenLocal() && runtime.GOOS == "windows"
|
||||||
}
|
}
|
||||||
|
|
||||||
func dnsListenAddress(lc *ctrld.ListenerConfig) string {
|
// ipAndMacFromMsg extracts IP and MAC information included in a DNS message, if any.
|
||||||
// If we are inside container and the listener loopback address, change
|
func ipAndMacFromMsg(msg *dns.Msg) (string, string) {
|
||||||
// the address to something like 0.0.0.0:53, so user can expose the port to outside.
|
ip, mac := "", ""
|
||||||
if inContainer() {
|
|
||||||
if ip := net.ParseIP(lc.IP); ip != nil && ip.IsLoopback() {
|
|
||||||
return net.JoinHostPort("0.0.0.0", strconv.Itoa(lc.Port))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return net.JoinHostPort(lc.IP, strconv.Itoa(lc.Port))
|
|
||||||
}
|
|
||||||
|
|
||||||
func macFromMsg(msg *dns.Msg) string {
|
|
||||||
if opt := msg.IsEdns0(); opt != nil {
|
if opt := msg.IsEdns0(); opt != nil {
|
||||||
for _, s := range opt.Option {
|
for _, s := range opt.Option {
|
||||||
switch e := s.(type) {
|
switch e := s.(type) {
|
||||||
case *dns.EDNS0_LOCAL:
|
case *dns.EDNS0_LOCAL:
|
||||||
if e.Code == EDNS0_OPTION_MAC {
|
if e.Code == EDNS0_OPTION_MAC {
|
||||||
return net.HardwareAddr(e.Data).String()
|
mac = net.HardwareAddr(e.Data).String()
|
||||||
|
}
|
||||||
|
case *dns.EDNS0_SUBNET:
|
||||||
|
if len(e.Address) > 0 && !e.Address.IsLoopback() {
|
||||||
|
ip = e.Address.String()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ""
|
return ip, mac
|
||||||
}
|
}
|
||||||
|
|
||||||
func spoofRemoteAddr(addr net.Addr, ci *ctrld.ClientInfo) net.Addr {
|
func spoofRemoteAddr(addr net.Addr, ci *ctrld.ClientInfo) net.Addr {
|
||||||
@@ -498,55 +505,73 @@ func runDNSServer(addr, network string, handler dns.Handler) (*dns.Server, <-cha
|
|||||||
return s, errCh
|
return s, errCh
|
||||||
}
|
}
|
||||||
|
|
||||||
// inContainer reports whether we're running in a container.
|
func (p *prog) getClientInfo(remoteIP string, msg *dns.Msg) *ctrld.ClientInfo {
|
||||||
//
|
ci := &ctrld.ClientInfo{}
|
||||||
// Copied from https://github.com/tailscale/tailscale/blob/v1.42.0/hostinfo/hostinfo.go#L260
|
if p.appCallback != nil {
|
||||||
// with modification for ctrld usage.
|
ci.IP = p.appCallback.LanIp()
|
||||||
func inContainer() bool {
|
ci.Mac = p.appCallback.MacAddress()
|
||||||
if runtime.GOOS != "linux" {
|
ci.Hostname = p.appCallback.HostName()
|
||||||
return false
|
ci.Self = true
|
||||||
|
return ci
|
||||||
|
}
|
||||||
|
ci.IP, ci.Mac = ipAndMacFromMsg(msg)
|
||||||
|
switch {
|
||||||
|
case ci.IP != "" && ci.Mac != "":
|
||||||
|
// Nothing to do.
|
||||||
|
case ci.IP == "" && ci.Mac != "":
|
||||||
|
// Have MAC, no IP.
|
||||||
|
ci.IP = p.ciTable.LookupIP(ci.Mac)
|
||||||
|
case ci.IP == "" && ci.Mac == "":
|
||||||
|
// Have nothing, use remote IP then lookup MAC.
|
||||||
|
ci.IP = remoteIP
|
||||||
|
fallthrough
|
||||||
|
case ci.IP != "" && ci.Mac == "":
|
||||||
|
// Have IP, no MAC.
|
||||||
|
ci.Mac = p.ciTable.LookupMac(ci.IP)
|
||||||
}
|
}
|
||||||
|
|
||||||
var ret bool
|
// If MAC is still empty here, that mean the requests are made from virtual interface,
|
||||||
if _, err := os.Stat("/.dockerenv"); err == nil {
|
// like VPN/Wireguard clients, so we use whatever MAC address associated with remoteIP
|
||||||
return true
|
// (most likely 127.0.0.1), and ci.IP as hostname, so we can distinguish those clients.
|
||||||
}
|
if ci.Mac == "" {
|
||||||
if _, err := os.Stat("/run/.containerenv"); err == nil {
|
ci.Mac = p.ciTable.LookupMac(remoteIP)
|
||||||
// See https://github.com/cri-o/cri-o/issues/5461
|
if hostname := p.ciTable.LookupHostname(ci.IP, ""); hostname != "" {
|
||||||
return true
|
ci.Hostname = hostname
|
||||||
}
|
} else {
|
||||||
lineread.File("/proc/1/cgroup", func(line []byte) error {
|
ci.Hostname = ci.IP
|
||||||
if mem.Contains(mem.B(line), mem.S("/docker/")) ||
|
p.ciTable.StoreVPNClient(ci)
|
||||||
mem.Contains(mem.B(line), mem.S("/lxc/")) {
|
|
||||||
ret = true
|
|
||||||
return io.EOF // arbitrary non-nil error to stop loop
|
|
||||||
}
|
}
|
||||||
return nil
|
} else {
|
||||||
})
|
ci.Hostname = p.ciTable.LookupHostname(ci.IP, ci.Mac)
|
||||||
lineread.File("/proc/mounts", func(line []byte) error {
|
}
|
||||||
if mem.Contains(mem.B(line), mem.S("lxcfs /proc/cpuinfo fuse.lxcfs")) {
|
ci.Self = queryFromSelf(ci.IP)
|
||||||
ret = true
|
return ci
|
||||||
return io.EOF
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
return ret
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *prog) getClientInfo(ip, mac string) *ctrld.ClientInfo {
|
// queryFromSelf reports whether the input IP is from device running ctrld.
|
||||||
ci := &ctrld.ClientInfo{}
|
func queryFromSelf(ip string) bool {
|
||||||
if mac != "" {
|
netIP := netip.MustParseAddr(ip)
|
||||||
ci.Mac = mac
|
ifaces, err := interfaces.GetList()
|
||||||
ci.IP = p.ciTable.LookupIP(mac)
|
if err != nil {
|
||||||
} else {
|
mainLog.Load().Warn().Err(err).Msg("could not get interfaces list")
|
||||||
ci.IP = ip
|
return false
|
||||||
ci.Mac = p.ciTable.LookupMac(ip)
|
}
|
||||||
if ip == "127.0.0.1" || ip == "::1" {
|
for _, iface := range ifaces {
|
||||||
ci.IP = p.ciTable.LookupIP(ci.Mac)
|
addrs, err := iface.Addrs()
|
||||||
|
if err != nil {
|
||||||
|
mainLog.Load().Warn().Err(err).Msgf("could not get interfaces addresses: %s", iface.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, a := range addrs {
|
||||||
|
switch v := a.(type) {
|
||||||
|
case *net.IPNet:
|
||||||
|
if pfx, ok := netaddr.FromStdIPNet(v); ok && pfx.Addr().Compare(netIP) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ci.Hostname = p.ciTable.LookupHostname(ci.IP, ci.Mac)
|
return false
|
||||||
return ci
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func needRFC1918Listeners(lc *ctrld.ListenerConfig) bool {
|
func needRFC1918Listeners(lc *ctrld.ListenerConfig) bool {
|
||||||
|
|||||||
@@ -156,19 +156,27 @@ func TestCache(t *testing.T) {
|
|||||||
assert.Equal(t, answer2.Rcode, got2.Rcode)
|
assert.Equal(t, answer2.Rcode, got2.Rcode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_macFromMsg(t *testing.T) {
|
func Test_ipAndMacFromMsg(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
ip string
|
||||||
|
wantIp bool
|
||||||
mac string
|
mac string
|
||||||
wantMac bool
|
wantMac bool
|
||||||
}{
|
}{
|
||||||
{"has mac", "4c:20:b8:ab:87:1b", true},
|
{"has ip v4 and mac", "1.2.3.4", true, "4c:20:b8:ab:87:1b", true},
|
||||||
{"no mac", "4c:20:b8:ab:87:1b", false},
|
{"has ip v6 and mac", "2606:1a40:3::1", true, "4c:20:b8:ab:87:1b", true},
|
||||||
|
{"no ip", "1.2.3.4", false, "4c:20:b8:ab:87:1b", false},
|
||||||
|
{"no mac", "1.2.3.4", false, "4c:20:b8:ab:87:1b", false},
|
||||||
}
|
}
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
tc := tc
|
tc := tc
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
ip := net.ParseIP(tc.ip)
|
||||||
|
if ip == nil {
|
||||||
|
t.Fatal("missing IP")
|
||||||
|
}
|
||||||
hw, err := net.ParseMAC(tc.mac)
|
hw, err := net.ParseMAC(tc.mac)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -180,13 +188,23 @@ func Test_macFromMsg(t *testing.T) {
|
|||||||
ec1 := &dns.EDNS0_LOCAL{Code: EDNS0_OPTION_MAC, Data: hw}
|
ec1 := &dns.EDNS0_LOCAL{Code: EDNS0_OPTION_MAC, Data: hw}
|
||||||
o.Option = append(o.Option, ec1)
|
o.Option = append(o.Option, ec1)
|
||||||
}
|
}
|
||||||
m.Extra = append(m.Extra, o)
|
if tc.wantIp {
|
||||||
got := macFromMsg(m)
|
ec2 := &dns.EDNS0_SUBNET{Address: ip}
|
||||||
if tc.wantMac && got != tc.mac {
|
o.Option = append(o.Option, ec2)
|
||||||
t.Errorf("mismatch, want: %q, got: %q", tc.mac, got)
|
|
||||||
}
|
}
|
||||||
if !tc.wantMac && got != "" {
|
m.Extra = append(m.Extra, o)
|
||||||
t.Errorf("unexpected mac: %q", got)
|
gotIP, gotMac := ipAndMacFromMsg(m)
|
||||||
|
if tc.wantMac && gotMac != tc.mac {
|
||||||
|
t.Errorf("mismatch, want: %q, got: %q", tc.mac, gotMac)
|
||||||
|
}
|
||||||
|
if !tc.wantMac && gotMac != "" {
|
||||||
|
t.Errorf("unexpected mac: %q", gotMac)
|
||||||
|
}
|
||||||
|
if tc.wantIp && gotIP != tc.ip {
|
||||||
|
t.Errorf("mismatch, want: %q, got: %q", tc.ip, gotIP)
|
||||||
|
}
|
||||||
|
if !tc.wantIp && gotIP != "" {
|
||||||
|
t.Errorf("unexpected ip: %q", gotIP)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
// AppCallback provides hooks for injecting certain functionalities
|
||||||
|
// from mobile platforms to main ctrld cli.
|
||||||
|
type AppCallback struct {
|
||||||
|
HostName func() string
|
||||||
|
LanIp func() string
|
||||||
|
MacAddress func() string
|
||||||
|
Exit func(error string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppConfig allows overwriting ctrld cli flags from mobile platforms.
|
||||||
|
type AppConfig struct {
|
||||||
|
CdUID string
|
||||||
|
HomeDir string
|
||||||
|
Verbose int
|
||||||
|
LogPath string
|
||||||
|
}
|
||||||
+100
@@ -0,0 +1,100 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
|
||||||
|
"github.com/Control-D-Inc/ctrld"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
loopTestDomain = ".test"
|
||||||
|
loopTestQtype = dns.TypeTXT
|
||||||
|
)
|
||||||
|
|
||||||
|
// isLoop reports whether the given upstream config is detected as having DNS loop.
|
||||||
|
func (p *prog) isLoop(uc *ctrld.UpstreamConfig) bool {
|
||||||
|
p.loopMu.Lock()
|
||||||
|
defer p.loopMu.Unlock()
|
||||||
|
return p.loop[uc.UID()]
|
||||||
|
}
|
||||||
|
|
||||||
|
// detectLoop checks if the given DNS message is initialized sent by ctrld.
|
||||||
|
// If yes, marking the corresponding upstream as loop, prevent infinite DNS
|
||||||
|
// forwarding loop.
|
||||||
|
//
|
||||||
|
// See p.checkDnsLoop for more details how it works.
|
||||||
|
func (p *prog) detectLoop(msg *dns.Msg) {
|
||||||
|
if len(msg.Question) != 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
q := msg.Question[0]
|
||||||
|
if q.Qtype != loopTestQtype {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
unFQDNname := strings.TrimSuffix(q.Name, ".")
|
||||||
|
uid := strings.TrimSuffix(unFQDNname, loopTestDomain)
|
||||||
|
p.loopMu.Lock()
|
||||||
|
if _, loop := p.loop[uid]; loop {
|
||||||
|
p.loop[uid] = loop
|
||||||
|
}
|
||||||
|
p.loopMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkDnsLoop sends a message to check if there's any DNS forwarding loop
|
||||||
|
// with all the upstreams. The way it works based on dnsmasq --dns-loop-detect.
|
||||||
|
//
|
||||||
|
// - Generating a TXT test query and sending it to all upstream.
|
||||||
|
// - The test query is formed by upstream UID and test domain: <uid>.test
|
||||||
|
// - If the test query returns to ctrld, mark the corresponding upstream as loop (see p.detectLoop).
|
||||||
|
//
|
||||||
|
// See: https://thekelleys.org.uk/dnsmasq/docs/dnsmasq-man.html
|
||||||
|
func (p *prog) checkDnsLoop() {
|
||||||
|
mainLog.Load().Debug().Msg("start checking DNS loop")
|
||||||
|
upstream := make(map[string]*ctrld.UpstreamConfig)
|
||||||
|
p.loopMu.Lock()
|
||||||
|
for _, uc := range p.cfg.Upstream {
|
||||||
|
uid := uc.UID()
|
||||||
|
p.loop[uid] = false
|
||||||
|
upstream[uid] = uc
|
||||||
|
}
|
||||||
|
p.loopMu.Unlock()
|
||||||
|
|
||||||
|
for uid := range p.loop {
|
||||||
|
msg := loopTestMsg(uid)
|
||||||
|
uc := upstream[uid]
|
||||||
|
resolver, err := ctrld.NewResolver(uc)
|
||||||
|
if err != nil {
|
||||||
|
mainLog.Load().Warn().Err(err).Msgf("could not perform loop check for upstream: %q, endpoint: %q", uc.Name, uc.Endpoint)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, err := resolver.Resolve(context.Background(), msg); err != nil {
|
||||||
|
mainLog.Load().Warn().Err(err).Msgf("could not send DNS loop check query for upstream: %q, endpoint: %q", uc.Name, uc.Endpoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mainLog.Load().Debug().Msg("end checking DNS loop")
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkDnsLoopTicker performs p.checkDnsLoop every minute.
|
||||||
|
func (p *prog) checkDnsLoopTicker() {
|
||||||
|
timer := time.NewTicker(time.Minute)
|
||||||
|
defer timer.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-p.stopCh:
|
||||||
|
return
|
||||||
|
case <-timer.C:
|
||||||
|
p.checkDnsLoop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// loopTestMsg generates DNS message for checking loop.
|
||||||
|
func loopTestMsg(uid string) *dns.Msg {
|
||||||
|
msg := new(dns.Msg)
|
||||||
|
msg.SetQuestion(dns.Fqdn(uid+loopTestDomain), loopTestQtype)
|
||||||
|
return msg
|
||||||
|
}
|
||||||
@@ -35,6 +35,12 @@ var (
|
|||||||
|
|
||||||
mainLog atomic.Pointer[zerolog.Logger]
|
mainLog atomic.Pointer[zerolog.Logger]
|
||||||
consoleWriter zerolog.ConsoleWriter
|
consoleWriter zerolog.ConsoleWriter
|
||||||
|
noConfigStart bool
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
cdUidFlagName = "cd"
|
||||||
|
cdOrgFlagName = "cd-org"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -65,6 +71,7 @@ func normalizeLogFilePath(logFilePath string) string {
|
|||||||
return filepath.Join(dir, logFilePath)
|
return filepath.Join(dir, logFilePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initConsoleLogging initializes console logging, then storing to mainLog.
|
||||||
func initConsoleLogging() {
|
func initConsoleLogging() {
|
||||||
consoleWriter = zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) {
|
consoleWriter = zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) {
|
||||||
w.TimeFormat = time.StampMilli
|
w.TimeFormat = time.StampMilli
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package cli
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -16,13 +17,21 @@ const (
|
|||||||
dns=none
|
dns=none
|
||||||
systemd-resolved=false
|
systemd-resolved=false
|
||||||
`
|
`
|
||||||
nmSystemdUnitName = "NetworkManager.service"
|
nmSystemdUnitName = "NetworkManager.service"
|
||||||
systemdEnabledState = "enabled"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var networkManagerCtrldConfFile = filepath.Join(nmConfDir, nmCtrldConfFilename)
|
var networkManagerCtrldConfFile = filepath.Join(nmConfDir, nmCtrldConfFilename)
|
||||||
|
|
||||||
|
// hasNetworkManager reports whether NetworkManager executable found.
|
||||||
|
func hasNetworkManager() bool {
|
||||||
|
exe, _ := exec.LookPath("NetworkManager")
|
||||||
|
return exe != ""
|
||||||
|
}
|
||||||
|
|
||||||
func setupNetworkManager() error {
|
func setupNetworkManager() error {
|
||||||
|
if !hasNetworkManager() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if content, _ := os.ReadFile(nmCtrldConfContent); string(content) == nmCtrldConfContent {
|
if content, _ := os.ReadFile(nmCtrldConfContent); string(content) == nmCtrldConfContent {
|
||||||
mainLog.Load().Debug().Msg("NetworkManager already setup, nothing to do")
|
mainLog.Load().Debug().Msg("NetworkManager already setup, nothing to do")
|
||||||
return nil
|
return nil
|
||||||
@@ -43,6 +52,9 @@ func setupNetworkManager() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func restoreNetworkManager() error {
|
func restoreNetworkManager() error {
|
||||||
|
if !hasNetworkManager() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
err := os.Remove(networkManagerCtrldConfFile)
|
err := os.Remove(networkManagerCtrldConfFile)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
mainLog.Load().Debug().Msg("NetworkManager is not available")
|
mainLog.Load().Debug().Msg("NetworkManager is not available")
|
||||||
@@ -71,6 +83,7 @@ func reloadNetworkManager() {
|
|||||||
waitCh := make(chan string)
|
waitCh := make(chan string)
|
||||||
if _, err := conn.ReloadUnitContext(ctx, nmSystemdUnitName, "ignore-dependencies", waitCh); err != nil {
|
if _, err := conn.ReloadUnitContext(ctx, nmSystemdUnitName, "ignore-dependencies", waitCh); err != nil {
|
||||||
mainLog.Load().Debug().Err(err).Msg("could not reload NetworkManager")
|
mainLog.Load().Debug().Err(err).Msg("could not reload NetworkManager")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
<-waitCh
|
<-waitCh
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-3
@@ -9,7 +9,6 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"reflect"
|
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
@@ -85,8 +84,13 @@ func setDNS(iface *net.Interface, nameservers []string) error {
|
|||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if useSystemdResolved {
|
||||||
|
if out, err := exec.Command("systemctl", "restart", "systemd-resolved").CombinedOutput(); err != nil {
|
||||||
|
mainLog.Load().Warn().Err(err).Msgf("could not restart systemd-resolved: %s", string(out))
|
||||||
|
}
|
||||||
|
}
|
||||||
currentNS := currentDNS(iface)
|
currentNS := currentDNS(iface)
|
||||||
if reflect.DeepEqual(currentNS, nameservers) {
|
if isSubSet(nameservers, currentNS) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,7 +108,7 @@ func setDNS(iface *net.Interface, nameservers []string) error {
|
|||||||
return fmt.Errorf("%s: %w", string(out), err)
|
return fmt.Errorf("%s: %w", string(out), err)
|
||||||
}
|
}
|
||||||
currentNS := currentDNS(iface)
|
currentNS := currentDNS(iface)
|
||||||
if reflect.DeepEqual(currentNS, nameservers) {
|
if isSubSet(nameservers, currentNS) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
@@ -265,3 +269,33 @@ func ignoringEINTR(fn func() error) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isSubSet reports whether s2 contains all elements of s1.
|
||||||
|
func isSubSet(s1, s2 []string) bool {
|
||||||
|
ok := true
|
||||||
|
for _, ns := range s1 {
|
||||||
|
// TODO(cuonglm): use slices.Contains once upgrading to go1.21
|
||||||
|
if sliceContains(s2, ns) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ok = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// sliceContains reports whether v is present in s.
|
||||||
|
func sliceContains[S ~[]E, E comparable](s S, v E) bool {
|
||||||
|
return sliceIndex(s, v) >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// sliceIndex returns the index of the first occurrence of v in s,
|
||||||
|
// or -1 if not present.
|
||||||
|
func sliceIndex[S ~[]E, E comparable](s S, v E) int {
|
||||||
|
for i := range s {
|
||||||
|
if v == s[i] {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|||||||
+117
-45
@@ -1,13 +1,16 @@
|
|||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
@@ -25,6 +28,8 @@ const (
|
|||||||
defaultSemaphoreCap = 256
|
defaultSemaphoreCap = 256
|
||||||
ctrldLogUnixSock = "ctrld_start.sock"
|
ctrldLogUnixSock = "ctrld_start.sock"
|
||||||
ctrldControlUnixSock = "ctrld_control.sock"
|
ctrldControlUnixSock = "ctrld_control.sock"
|
||||||
|
upstreamPrefix = "upstream."
|
||||||
|
upstreamOS = upstreamPrefix + "os"
|
||||||
)
|
)
|
||||||
|
|
||||||
var logf = func(format string, args ...any) {
|
var logf = func(format string, args ...any) {
|
||||||
@@ -46,11 +51,16 @@ type prog struct {
|
|||||||
logConn net.Conn
|
logConn net.Conn
|
||||||
cs *controlServer
|
cs *controlServer
|
||||||
|
|
||||||
cfg *ctrld.Config
|
cfg *ctrld.Config
|
||||||
cache dnscache.Cacher
|
appCallback *AppCallback
|
||||||
sema semaphore
|
cache dnscache.Cacher
|
||||||
ciTable *clientinfo.Table
|
sema semaphore
|
||||||
router router.Router
|
ciTable *clientinfo.Table
|
||||||
|
um *upstreamMonitor
|
||||||
|
router router.Router
|
||||||
|
|
||||||
|
loopMu sync.Mutex
|
||||||
|
loop map[string]bool
|
||||||
|
|
||||||
started chan struct{}
|
started chan struct{}
|
||||||
onStartedDone chan struct{}
|
onStartedDone chan struct{}
|
||||||
@@ -84,6 +94,7 @@ func (p *prog) run() {
|
|||||||
numListeners := len(p.cfg.Listener)
|
numListeners := len(p.cfg.Listener)
|
||||||
p.started = make(chan struct{}, numListeners)
|
p.started = make(chan struct{}, numListeners)
|
||||||
p.onStartedDone = make(chan struct{})
|
p.onStartedDone = make(chan struct{})
|
||||||
|
p.loop = make(map[string]bool)
|
||||||
if p.cfg.Service.CacheEnable {
|
if p.cfg.Service.CacheEnable {
|
||||||
cacher, err := dnscache.NewLRUCache(p.cfg.Service.CacheSize)
|
cacher, err := dnscache.NewLRUCache(p.cfg.Service.CacheSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -114,6 +125,8 @@ func (p *prog) run() {
|
|||||||
nc.IPNets = append(nc.IPNets, ipNet)
|
nc.IPNets = append(nc.IPNets, ipNet)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.um = newUpstreamMonitor(p.cfg)
|
||||||
for n := range p.cfg.Upstream {
|
for n := range p.cfg.Upstream {
|
||||||
uc := p.cfg.Upstream[n]
|
uc := p.cfg.Upstream[n]
|
||||||
uc.Init()
|
uc.Init()
|
||||||
@@ -133,12 +146,14 @@ func (p *prog) run() {
|
|||||||
format := ctrld.LeaseFileFormat(p.cfg.Service.DHCPLeaseFileFormat)
|
format := ctrld.LeaseFileFormat(p.cfg.Service.DHCPLeaseFileFormat)
|
||||||
p.ciTable.AddLeaseFile(leaseFile, format)
|
p.ciTable.AddLeaseFile(leaseFile, format)
|
||||||
}
|
}
|
||||||
|
// Newer versions of android and iOS denies permission which breaks connectivity.
|
||||||
go func() {
|
if !isMobile() {
|
||||||
p.ciTable.Init()
|
go func() {
|
||||||
p.ciTable.RefreshLoop(p.stopCh)
|
p.ciTable.Init()
|
||||||
}()
|
p.ciTable.RefreshLoop(p.stopCh)
|
||||||
go p.watchLinkState()
|
}()
|
||||||
|
go p.watchLinkState()
|
||||||
|
}
|
||||||
|
|
||||||
for listenerNum := range p.cfg.Listener {
|
for listenerNum := range p.cfg.Listener {
|
||||||
p.cfg.Listener[listenerNum].Init()
|
p.cfg.Listener[listenerNum].Init()
|
||||||
@@ -163,8 +178,13 @@ func (p *prog) run() {
|
|||||||
for _, f := range p.onStarted {
|
for _, f := range p.onStarted {
|
||||||
f()
|
f()
|
||||||
}
|
}
|
||||||
|
// Check for possible DNS loop.
|
||||||
|
p.checkDnsLoop()
|
||||||
close(p.onStartedDone)
|
close(p.onStartedDone)
|
||||||
|
|
||||||
|
// Start check DNS loop ticker.
|
||||||
|
go p.checkDnsLoopTicker()
|
||||||
|
|
||||||
// Stop writing log to unix socket.
|
// Stop writing log to unix socket.
|
||||||
consoleWriter.Out = os.Stdout
|
consoleWriter.Out = os.Stdout
|
||||||
initLoggingWithBackup(false)
|
initLoggingWithBackup(false)
|
||||||
@@ -345,48 +365,100 @@ var (
|
|||||||
func errUrlNetworkError(err error) bool {
|
func errUrlNetworkError(err error) bool {
|
||||||
var urlErr *url.Error
|
var urlErr *url.Error
|
||||||
if errors.As(err, &urlErr) {
|
if errors.As(err, &urlErr) {
|
||||||
var opErr *net.OpError
|
return errNetworkError(urlErr.Err)
|
||||||
if errors.As(urlErr.Err, &opErr) {
|
}
|
||||||
if opErr.Temporary() {
|
return false
|
||||||
return true
|
}
|
||||||
}
|
|
||||||
switch {
|
func errNetworkError(err error) bool {
|
||||||
case errors.Is(opErr.Err, syscall.ECONNREFUSED),
|
var opErr *net.OpError
|
||||||
errors.Is(opErr.Err, syscall.EINVAL),
|
if errors.As(err, &opErr) {
|
||||||
errors.Is(opErr.Err, syscall.ENETUNREACH),
|
if opErr.Temporary() {
|
||||||
errors.Is(opErr.Err, windowsENETUNREACH),
|
return true
|
||||||
errors.Is(opErr.Err, windowsEINVAL),
|
}
|
||||||
errors.Is(opErr.Err, windowsECONNREFUSED):
|
switch {
|
||||||
return true
|
case errors.Is(opErr.Err, syscall.ECONNREFUSED),
|
||||||
}
|
errors.Is(opErr.Err, syscall.EINVAL),
|
||||||
|
errors.Is(opErr.Err, syscall.ENETUNREACH),
|
||||||
|
errors.Is(opErr.Err, windowsENETUNREACH),
|
||||||
|
errors.Is(opErr.Err, windowsEINVAL),
|
||||||
|
errors.Is(opErr.Err, windowsECONNREFUSED):
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// defaultRouteIP returns IP string of the default route if present, prefer IPv4 over IPv6.
|
func ifaceFirstPrivateIP(iface *net.Interface) string {
|
||||||
func defaultRouteIP() string {
|
if iface == nil {
|
||||||
if dr, err := interfaces.DefaultRoute(); err == nil {
|
return ""
|
||||||
if netIface, err := netInterface(dr.InterfaceName); err == nil {
|
}
|
||||||
addrs, _ := netIface.Addrs()
|
do := func(addrs []net.Addr, v4 bool) net.IP {
|
||||||
do := func(v4 bool) net.IP {
|
for _, addr := range addrs {
|
||||||
for _, addr := range addrs {
|
if netIP, ok := addr.(*net.IPNet); ok && netIP.IP.IsPrivate() {
|
||||||
if netIP, ok := addr.(*net.IPNet); ok && netIP.IP.IsPrivate() {
|
if v4 {
|
||||||
if v4 {
|
return netIP.IP.To4()
|
||||||
return netIP.IP.To4()
|
|
||||||
}
|
|
||||||
return netIP.IP
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return netIP.IP
|
||||||
}
|
|
||||||
if ip := do(true); ip != nil {
|
|
||||||
return ip.String()
|
|
||||||
}
|
|
||||||
if ip := do(false); ip != nil {
|
|
||||||
return ip.String()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
addrs, _ := iface.Addrs()
|
||||||
|
if ip := do(addrs, true); ip != nil {
|
||||||
|
return ip.String()
|
||||||
|
}
|
||||||
|
if ip := do(addrs, false); ip != nil {
|
||||||
|
return ip.String()
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// defaultRouteIP returns private IP string of the default route if present, prefer IPv4 over IPv6.
|
||||||
|
func defaultRouteIP() string {
|
||||||
|
dr, err := interfaces.DefaultRoute()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
drNetIface, err := netInterface(dr.InterfaceName)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
mainLog.Load().Debug().Str("iface", drNetIface.Name).Msg("checking default route interface")
|
||||||
|
if ip := ifaceFirstPrivateIP(drNetIface); ip != "" {
|
||||||
|
mainLog.Load().Debug().Str("ip", ip).Msg("found ip with default route interface")
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we reach here, it means the default route interface is connected directly to ISP.
|
||||||
|
// We need to find the LAN interface with the same Mac address with drNetIface.
|
||||||
|
//
|
||||||
|
// There could be multiple LAN interfaces with the same Mac address, so we find all private
|
||||||
|
// IPs then using the smallest one.
|
||||||
|
var addrs []netip.Addr
|
||||||
|
interfaces.ForeachInterface(func(i interfaces.Interface, prefixes []netip.Prefix) {
|
||||||
|
if i.Name == drNetIface.Name {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if bytes.Equal(i.HardwareAddr, drNetIface.HardwareAddr) {
|
||||||
|
for _, pfx := range prefixes {
|
||||||
|
addr := pfx.Addr()
|
||||||
|
if addr.IsPrivate() {
|
||||||
|
addrs = append(addrs, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(addrs) == 0 {
|
||||||
|
mainLog.Load().Warn().Msg("no default route IP found")
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
sort.Slice(addrs, func(i, j int) bool {
|
||||||
|
return addrs[i].Less(addrs[j])
|
||||||
|
})
|
||||||
|
|
||||||
|
ip := addrs[0].String()
|
||||||
|
mainLog.Load().Debug().Str("ip", ip).Msg("found LAN interface IP")
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"github.com/kardianos/service"
|
"github.com/kardianos/service"
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/dns"
|
"github.com/Control-D-Inc/ctrld/internal/dns"
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -21,9 +20,8 @@ func setDependencies(svc *service.Config) {
|
|||||||
"After=NetworkManager-wait-online.service",
|
"After=NetworkManager-wait-online.service",
|
||||||
"Wants=systemd-networkd-wait-online.service",
|
"Wants=systemd-networkd-wait-online.service",
|
||||||
"After=systemd-networkd-wait-online.service",
|
"After=systemd-networkd-wait-online.service",
|
||||||
}
|
"Wants=nss-lookup.target",
|
||||||
if routerDeps := router.ServiceDependencies(); len(routerDeps) > 0 {
|
"After=nss-lookup.target",
|
||||||
svc.Dependencies = append(svc.Dependencies, routerDeps...)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"tailscale.com/logtail/backoff"
|
||||||
|
|
||||||
|
"github.com/Control-D-Inc/ctrld"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// maxFailureRequest is the maximum failed queries allowed before an upstream is marked as down.
|
||||||
|
maxFailureRequest = 100
|
||||||
|
// checkUpstreamMaxBackoff is the max backoff time when checking upstream status.
|
||||||
|
checkUpstreamMaxBackoff = 2 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
// upstreamMonitor performs monitoring upstreams health.
|
||||||
|
type upstreamMonitor struct {
|
||||||
|
cfg *ctrld.Config
|
||||||
|
|
||||||
|
down map[string]*atomic.Bool
|
||||||
|
failureReq map[string]*atomic.Uint64
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
checking map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUpstreamMonitor(cfg *ctrld.Config) *upstreamMonitor {
|
||||||
|
um := &upstreamMonitor{
|
||||||
|
cfg: cfg,
|
||||||
|
down: make(map[string]*atomic.Bool),
|
||||||
|
failureReq: make(map[string]*atomic.Uint64),
|
||||||
|
checking: make(map[string]bool),
|
||||||
|
}
|
||||||
|
for n := range cfg.Upstream {
|
||||||
|
upstream := upstreamPrefix + n
|
||||||
|
um.down[upstream] = new(atomic.Bool)
|
||||||
|
um.failureReq[upstream] = new(atomic.Uint64)
|
||||||
|
}
|
||||||
|
um.down[upstreamOS] = new(atomic.Bool)
|
||||||
|
um.failureReq[upstreamOS] = new(atomic.Uint64)
|
||||||
|
return um
|
||||||
|
}
|
||||||
|
|
||||||
|
// increaseFailureCount increase failed queries count for an upstream by 1.
|
||||||
|
func (um *upstreamMonitor) increaseFailureCount(upstream string) {
|
||||||
|
failedCount := um.failureReq[upstream].Add(1)
|
||||||
|
um.down[upstream].Store(failedCount >= maxFailureRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// isDown reports whether the given upstream is being marked as down.
|
||||||
|
func (um *upstreamMonitor) isDown(upstream string) bool {
|
||||||
|
return um.down[upstream].Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset marks an upstream as up and set failed queries counter to zero.
|
||||||
|
func (um *upstreamMonitor) reset(upstream string) {
|
||||||
|
um.failureReq[upstream].Store(0)
|
||||||
|
um.down[upstream].Store(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkUpstream checks the given upstream status, periodically sending query to upstream
|
||||||
|
// until successfully. An upstream status/counter will be reset once it becomes reachable.
|
||||||
|
func (um *upstreamMonitor) checkUpstream(upstream string, uc *ctrld.UpstreamConfig) {
|
||||||
|
um.mu.Lock()
|
||||||
|
isChecking := um.checking[upstream]
|
||||||
|
if isChecking {
|
||||||
|
um.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
um.checking[upstream] = true
|
||||||
|
um.mu.Unlock()
|
||||||
|
|
||||||
|
bo := backoff.NewBackoff("checkUpstream", logf, checkUpstreamMaxBackoff)
|
||||||
|
resolver, err := ctrld.NewResolver(uc)
|
||||||
|
if err != nil {
|
||||||
|
mainLog.Load().Warn().Err(err).Msg("could not check upstream")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msg := new(dns.Msg)
|
||||||
|
msg.SetQuestion(".", dns.TypeNS)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
for {
|
||||||
|
_, err := resolver.Resolve(ctx, msg)
|
||||||
|
if err == nil {
|
||||||
|
mainLog.Load().Debug().Msgf("upstream %q is online", uc.Endpoint)
|
||||||
|
um.reset(upstream)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bo.BackOff(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package ctrld_library
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/Control-D-Inc/ctrld/cmd/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Controller holds global state
|
||||||
|
type Controller struct {
|
||||||
|
stopCh chan struct{}
|
||||||
|
AppCallback AppCallback
|
||||||
|
Config cli.AppConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewController provides reference to global state to be managed by android vpn service and iOS network extension.
|
||||||
|
// reference is not safe for concurrent use.
|
||||||
|
func NewController(appCallback AppCallback) *Controller {
|
||||||
|
return &Controller{AppCallback: appCallback}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppCallback provides access to app instance.
|
||||||
|
type AppCallback interface {
|
||||||
|
Hostname() string
|
||||||
|
LanIp() string
|
||||||
|
MacAddress() string
|
||||||
|
Exit(error string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, HomeDir string, logLevel int, logPath string) {
|
||||||
|
if c.stopCh == nil {
|
||||||
|
c.stopCh = make(chan struct{})
|
||||||
|
c.Config = cli.AppConfig{
|
||||||
|
CdUID: CdUID,
|
||||||
|
HomeDir: HomeDir,
|
||||||
|
Verbose: logLevel,
|
||||||
|
LogPath: logPath,
|
||||||
|
}
|
||||||
|
appCallback := mapCallback(c.AppCallback)
|
||||||
|
cli.RunMobile(&c.Config, &appCallback, c.stopCh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// As workaround to avoid circular dependency between cli and ctrld_library module
|
||||||
|
func mapCallback(callback AppCallback) cli.AppCallback {
|
||||||
|
return cli.AppCallback{
|
||||||
|
HostName: func() string {
|
||||||
|
return callback.Hostname()
|
||||||
|
},
|
||||||
|
LanIp: func() string {
|
||||||
|
return callback.LanIp()
|
||||||
|
},
|
||||||
|
MacAddress: func() string {
|
||||||
|
return callback.MacAddress()
|
||||||
|
},
|
||||||
|
Exit: func(err string) {
|
||||||
|
callback.Exit(err)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) Stop() bool {
|
||||||
|
if c.stopCh != nil {
|
||||||
|
close(c.stopCh)
|
||||||
|
c.stopCh = nil
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) IsRunning() bool {
|
||||||
|
return c.stopCh != nil
|
||||||
|
}
|
||||||
@@ -2,8 +2,10 @@ package ctrld
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
crand "crypto/rand"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
@@ -78,8 +80,8 @@ func SetConfigNameWithPath(v *viper.Viper, name, configPath string) {
|
|||||||
func InitConfig(v *viper.Viper, name string) {
|
func InitConfig(v *viper.Viper, name string) {
|
||||||
v.SetDefault("listener", map[string]*ListenerConfig{
|
v.SetDefault("listener", map[string]*ListenerConfig{
|
||||||
"0": {
|
"0": {
|
||||||
IP: "127.0.0.1",
|
IP: "",
|
||||||
Port: 53,
|
Port: 0,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
v.SetDefault("network", map[string]*NetworkConfig{
|
v.SetDefault("network", map[string]*NetworkConfig{
|
||||||
@@ -178,6 +180,7 @@ type ServiceConfig struct {
|
|||||||
DiscoverARP *bool `mapstructure:"discover_arp" toml:"discover_dhcp,omitempty"`
|
DiscoverARP *bool `mapstructure:"discover_arp" toml:"discover_dhcp,omitempty"`
|
||||||
DiscoverDHCP *bool `mapstructure:"discover_dhcp" toml:"discover_dhcp,omitempty"`
|
DiscoverDHCP *bool `mapstructure:"discover_dhcp" toml:"discover_dhcp,omitempty"`
|
||||||
DiscoverPtr *bool `mapstructure:"discover_ptr" toml:"discover_ptr,omitempty"`
|
DiscoverPtr *bool `mapstructure:"discover_ptr" toml:"discover_ptr,omitempty"`
|
||||||
|
DiscoverHosts *bool `mapstructure:"discover_hosts" toml:"discover_hosts,omitempty"`
|
||||||
Daemon bool `mapstructure:"-" toml:"-"`
|
Daemon bool `mapstructure:"-" toml:"-"`
|
||||||
AllocateIP bool `mapstructure:"-" toml:"-"`
|
AllocateIP bool `mapstructure:"-" toml:"-"`
|
||||||
}
|
}
|
||||||
@@ -216,6 +219,7 @@ type UpstreamConfig struct {
|
|||||||
http3RoundTripper6 http.RoundTripper
|
http3RoundTripper6 http.RoundTripper
|
||||||
certPool *x509.CertPool
|
certPool *x509.CertPool
|
||||||
u *url.URL
|
u *url.URL
|
||||||
|
uid string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListenerConfig specifies the networks configuration that ctrld will run on.
|
// ListenerConfig specifies the networks configuration that ctrld will run on.
|
||||||
@@ -260,6 +264,7 @@ type Rule map[string][]string
|
|||||||
|
|
||||||
// Init initialized necessary values for an UpstreamConfig.
|
// Init initialized necessary values for an UpstreamConfig.
|
||||||
func (uc *UpstreamConfig) Init() {
|
func (uc *UpstreamConfig) Init() {
|
||||||
|
uc.uid = upstreamUID()
|
||||||
if u, err := url.Parse(uc.Endpoint); err == nil {
|
if u, err := url.Parse(uc.Endpoint); err == nil {
|
||||||
uc.Domain = u.Host
|
uc.Domain = u.Host
|
||||||
switch uc.Type {
|
switch uc.Type {
|
||||||
@@ -340,6 +345,11 @@ func (uc *UpstreamConfig) SetupBootstrapIP() {
|
|||||||
uc.setupBootstrapIP(true)
|
uc.setupBootstrapIP(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UID returns the unique identifier of the upstream.
|
||||||
|
func (uc *UpstreamConfig) UID() string {
|
||||||
|
return uc.uid
|
||||||
|
}
|
||||||
|
|
||||||
// SetupBootstrapIP manually find all available IPs of the upstream.
|
// SetupBootstrapIP manually find all available IPs of the upstream.
|
||||||
// The first usable IP will be used as bootstrap IP of the upstream.
|
// The first usable IP will be used as bootstrap IP of the upstream.
|
||||||
func (uc *UpstreamConfig) setupBootstrapIP(withBootstrapDNS bool) {
|
func (uc *UpstreamConfig) setupBootstrapIP(withBootstrapDNS bool) {
|
||||||
@@ -679,3 +689,15 @@ func ResolverTypeFromEndpoint(endpoint string) string {
|
|||||||
func pick(s []string) string {
|
func pick(s []string) string {
|
||||||
return s[rand.Intn(len(s))]
|
return s[rand.Intn(len(s))]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// upstreamUID generates an unique identifier for an upstream.
|
||||||
|
func upstreamUID() string {
|
||||||
|
b := make([]byte, 4)
|
||||||
|
for {
|
||||||
|
if _, err := crand.Read(b); err != nil {
|
||||||
|
ProxyLogger.Load().Warn().Err(err).Msg("could not generate uid for upstream, retrying...")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -185,6 +185,7 @@ func TestUpstreamConfig_Init(t *testing.T) {
|
|||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
tc.uc.Init()
|
tc.uc.Init()
|
||||||
|
tc.uc.uid = "" // we don't care about the uid.
|
||||||
assert.Equal(t, tc.expected, tc.uc)
|
assert.Equal(t, tc.expected, tc.uc)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-7
@@ -8,6 +8,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -43,7 +44,6 @@ func (uc *UpstreamConfig) newDOH3Transport(addrs []string) http.RoundTripper {
|
|||||||
rt := &http3.RoundTripper{}
|
rt := &http3.RoundTripper{}
|
||||||
rt.TLSClientConfig = &tls.Config{RootCAs: uc.certPool}
|
rt.TLSClientConfig = &tls.Config{RootCAs: uc.certPool}
|
||||||
rt.Dial = func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
rt.Dial = func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
||||||
domain := addr
|
|
||||||
_, port, _ := net.SplitHostPort(addr)
|
_, port, _ := net.SplitHostPort(addr)
|
||||||
// if we have a bootstrap ip set, use it to avoid DNS lookup
|
// if we have a bootstrap ip set, use it to avoid DNS lookup
|
||||||
if uc.BootstrapIP != "" {
|
if uc.BootstrapIP != "" {
|
||||||
@@ -57,20 +57,23 @@ func (uc *UpstreamConfig) newDOH3Transport(addrs []string) http.RoundTripper {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return quic.DialEarlyContext(ctx, udpConn, remoteAddr, domain, tlsCfg, cfg)
|
return quic.DialEarly(ctx, udpConn, remoteAddr, tlsCfg, cfg)
|
||||||
}
|
}
|
||||||
dialAddrs := make([]string, len(addrs))
|
dialAddrs := make([]string, len(addrs))
|
||||||
for i := range addrs {
|
for i := range addrs {
|
||||||
dialAddrs[i] = net.JoinHostPort(addrs[i], port)
|
dialAddrs[i] = net.JoinHostPort(addrs[i], port)
|
||||||
}
|
}
|
||||||
pd := &quicParallelDialer{}
|
pd := &quicParallelDialer{}
|
||||||
conn, err := pd.Dial(ctx, domain, dialAddrs, tlsCfg, cfg)
|
conn, err := pd.Dial(ctx, dialAddrs, tlsCfg, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
ProxyLogger.Load().Debug().Msgf("sending doh3 request to: %s", conn.RemoteAddr())
|
ProxyLogger.Load().Debug().Msgf("sending doh3 request to: %s", conn.RemoteAddr())
|
||||||
return conn, err
|
return conn, err
|
||||||
}
|
}
|
||||||
|
runtime.SetFinalizer(rt, func(rt *http3.RoundTripper) {
|
||||||
|
rt.CloseIdleConnections()
|
||||||
|
})
|
||||||
return rt
|
return rt
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,13 +110,15 @@ type parallelDialerResult struct {
|
|||||||
type quicParallelDialer struct{}
|
type quicParallelDialer struct{}
|
||||||
|
|
||||||
// Dial performs parallel dialing to the given address list.
|
// Dial performs parallel dialing to the given address list.
|
||||||
func (d *quicParallelDialer) Dial(ctx context.Context, domain string, addrs []string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
func (d *quicParallelDialer) Dial(ctx context.Context, addrs []string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
||||||
if len(addrs) == 0 {
|
if len(addrs) == 0 {
|
||||||
return nil, errors.New("empty addresses")
|
return nil, errors.New("empty addresses")
|
||||||
}
|
}
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
defer close(done)
|
||||||
ch := make(chan *parallelDialerResult, len(addrs))
|
ch := make(chan *parallelDialerResult, len(addrs))
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Add(len(addrs))
|
wg.Add(len(addrs))
|
||||||
@@ -135,9 +140,14 @@ func (d *quicParallelDialer) Dial(ctx context.Context, domain string, addrs []st
|
|||||||
ch <- ¶llelDialerResult{conn: nil, err: err}
|
ch <- ¶llelDialerResult{conn: nil, err: err}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
conn, err := quic.DialEarly(ctx, udpConn, remoteAddr, tlsCfg, cfg)
|
||||||
conn, err := quic.DialEarlyContext(ctx, udpConn, remoteAddr, domain, tlsCfg, cfg)
|
select {
|
||||||
ch <- ¶llelDialerResult{conn: conn, err: err}
|
case ch <- ¶llelDialerResult{conn: conn, err: err}:
|
||||||
|
case <-done:
|
||||||
|
if conn != nil {
|
||||||
|
conn.CloseWithError(quic.ApplicationErrorCode(http3.ErrCodeNoError), "")
|
||||||
|
}
|
||||||
|
}
|
||||||
}(addr)
|
}(addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Using Debian bullseye for building regular image.
|
||||||
|
# Using scratch image for minimal image size.
|
||||||
|
# The final image has:
|
||||||
|
#
|
||||||
|
# - Timezone info file.
|
||||||
|
# - CA certs file.
|
||||||
|
# - /etc/{passwd,group} file.
|
||||||
|
# - Non-cgo ctrld binary.
|
||||||
|
#
|
||||||
|
# CI_COMMIT_TAG is used to set the version of ctrld binary.
|
||||||
|
FROM golang:1.20-bullseye as base
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y upx-ucl
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ARG tag=master
|
||||||
|
ENV CI_COMMIT_TAG=$tag
|
||||||
|
RUN CTRLD_NO_QF=yes CGO_ENABLED=0 ./scripts/build.sh
|
||||||
|
|
||||||
|
FROM alpine
|
||||||
|
|
||||||
|
COPY --from=base /usr/share/zoneinfo /usr/share/zoneinfo
|
||||||
|
COPY --from=base /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||||
|
COPY --from=base /etc/passwd /etc/passwd
|
||||||
|
COPY --from=base /etc/group /etc/group
|
||||||
|
|
||||||
|
COPY --from=base /app/ctrld-linux-*-nocgo ctrld
|
||||||
|
|
||||||
|
ENTRYPOINT ["./ctrld", "run"]
|
||||||
@@ -193,6 +193,13 @@ Perform LAN client discovery using PTR queries.
|
|||||||
- Required: no
|
- Required: no
|
||||||
- Default: true
|
- Default: true
|
||||||
|
|
||||||
|
### discover_hosts
|
||||||
|
Perform LAN client discovery using hosts file.
|
||||||
|
|
||||||
|
- Type: boolean
|
||||||
|
- Required: no
|
||||||
|
- Default: true
|
||||||
|
|
||||||
### dhcp_lease_file_path
|
### dhcp_lease_file_path
|
||||||
Relative or absolute path to a custom DHCP leases file location.
|
Relative or absolute path to a custom DHCP leases file location.
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/cuonglm/osinfo"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
@@ -16,9 +21,56 @@ const (
|
|||||||
dohMacHeader = "x-cd-mac"
|
dohMacHeader = "x-cd-mac"
|
||||||
dohIPHeader = "x-cd-ip"
|
dohIPHeader = "x-cd-ip"
|
||||||
dohHostHeader = "x-cd-host"
|
dohHostHeader = "x-cd-host"
|
||||||
|
dohOsHeader = "x-cd-os"
|
||||||
headerApplicationDNS = "application/dns-message"
|
headerApplicationDNS = "application/dns-message"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// EncodeOsNameMap provides mapping from OS name to a shorter string, used for encoding x-cd-os value.
|
||||||
|
var EncodeOsNameMap = map[string]string{
|
||||||
|
"windows": "1",
|
||||||
|
"darwin": "2",
|
||||||
|
"linux": "3",
|
||||||
|
"freebsd": "4",
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeOsNameMap provides mapping from encoded OS name to real value, used for decoding x-cd-os value.
|
||||||
|
var DecodeOsNameMap = map[string]string{}
|
||||||
|
|
||||||
|
// EncodeArchNameMap provides mapping from OS arch to a shorter string, used for encoding x-cd-os value.
|
||||||
|
var EncodeArchNameMap = map[string]string{
|
||||||
|
"amd64": "1",
|
||||||
|
"arm64": "2",
|
||||||
|
"arm": "3",
|
||||||
|
"386": "4",
|
||||||
|
"mips": "5",
|
||||||
|
"mipsle": "6",
|
||||||
|
"mips64": "7",
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeArchNameMap provides mapping from encoded OS arch to real value, used for decoding x-cd-os value.
|
||||||
|
var DecodeArchNameMap = map[string]string{}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
for k, v := range EncodeOsNameMap {
|
||||||
|
DecodeOsNameMap[v] = k
|
||||||
|
}
|
||||||
|
for k, v := range EncodeArchNameMap {
|
||||||
|
DecodeArchNameMap[v] = k
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: use sync.OnceValue when upgrading to go1.21
|
||||||
|
var xCdOsValueOnce sync.Once
|
||||||
|
var xCdOsValue string
|
||||||
|
|
||||||
|
func dohOsHeaderValue() string {
|
||||||
|
xCdOsValueOnce.Do(func() {
|
||||||
|
oi := osinfo.New()
|
||||||
|
xCdOsValue = strings.Join([]string{EncodeOsNameMap[runtime.GOOS], EncodeArchNameMap[runtime.GOARCH], oi.Dist}, "-")
|
||||||
|
})
|
||||||
|
return xCdOsValue
|
||||||
|
}
|
||||||
|
|
||||||
func newDohResolver(uc *UpstreamConfig) *dohResolver {
|
func newDohResolver(uc *UpstreamConfig) *dohResolver {
|
||||||
r := &dohResolver{
|
r := &dohResolver{
|
||||||
endpoint: uc.u,
|
endpoint: uc.u,
|
||||||
@@ -97,8 +149,12 @@ func (r *dohResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, erro
|
|||||||
func addHeader(ctx context.Context, req *http.Request, sendClientInfo bool) {
|
func addHeader(ctx context.Context, req *http.Request, sendClientInfo bool) {
|
||||||
req.Header.Set("Content-Type", headerApplicationDNS)
|
req.Header.Set("Content-Type", headerApplicationDNS)
|
||||||
req.Header.Set("Accept", headerApplicationDNS)
|
req.Header.Set("Accept", headerApplicationDNS)
|
||||||
|
req.Header.Set(dohOsHeader, dohOsHeaderValue())
|
||||||
|
|
||||||
|
printed := false
|
||||||
if sendClientInfo {
|
if sendClientInfo {
|
||||||
if ci, ok := ctx.Value(ClientInfoCtxKey{}).(*ClientInfo); ok && ci != nil {
|
if ci, ok := ctx.Value(ClientInfoCtxKey{}).(*ClientInfo); ok && ci != nil {
|
||||||
|
printed = ci.Mac != "" || ci.IP != "" || ci.Hostname != ""
|
||||||
if ci.Mac != "" {
|
if ci.Mac != "" {
|
||||||
req.Header.Set(dohMacHeader, ci.Mac)
|
req.Header.Set(dohMacHeader, ci.Mac)
|
||||||
}
|
}
|
||||||
@@ -108,7 +164,12 @@ func addHeader(ctx context.Context, req *http.Request, sendClientInfo bool) {
|
|||||||
if ci.Hostname != "" {
|
if ci.Hostname != "" {
|
||||||
req.Header.Set(dohHostHeader, ci.Hostname)
|
req.Header.Set(dohHostHeader, ci.Hostname)
|
||||||
}
|
}
|
||||||
|
if ci.Self {
|
||||||
|
req.Header.Set(dohOsHeader, dohOsHeaderValue())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log(ctx, ProxyLogger.Load().Debug().Interface("header", req.Header), "sending request header")
|
if printed {
|
||||||
|
Log(ctx, ProxyLogger.Load().Debug().Interface("header", req.Header), "sending request header")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
package ctrld
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_dohOsHeaderValue(t *testing.T) {
|
||||||
|
val := dohOsHeaderValue()
|
||||||
|
if val == "" {
|
||||||
|
t.Fatalf("empty %s", dohOsHeader)
|
||||||
|
}
|
||||||
|
t.Log(val)
|
||||||
|
|
||||||
|
encodedOs := EncodeOsNameMap[runtime.GOOS]
|
||||||
|
if encodedOs == "" {
|
||||||
|
t.Fatalf("missing encoding value for: %q", runtime.GOOS)
|
||||||
|
}
|
||||||
|
decodedOs := DecodeOsNameMap[encodedOs]
|
||||||
|
if decodedOs == "" {
|
||||||
|
t.Fatalf("missing decoding value for: %q", runtime.GOOS)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,7 +51,7 @@ func resolve(ctx context.Context, msg *dns.Msg, endpoint string, tlsConfig *tls.
|
|||||||
}
|
}
|
||||||
|
|
||||||
func doResolve(ctx context.Context, msg *dns.Msg, endpoint string, tlsConfig *tls.Config) (*dns.Msg, error) {
|
func doResolve(ctx context.Context, msg *dns.Msg, endpoint string, tlsConfig *tls.Config) (*dns.Msg, error) {
|
||||||
session, err := quic.DialAddr(endpoint, tlsConfig, nil)
|
session, err := quic.DialAddr(ctx, endpoint, tlsConfig, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ go 1.20
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0
|
github.com/coreos/go-systemd/v22 v22.5.0
|
||||||
github.com/cuonglm/osinfo v0.0.0-20230329055532-c513f836da19
|
github.com/cuonglm/osinfo v0.0.0-20230921071424-e0e1b1e0bbbf
|
||||||
github.com/frankban/quicktest v1.14.5
|
github.com/frankban/quicktest v1.14.5
|
||||||
github.com/fsnotify/fsnotify v1.6.0
|
github.com/fsnotify/fsnotify v1.6.0
|
||||||
github.com/go-playground/validator/v10 v10.11.1
|
github.com/go-playground/validator/v10 v10.11.1
|
||||||
@@ -12,19 +12,19 @@ require (
|
|||||||
github.com/hashicorp/golang-lru/v2 v2.0.1
|
github.com/hashicorp/golang-lru/v2 v2.0.1
|
||||||
github.com/illarion/gonotify v1.0.1
|
github.com/illarion/gonotify v1.0.1
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16
|
github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16
|
||||||
|
github.com/jaytaylor/go-hostsfile v0.0.0-20220426042432-61485ac1fa6c
|
||||||
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86
|
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86
|
||||||
github.com/kardianos/service v1.2.1
|
github.com/kardianos/service v1.2.1
|
||||||
github.com/miekg/dns v1.1.55
|
github.com/miekg/dns v1.1.55
|
||||||
github.com/olekukonko/tablewriter v0.0.5
|
github.com/olekukonko/tablewriter v0.0.5
|
||||||
github.com/pelletier/go-toml/v2 v2.0.8
|
github.com/pelletier/go-toml/v2 v2.0.8
|
||||||
github.com/quic-go/quic-go v0.32.0
|
github.com/quic-go/quic-go v0.38.0
|
||||||
github.com/rs/zerolog v1.28.0
|
github.com/rs/zerolog v1.28.0
|
||||||
github.com/spf13/cobra v1.7.0
|
github.com/spf13/cobra v1.7.0
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/spf13/viper v1.16.0
|
github.com/spf13/viper v1.16.0
|
||||||
github.com/stretchr/testify v1.8.3
|
github.com/stretchr/testify v1.8.3
|
||||||
github.com/vishvananda/netlink v1.2.1-beta.2
|
github.com/vishvananda/netlink v1.2.1-beta.2
|
||||||
go4.org/mem v0.0.0-20220726221520-4f986261bf13
|
|
||||||
golang.org/x/net v0.10.0
|
golang.org/x/net v0.10.0
|
||||||
golang.org/x/sync v0.2.0
|
golang.org/x/sync v0.2.0
|
||||||
golang.org/x/sys v0.8.1-0.20230609144347-5059a07aa46a
|
golang.org/x/sys v0.8.1-0.20230609144347-5059a07aa46a
|
||||||
@@ -37,7 +37,7 @@ require (
|
|||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/go-playground/locales v0.14.0 // indirect
|
github.com/go-playground/locales v0.14.0 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
|
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||||
github.com/golang/mock v1.6.0 // indirect
|
github.com/golang/mock v1.6.0 // indirect
|
||||||
github.com/google/go-cmp v0.5.9 // indirect
|
github.com/google/go-cmp v0.5.9 // indirect
|
||||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
|
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
|
||||||
@@ -56,13 +56,11 @@ require (
|
|||||||
github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 // indirect
|
github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 // indirect
|
||||||
github.com/mdlayher/socket v0.4.1 // indirect
|
github.com/mdlayher/socket v0.4.1 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/onsi/ginkgo/v2 v2.2.0 // indirect
|
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
|
||||||
github.com/pierrec/lz4/v4 v4.1.17 // indirect
|
github.com/pierrec/lz4/v4 v4.1.17 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/quic-go/qpack v0.4.0 // indirect
|
github.com/quic-go/qpack v0.4.0 // indirect
|
||||||
github.com/quic-go/qtls-go1-18 v0.2.0 // indirect
|
github.com/quic-go/qtls-go1-20 v0.3.2 // indirect
|
||||||
github.com/quic-go/qtls-go1-19 v0.2.0 // indirect
|
|
||||||
github.com/quic-go/qtls-go1-20 v0.1.0 // indirect
|
|
||||||
github.com/rivo/uniseg v0.4.4 // indirect
|
github.com/rivo/uniseg v0.4.4 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
||||||
github.com/spf13/afero v1.9.5 // indirect
|
github.com/spf13/afero v1.9.5 // indirect
|
||||||
@@ -71,8 +69,10 @@ require (
|
|||||||
github.com/subosito/gotenv v1.4.2 // indirect
|
github.com/subosito/gotenv v1.4.2 // indirect
|
||||||
github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 // indirect
|
github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 // indirect
|
||||||
github.com/vishvananda/netns v0.0.4 // indirect
|
github.com/vishvananda/netns v0.0.4 // indirect
|
||||||
|
go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect
|
||||||
golang.org/x/crypto v0.9.0 // indirect
|
golang.org/x/crypto v0.9.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 // indirect
|
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 // indirect
|
||||||
|
golang.org/x/mobile v0.0.0-20230531173138-3c911d8e3eda // indirect
|
||||||
golang.org/x/mod v0.10.0 // indirect
|
golang.org/x/mod v0.10.0 // indirect
|
||||||
golang.org/x/text v0.9.0 // indirect
|
golang.org/x/text v0.9.0 // indirect
|
||||||
golang.org/x/tools v0.9.1 // indirect
|
golang.org/x/tools v0.9.1 // indirect
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
|
|||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/cuonglm/osinfo v0.0.0-20230329055532-c513f836da19 h1:7P/f19Mr0oa3ug8BYt4JuRe/Zq3dF4Mrr4m8+Kw+Hcs=
|
github.com/cuonglm/osinfo v0.0.0-20230329055532-c513f836da19 h1:7P/f19Mr0oa3ug8BYt4JuRe/Zq3dF4Mrr4m8+Kw+Hcs=
|
||||||
github.com/cuonglm/osinfo v0.0.0-20230329055532-c513f836da19/go.mod h1:G45410zMgmnSjLVKCq4f6GpbYAzoP2plX9rPwgx6C24=
|
github.com/cuonglm/osinfo v0.0.0-20230329055532-c513f836da19/go.mod h1:G45410zMgmnSjLVKCq4f6GpbYAzoP2plX9rPwgx6C24=
|
||||||
|
github.com/cuonglm/osinfo v0.0.0-20230921071424-e0e1b1e0bbbf h1:40DHYsri+d1bnroFDU2FQAeq68f3kAlOzlQ93kCf26Q=
|
||||||
|
github.com/cuonglm/osinfo v0.0.0-20230921071424-e0e1b1e0bbbf/go.mod h1:G45410zMgmnSjLVKCq4f6GpbYAzoP2plX9rPwgx6C24=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -73,6 +75,7 @@ github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbS
|
|||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
|
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
||||||
@@ -81,8 +84,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/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 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ=
|
||||||
github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
||||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
|
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
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.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
@@ -162,6 +165,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
|
|||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16 h1:+aAGyK41KRn8jbF2Q7PLL0Sxwg6dShGcQSeCC7nZQ8E=
|
github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16 h1:+aAGyK41KRn8jbF2Q7PLL0Sxwg6dShGcQSeCC7nZQ8E=
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16/go.mod h1:IKrnDWs3/Mqq5n0lI+RxA2sB7MvN/vbMBP3ehXg65UI=
|
github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16/go.mod h1:IKrnDWs3/Mqq5n0lI+RxA2sB7MvN/vbMBP3ehXg65UI=
|
||||||
|
github.com/jaytaylor/go-hostsfile v0.0.0-20220426042432-61485ac1fa6c h1:kbTQ8oGf+BVFvt/fM+ECI+NbZDCqoi0vtZTfB2p2hrI=
|
||||||
|
github.com/jaytaylor/go-hostsfile v0.0.0-20220426042432-61485ac1fa6c/go.mod h1:k6+89xKz7BSMJ+DzIerBdtpEUeTlBMugO/hcVSzahog=
|
||||||
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||||
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0WwEDBeqxKwb7WB62QX8bvZ/FJnVXIfk=
|
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0WwEDBeqxKwb7WB62QX8bvZ/FJnVXIfk=
|
||||||
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMtuldEgx/4q7iSGazk22+IcgvtiC+HIimFO9XlS8=
|
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMtuldEgx/4q7iSGazk22+IcgvtiC+HIimFO9XlS8=
|
||||||
@@ -211,9 +216,9 @@ 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/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 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||||
github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI=
|
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
|
||||||
github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk=
|
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
|
||||||
github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q=
|
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
|
||||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
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/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=
|
github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||||
@@ -227,14 +232,10 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
|||||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||||
github.com/quic-go/qtls-go1-18 v0.2.0 h1:5ViXqBZ90wpUcZS0ge79rf029yx0dYB0McyPJwqqj7U=
|
github.com/quic-go/qtls-go1-20 v0.3.2 h1:rRgN3WfnKbyik4dBV8A6girlJVxGand/d+jVKbQq5GI=
|
||||||
github.com/quic-go/qtls-go1-18 v0.2.0/go.mod h1:moGulGHK7o6O8lSPSZNoOwcLvJKJ85vVNc7oJFD65bc=
|
github.com/quic-go/qtls-go1-20 v0.3.2/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
|
||||||
github.com/quic-go/qtls-go1-19 v0.2.0 h1:Cvn2WdhyViFUHoOqK52i51k4nDX8EwIh5VJiVM4nttk=
|
github.com/quic-go/quic-go v0.38.0 h1:T45lASr5q/TrVwt+jrVccmqHhPL2XuSyoCLVCpfOSLc=
|
||||||
github.com/quic-go/qtls-go1-19 v0.2.0/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
|
github.com/quic-go/quic-go v0.38.0/go.mod h1:MPCuRq7KBK2hNcfKj/1iD1BGuN3eAYMeNxp3T42LRUg=
|
||||||
github.com/quic-go/qtls-go1-20 v0.1.0 h1:d1PK3ErFy9t7zxKsG3NXBJXZjp/kMLoIb3y/kV54oAI=
|
|
||||||
github.com/quic-go/qtls-go1-20 v0.1.0/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
|
|
||||||
github.com/quic-go/quic-go v0.32.0 h1:lY02md31s1JgPiiyfqJijpu/UX/Iun304FI3yUqX7tA=
|
|
||||||
github.com/quic-go/quic-go v0.32.0/go.mod h1:/fCsKANhQIeD5l76c2JFU+07gVE3KaA0FP+0zMWwfwo=
|
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
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 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
@@ -330,6 +331,8 @@ golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPI
|
|||||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||||
|
golang.org/x/mobile v0.0.0-20230531173138-3c911d8e3eda h1:O+EUvnBNPwI4eLthn8W5K+cS8zQZfgTABPLNm6Bna34=
|
||||||
|
golang.org/x/mobile v0.0.0-20230531173138-3c911d8e3eda/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY=
|
||||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ func (a *arpDiscover) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *arpDiscover) List() []string {
|
func (a *arpDiscover) List() []string {
|
||||||
|
if a == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
var ips []string
|
var ips []string
|
||||||
a.ip.Range(func(key, value any) bool {
|
a.ip.Range(func(key, value any) bool {
|
||||||
ips = append(ips, value.(string))
|
ips = append(ips, value.(string))
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ type Table struct {
|
|||||||
arp *arpDiscover
|
arp *arpDiscover
|
||||||
ptr *ptrDiscover
|
ptr *ptrDiscover
|
||||||
mdns *mdns
|
mdns *mdns
|
||||||
|
hf *hostsFile
|
||||||
|
vni *virtualNetworkIface
|
||||||
cfg *ctrld.Config
|
cfg *ctrld.Config
|
||||||
quitCh chan struct{}
|
quitCh chan struct{}
|
||||||
selfIP string
|
selfIP string
|
||||||
@@ -116,6 +118,7 @@ func (t *Table) Init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Table) init() {
|
func (t *Table) init() {
|
||||||
|
// Custom client ID presents, use it as the only source.
|
||||||
if _, clientID := controld.ParseRawUID(t.cdUID); clientID != "" {
|
if _, clientID := controld.ParseRawUID(t.cdUID); clientID != "" {
|
||||||
ctrld.ProxyLogger.Load().Debug().Msg("start self discovery")
|
ctrld.ProxyLogger.Load().Debug().Msg("start self discovery")
|
||||||
t.dhcp = &dhcp{selfIP: t.selfIP}
|
t.dhcp = &dhcp{selfIP: t.selfIP}
|
||||||
@@ -125,6 +128,11 @@ func (t *Table) init() {
|
|||||||
t.hostnameResolvers = append(t.hostnameResolvers, t.dhcp)
|
t.hostnameResolvers = append(t.hostnameResolvers, t.dhcp)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Otherwise, process all possible sources in order, that means
|
||||||
|
// the first result of IP/MAC/Hostname lookup will be used.
|
||||||
|
//
|
||||||
|
// Merlin custom clients.
|
||||||
if t.discoverDHCP() || t.discoverARP() {
|
if t.discoverDHCP() || t.discoverARP() {
|
||||||
t.merlin = &merlinDiscover{}
|
t.merlin = &merlinDiscover{}
|
||||||
if err := t.merlin.refresh(); err != nil {
|
if err := t.merlin.refresh(); err != nil {
|
||||||
@@ -134,6 +142,19 @@ func (t *Table) init() {
|
|||||||
t.refreshers = append(t.refreshers, t.merlin)
|
t.refreshers = append(t.refreshers, t.merlin)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Hosts file mapping.
|
||||||
|
if t.discoverHosts() {
|
||||||
|
t.hf = &hostsFile{}
|
||||||
|
ctrld.ProxyLogger.Load().Debug().Msg("start hosts file discovery")
|
||||||
|
if err := t.hf.init(); err != nil {
|
||||||
|
ctrld.ProxyLogger.Load().Error().Err(err).Msg("could not init hosts file discover")
|
||||||
|
} else {
|
||||||
|
t.hostnameResolvers = append(t.hostnameResolvers, t.hf)
|
||||||
|
t.refreshers = append(t.refreshers, t.hf)
|
||||||
|
}
|
||||||
|
go t.hf.watchChanges()
|
||||||
|
}
|
||||||
|
// DHCP lease files.
|
||||||
if t.discoverDHCP() {
|
if t.discoverDHCP() {
|
||||||
t.dhcp = &dhcp{selfIP: t.selfIP}
|
t.dhcp = &dhcp{selfIP: t.selfIP}
|
||||||
ctrld.ProxyLogger.Load().Debug().Msg("start dhcp discovery")
|
ctrld.ProxyLogger.Load().Debug().Msg("start dhcp discovery")
|
||||||
@@ -146,6 +167,7 @@ func (t *Table) init() {
|
|||||||
}
|
}
|
||||||
go t.dhcp.watchChanges()
|
go t.dhcp.watchChanges()
|
||||||
}
|
}
|
||||||
|
// ARP table.
|
||||||
if t.discoverARP() {
|
if t.discoverARP() {
|
||||||
t.arp = &arpDiscover{}
|
t.arp = &arpDiscover{}
|
||||||
ctrld.ProxyLogger.Load().Debug().Msg("start arp discovery")
|
ctrld.ProxyLogger.Load().Debug().Msg("start arp discovery")
|
||||||
@@ -157,6 +179,7 @@ func (t *Table) init() {
|
|||||||
t.refreshers = append(t.refreshers, t.arp)
|
t.refreshers = append(t.refreshers, t.arp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// PTR lookup.
|
||||||
if t.discoverPTR() {
|
if t.discoverPTR() {
|
||||||
t.ptr = &ptrDiscover{resolver: ctrld.NewPrivateResolver()}
|
t.ptr = &ptrDiscover{resolver: ctrld.NewPrivateResolver()}
|
||||||
ctrld.ProxyLogger.Load().Debug().Msg("start ptr discovery")
|
ctrld.ProxyLogger.Load().Debug().Msg("start ptr discovery")
|
||||||
@@ -167,6 +190,7 @@ func (t *Table) init() {
|
|||||||
t.refreshers = append(t.refreshers, t.ptr)
|
t.refreshers = append(t.refreshers, t.ptr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// mdns.
|
||||||
if t.discoverMDNS() {
|
if t.discoverMDNS() {
|
||||||
t.mdns = &mdns{}
|
t.mdns = &mdns{}
|
||||||
ctrld.ProxyLogger.Load().Debug().Msg("start mdns discovery")
|
ctrld.ProxyLogger.Load().Debug().Msg("start mdns discovery")
|
||||||
@@ -176,6 +200,11 @@ func (t *Table) init() {
|
|||||||
t.hostnameResolvers = append(t.hostnameResolvers, t.mdns)
|
t.hostnameResolvers = append(t.hostnameResolvers, t.mdns)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// VPN clients.
|
||||||
|
if t.discoverDHCP() || t.discoverARP() {
|
||||||
|
t.vni = &virtualNetworkIface{}
|
||||||
|
t.hostnameResolvers = append(t.hostnameResolvers, t.vni)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Table) LookupIP(mac string) string {
|
func (t *Table) LookupIP(mac string) string {
|
||||||
@@ -259,7 +288,7 @@ func (t *Table) ListClients() []*Client {
|
|||||||
_ = r.refresh()
|
_ = r.refresh()
|
||||||
}
|
}
|
||||||
ipMap := make(map[string]*Client)
|
ipMap := make(map[string]*Client)
|
||||||
il := []ipLister{t.dhcp, t.arp, t.ptr, t.mdns}
|
il := []ipLister{t.dhcp, t.arp, t.ptr, t.mdns, t.vni}
|
||||||
for _, ir := range il {
|
for _, ir := range il {
|
||||||
for _, ip := range ir.List() {
|
for _, ip := range ir.List() {
|
||||||
c, ok := ipMap[ip]
|
c, ok := ipMap[ip]
|
||||||
@@ -300,6 +329,15 @@ func (t *Table) ListClients() []*Client {
|
|||||||
return clients
|
return clients
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StoreVPNClient stores client info for VPN clients.
|
||||||
|
func (t *Table) StoreVPNClient(ci *ctrld.ClientInfo) {
|
||||||
|
if ci == nil || t.vni == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.vni.mac.Store(ci.IP, ci.Mac)
|
||||||
|
t.vni.ip2name.Store(ci.IP, ci.Hostname)
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Table) discoverDHCP() bool {
|
func (t *Table) discoverDHCP() bool {
|
||||||
if t.cfg.Service.DiscoverDHCP == nil {
|
if t.cfg.Service.DiscoverDHCP == nil {
|
||||||
return true
|
return true
|
||||||
@@ -328,6 +366,13 @@ func (t *Table) discoverPTR() bool {
|
|||||||
return *t.cfg.Service.DiscoverPtr
|
return *t.cfg.Service.DiscoverPtr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Table) discoverHosts() bool {
|
||||||
|
if t.cfg.Service.DiscoverHosts == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return *t.cfg.Service.DiscoverHosts
|
||||||
|
}
|
||||||
|
|
||||||
// normalizeIP normalizes the ip parsed from dnsmasq/dhcpd lease file.
|
// normalizeIP normalizes the ip parsed from dnsmasq/dhcpd lease file.
|
||||||
func normalizeIP(in string) string {
|
func normalizeIP(in string) string {
|
||||||
// dnsmasq may put ip with interface index in lease file, strip it here.
|
// dnsmasq may put ip with interface index in lease file, strip it here.
|
||||||
|
|||||||
@@ -47,12 +47,25 @@ func (d *dhcp) watchChanges() {
|
|||||||
if d.watcher == nil {
|
if d.watcher == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if dir := router.LeaseFilesDir(); dir != "" {
|
||||||
|
if err := d.watcher.Add(dir); err != nil {
|
||||||
|
ctrld.ProxyLogger.Load().Err(err).Str("dir", dir).Msg("could not watch lease dir")
|
||||||
|
}
|
||||||
|
}
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case event, ok := <-d.watcher.Events:
|
case event, ok := <-d.watcher.Events:
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if event.Has(fsnotify.Create) {
|
||||||
|
if format, ok := clientInfoFiles[event.Name]; ok {
|
||||||
|
if err := d.addLeaseFile(event.Name, format); err != nil {
|
||||||
|
ctrld.ProxyLogger.Load().Err(err).Str("file", event.Name).Msg("could not add lease file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
if event.Has(fsnotify.Write) || event.Has(fsnotify.Rename) || event.Has(fsnotify.Chmod) || event.Has(fsnotify.Remove) {
|
if event.Has(fsnotify.Write) || event.Has(fsnotify.Rename) || event.Has(fsnotify.Chmod) || event.Has(fsnotify.Remove) {
|
||||||
format := clientInfoFiles[event.Name]
|
format := clientInfoFiles[event.Name]
|
||||||
if err := d.readLeaseFile(event.Name, format); err != nil && !os.IsNotExist(err) {
|
if err := d.readLeaseFile(event.Name, format); err != nil && !os.IsNotExist(err) {
|
||||||
@@ -106,6 +119,9 @@ func (d *dhcp) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *dhcp) List() []string {
|
func (d *dhcp) List() []string {
|
||||||
|
if d == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
var ips []string
|
var ips []string
|
||||||
d.ip.Range(func(key, value any) bool {
|
d.ip.Range(func(key, value any) bool {
|
||||||
ips = append(ips, value.(string))
|
ips = append(ips, value.(string))
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package clientinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
"github.com/jaytaylor/go-hostsfile"
|
||||||
|
|
||||||
|
"github.com/Control-D-Inc/ctrld"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ipv4LocalhostName = "localhost"
|
||||||
|
ipv6LocalhostName = "ip6-localhost"
|
||||||
|
ipv6LoopbackName = "ip6-loopback"
|
||||||
|
)
|
||||||
|
|
||||||
|
// hostsFile provides client discovery functionality using system hosts file.
|
||||||
|
type hostsFile struct {
|
||||||
|
watcher *fsnotify.Watcher
|
||||||
|
mu sync.Mutex
|
||||||
|
m map[string][]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// init performs initialization works, which is necessary before hostsFile can be fully operated.
|
||||||
|
func (hf *hostsFile) init() error {
|
||||||
|
watcher, err := fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
hf.watcher = watcher
|
||||||
|
if err := hf.watcher.Add(hostsfile.HostsPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m, err := hostsfile.ParseHosts(hostsfile.ReadHostsFile())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
hf.mu.Lock()
|
||||||
|
hf.m = m
|
||||||
|
hf.mu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// refresh reloads hosts file entries.
|
||||||
|
func (hf *hostsFile) refresh() error {
|
||||||
|
m, err := hostsfile.ParseHosts(hostsfile.ReadHostsFile())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
hf.mu.Lock()
|
||||||
|
hf.m = m
|
||||||
|
hf.mu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// watchChanges watches and updates hosts file data if any changes happens.
|
||||||
|
func (hf *hostsFile) watchChanges() {
|
||||||
|
if hf.watcher == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event, ok := <-hf.watcher.Events:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if event.Has(fsnotify.Write) || event.Has(fsnotify.Rename) || event.Has(fsnotify.Chmod) || event.Has(fsnotify.Remove) {
|
||||||
|
if err := hf.refresh(); err != nil && !os.IsNotExist(err) {
|
||||||
|
ctrld.ProxyLogger.Load().Err(err).Msg("hosts file changed but failed to update client info")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case err, ok := <-hf.watcher.Errors:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctrld.ProxyLogger.Load().Err(err).Msg("could not watch client info file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupHostnameByIP returns hostname for given IP from current hosts file entries.
|
||||||
|
func (hf *hostsFile) LookupHostnameByIP(ip string) string {
|
||||||
|
hf.mu.Lock()
|
||||||
|
defer hf.mu.Unlock()
|
||||||
|
if names := hf.m[ip]; len(names) > 0 {
|
||||||
|
isLoopback := ip == "127.0.0.1" || ip == "::1"
|
||||||
|
for _, hostname := range names {
|
||||||
|
name := normalizeHostname(hostname)
|
||||||
|
// Ignoring ipv4/ipv6 loopback entry.
|
||||||
|
if isLoopback && isLocalhostName(name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupHostnameByMac returns hostname for given Mac from current hosts file entries.
|
||||||
|
func (hf *hostsFile) LookupHostnameByMac(mac string) string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns human-readable format of hostsFile.
|
||||||
|
func (hf *hostsFile) String() string {
|
||||||
|
return "hosts"
|
||||||
|
}
|
||||||
|
|
||||||
|
// isLocalhostName reports whether the given hostname represents localhost.
|
||||||
|
func isLocalhostName(hostname string) bool {
|
||||||
|
switch hostname {
|
||||||
|
case ipv4LocalhostName, ipv6LocalhostName, ipv6LoopbackName:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package clientinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_hostsFile_LookupHostnameByIP(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ip string
|
||||||
|
hostnames []string
|
||||||
|
expectedHostname string
|
||||||
|
}{
|
||||||
|
{"ipv4 loopback", "127.0.0.1", []string{ipv4LocalhostName}, ""},
|
||||||
|
{"ipv6 loopback", "::1", []string{ipv6LocalhostName, ipv6LoopbackName}, ""},
|
||||||
|
{"non-localhost", "::1", []string{"foo"}, "foo"},
|
||||||
|
{"multiple hostnames", "::1", []string{ipv4LocalhostName, "foo"}, "foo"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
hf := &hostsFile{m: make(map[string][]string)}
|
||||||
|
hf.mu.Lock()
|
||||||
|
hf.m[tc.ip] = tc.hostnames
|
||||||
|
hf.mu.Unlock()
|
||||||
|
if got := hf.LookupHostnameByIP(tc.ip); got != tc.expectedHostname {
|
||||||
|
t.Errorf("unpexpected result, want: %q, got: %q", tc.expectedHostname, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,6 +48,9 @@ func (m *mdns) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *mdns) List() []string {
|
func (m *mdns) List() []string {
|
||||||
|
if m == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
var ips []string
|
var ips []string
|
||||||
m.name.Range(func(key, value any) bool {
|
m.name.Range(func(key, value any) bool {
|
||||||
ips = append(ips, key.(string))
|
ips = append(ips, key.(string))
|
||||||
|
|||||||
@@ -3,16 +3,19 @@ package clientinfo
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
|
"tailscale.com/logtail/backoff"
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld"
|
"github.com/Control-D-Inc/ctrld"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ptrDiscover struct {
|
type ptrDiscover struct {
|
||||||
hostname sync.Map // ip => hostname
|
hostname sync.Map // ip => hostname
|
||||||
resolver ctrld.Resolver
|
resolver ctrld.Resolver
|
||||||
|
serverDown atomic.Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ptrDiscover) refresh() error {
|
func (p *ptrDiscover) refresh() error {
|
||||||
@@ -41,6 +44,9 @@ func (p *ptrDiscover) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *ptrDiscover) List() []string {
|
func (p *ptrDiscover) List() []string {
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
var ips []string
|
var ips []string
|
||||||
p.hostname.Range(func(key, value any) bool {
|
p.hostname.Range(func(key, value any) bool {
|
||||||
ips = append(ips, key.(string))
|
ips = append(ips, key.(string))
|
||||||
@@ -57,18 +63,24 @@ func (p *ptrDiscover) lookupHostnameFromCache(ip string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *ptrDiscover) lookupHostname(ip string) string {
|
func (p *ptrDiscover) lookupHostname(ip string) string {
|
||||||
|
// If nameserver is down, do nothing.
|
||||||
|
if p.serverDown.Load() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
msg := new(dns.Msg)
|
msg := new(dns.Msg)
|
||||||
addr, err := dns.ReverseAddr(ip)
|
addr, err := dns.ReverseAddr(ip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctrld.ProxyLogger.Load().Error().Err(err).Msg("invalid ip address")
|
ctrld.ProxyLogger.Load().Warn().Str("discovery", "ptr").Err(err).Msg("invalid ip address")
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
msg.SetQuestion(addr, dns.TypePTR)
|
msg.SetQuestion(addr, dns.TypePTR)
|
||||||
ans, err := p.resolver.Resolve(ctx, msg)
|
ans, err := p.resolver.Resolve(ctx, msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctrld.ProxyLogger.Load().Error().Err(err).Msg("could not lookup IP")
|
ctrld.ProxyLogger.Load().Warn().Str("discovery", "ptr").Err(err).Msg("could not perform PTR lookup")
|
||||||
|
p.serverDown.Store(true)
|
||||||
|
go p.checkServer()
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
for _, rr := range ans.Answer {
|
for _, rr := range ans.Answer {
|
||||||
@@ -80,3 +92,25 @@ func (p *ptrDiscover) lookupHostname(ip string) string {
|
|||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkServer monitors if the resolver can reach its nameserver. When the nameserver
|
||||||
|
// 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 := new(dns.Msg)
|
||||||
|
m.SetQuestion(".", dns.TypeNS)
|
||||||
|
ping := func() error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||||
|
defer cancel()
|
||||||
|
_, err := p.resolver.Resolve(ctx, m)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
if err := ping(); err != nil {
|
||||||
|
bo.BackOff(context.Background(), err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
p.serverDown.Store(false)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package clientinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// virtualNetworkIface is the manager for clients from virtual network interface.
|
||||||
|
type virtualNetworkIface struct {
|
||||||
|
ip2name sync.Map // ip => name
|
||||||
|
mac sync.Map // ip => mac
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupHostnameByIP returns hostname of the given VPN client ip.
|
||||||
|
func (v *virtualNetworkIface) LookupHostnameByIP(ip string) string {
|
||||||
|
val, ok := v.ip2name.Load(ip)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return val.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupHostnameByMac always returns empty string.
|
||||||
|
func (v *virtualNetworkIface) LookupHostnameByMac(mac string) string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the string representation of virtualNetworkIface struct.
|
||||||
|
func (v *virtualNetworkIface) String() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// List lists all known VPN clients IP.
|
||||||
|
func (v *virtualNetworkIface) List() []string {
|
||||||
|
if v == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var ips []string
|
||||||
|
v.mac.Range(func(key, value any) bool {
|
||||||
|
ips = append(ips, key.(string))
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return ips
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ server={{ .IP }}#{{ .Port }}
|
|||||||
{{- end}}
|
{{- end}}
|
||||||
{{- if .SendClientInfo}}
|
{{- if .SendClientInfo}}
|
||||||
add-mac
|
add-mac
|
||||||
|
add-subnet=32,128
|
||||||
{{- end}}
|
{{- end}}
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -39,7 +40,10 @@ if [ -n "$pid" ] && [ -f "/proc/${pid}/cmdline" ]; then
|
|||||||
pc_append "server={{ .IP }}#{{ .Port }}" "$config_file"
|
pc_append "server={{ .IP }}#{{ .Port }}" "$config_file"
|
||||||
{{- end}}
|
{{- end}}
|
||||||
{{- if .SendClientInfo}}
|
{{- if .SendClientInfo}}
|
||||||
|
pc_delete "add-mac" "$config_file"
|
||||||
|
pc_delete "add-subnet" "$config_file"
|
||||||
pc_append "add-mac" "$config_file" # add client mac
|
pc_append "add-mac" "$config_file" # add client mac
|
||||||
|
pc_append "add-subnet=32,128" "$config_file" # add client ip
|
||||||
{{- end}}
|
{{- end}}
|
||||||
pc_delete "dnssec" "$config_file" # disable DNSSEC
|
pc_delete "dnssec" "$config_file" # disable DNSSEC
|
||||||
pc_delete "trust-anchor=" "$config_file" # disable DNSSEC
|
pc_delete "trust-anchor=" "$config_file" # disable DNSSEC
|
||||||
|
|||||||
@@ -169,9 +169,16 @@ func ContentFilteringEnabled() bool {
|
|||||||
return err == nil && !st.IsDir()
|
return err == nil && !st.IsDir()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func LeaseFileDir() string {
|
||||||
|
if checkUSG() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return "/run"
|
||||||
|
}
|
||||||
|
|
||||||
func checkUSG() bool {
|
func checkUSG() bool {
|
||||||
out, _ := exec.Command("mca-cli-op", "info").Output()
|
out, _ := os.ReadFile("/etc/version")
|
||||||
return bytes.Contains(out, []byte("UniFi-Gateway-"))
|
return bytes.HasPrefix(out, []byte("UniFiSecurityGateway."))
|
||||||
}
|
}
|
||||||
|
|
||||||
func restartDNSMasq() error {
|
func restartDNSMasq() error {
|
||||||
|
|||||||
@@ -173,20 +173,6 @@ func CanListenLocalhost() bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceDependencies returns list of dependencies that ctrld services needs on this router.
|
|
||||||
// See https://pkg.go.dev/github.com/kardianos/service#Config for list format.
|
|
||||||
func ServiceDependencies() []string {
|
|
||||||
if Name() == edgeos.Name {
|
|
||||||
// On EdeOS, ctrld needs to start after vyatta-dhcpd, so it can read leases file.
|
|
||||||
return []string{
|
|
||||||
"Wants=vyatta-dhcpd.service",
|
|
||||||
"After=vyatta-dhcpd.service",
|
|
||||||
"Wants=dnsmasq.service",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SelfInterfaces return list of *net.Interface that will be source of requests from router itself.
|
// SelfInterfaces return list of *net.Interface that will be source of requests from router itself.
|
||||||
func SelfInterfaces() []*net.Interface {
|
func SelfInterfaces() []*net.Interface {
|
||||||
switch Name() {
|
switch Name() {
|
||||||
@@ -197,6 +183,14 @@ func SelfInterfaces() []*net.Interface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LeaseFilesDir is the directory which contains lease files.
|
||||||
|
func LeaseFilesDir() string {
|
||||||
|
if Name() == edgeos.Name {
|
||||||
|
edgeos.LeaseFileDir()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func distroName() string {
|
func distroName() string {
|
||||||
switch {
|
switch {
|
||||||
case bytes.HasPrefix(unameO(), []byte("DD-WRT")):
|
case bytes.HasPrefix(unameO(), []byte("DD-WRT")):
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
//go:build unix
|
||||||
|
|
||||||
|
package ctrld
|
||||||
|
|
||||||
|
import "github.com/Control-D-Inc/ctrld/internal/resolvconffile"
|
||||||
|
|
||||||
|
func nameserversFromResolvconf() []string {
|
||||||
|
return resolvconffile.NameServers("")
|
||||||
|
}
|
||||||
@@ -58,3 +58,7 @@ func dnsFromAdapter() []string {
|
|||||||
}
|
}
|
||||||
return ns
|
return ns
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func nameserversFromResolvconf() []string {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
+38
-7
@@ -27,13 +27,15 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var bootstrapDNS = "76.76.2.0"
|
var bootstrapDNS = "76.76.2.0"
|
||||||
var or = &osResolver{nameservers: nameservers()}
|
|
||||||
|
|
||||||
func init() {
|
// or is the Resolver used for ResolverTypeOS.
|
||||||
if len(or.nameservers) == 0 {
|
var or = &osResolver{nameservers: defaultNameservers()}
|
||||||
// Add bootstrap DNS in case we did not find any.
|
|
||||||
or.nameservers = []string{net.JoinHostPort(bootstrapDNS, "53")}
|
// defaultNameservers returns OS nameservers plus ctrld bootstrap nameserver.
|
||||||
}
|
func defaultNameservers() []string {
|
||||||
|
ns := nameservers()
|
||||||
|
ns = append(ns, net.JoinHostPort(bootstrapDNS, "53"))
|
||||||
|
return ns
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolver is the interface that wraps the basic DNS operations.
|
// Resolver is the interface that wraps the basic DNS operations.
|
||||||
@@ -237,13 +239,25 @@ func NewBootstrapResolver(servers ...string) Resolver {
|
|||||||
return resolver
|
return resolver
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPrivateResolver returns an OS resolver, which includes only private DNS servers.
|
// NewPrivateResolver returns an OS resolver, which includes only private DNS servers,
|
||||||
|
// excluding nameservers from /etc/resolv.conf file.
|
||||||
|
//
|
||||||
// This is useful for doing PTR lookup in LAN network.
|
// This is useful for doing PTR lookup in LAN network.
|
||||||
func NewPrivateResolver() Resolver {
|
func NewPrivateResolver() Resolver {
|
||||||
nss := nameservers()
|
nss := nameservers()
|
||||||
|
resolveConfNss := nameserversFromResolvconf()
|
||||||
n := 0
|
n := 0
|
||||||
for _, ns := range nss {
|
for _, ns := range nss {
|
||||||
host, _, _ := net.SplitHostPort(ns)
|
host, _, _ := net.SplitHostPort(ns)
|
||||||
|
// Ignore nameserver from resolve.conf file, because the nameserver can be either:
|
||||||
|
//
|
||||||
|
// - ctrld itself.
|
||||||
|
// - Direct listener that has ctrld as an upstream (e.g: dnsmasq).
|
||||||
|
//
|
||||||
|
// causing the query always succeed.
|
||||||
|
if sliceContains(resolveConfNss, host) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
ip := net.ParseIP(host)
|
ip := net.ParseIP(host)
|
||||||
if ip != nil && ip.IsPrivate() && !ip.IsLoopback() {
|
if ip != nil && ip.IsPrivate() && !ip.IsLoopback() {
|
||||||
nss[n] = ns
|
nss[n] = ns
|
||||||
@@ -269,3 +283,20 @@ func newDialer(dnsAddress string) *net.Dialer {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(cuonglm): use slices.Contains once upgrading to go1.21
|
||||||
|
// sliceContains reports whether v is present in s.
|
||||||
|
func sliceContains[S ~[]E, E comparable](s S, v E) bool {
|
||||||
|
return sliceIndex(s, v) >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// sliceIndex returns the index of the first occurrence of v in s,
|
||||||
|
// or -1 if not present.
|
||||||
|
func sliceIndex[S ~[]E, E comparable](s S, v E) int {
|
||||||
|
for i := range s {
|
||||||
|
if v == s[i] {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -59,7 +59,7 @@ compress() {
|
|||||||
build() {
|
build() {
|
||||||
goos=$1
|
goos=$1
|
||||||
goarch=$2
|
goarch=$2
|
||||||
ldflags="-s -w -X github.com/Windscribe/ctrld/cmd/cli.version="${CI_COMMIT_TAG:-dev}" -X github.com/Windscribe/ctrld/cmd/cli.commit=$(git rev-parse HEAD)"
|
ldflags="-s -w -X github.com/Control-D-Inc/ctrld/cmd/cli.version="${CI_COMMIT_TAG:-dev}" -X github.com/Control-D-Inc/ctrld/cmd/cli.commit=$(git rev-parse HEAD)"
|
||||||
|
|
||||||
case $3 in
|
case $3 in
|
||||||
5 | 6 | 7)
|
5 | 6 | 7)
|
||||||
|
|||||||
Reference in New Issue
Block a user