mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-07-16 13:17:19 +02:00
Compare commits
83 Commits
v1.3.1
...
fix-doc-links
| Author | SHA1 | Date | |
|---|---|---|---|
| 6bb9e7a766 | |||
| 61fb71b1fa | |||
| f8967c376f | |||
| 6d3c86c0be | |||
| e42554f892 | |||
| 28984090e5 | |||
| 251255c746 | |||
| 32709dc64c | |||
| 71f26a6d81 | |||
| 44352f8006 | |||
| af38623590 | |||
| 9c1665a759 | |||
| eaad24e5e5 | |||
| cfaf32f71a | |||
| 51b235b61a | |||
| 0a6d9d4454 | |||
| dc700bbd52 | |||
| cb445825f4 | |||
| 4d996e317b | |||
| 30c9012004 | |||
| 2a23feaf4b | |||
| b82ad3720c | |||
| 8d2cb6091e | |||
| 3023f33dff | |||
| 22e97e981a | |||
| 44484e1231 | |||
| eac60b87c7 | |||
| 8db28cb76e | |||
| 8dbe828b99 | |||
| 5c24acd952 | |||
| 998b9a5c5d | |||
| 0084e9ef26 | |||
| 122600bff2 | |||
| 41846b6d4c | |||
| dfbcb1489d | |||
| 684019c2e3 | |||
| e92619620d | |||
| cebfd12d5c | |||
| 874ff01ab8 | |||
| 0bb8703f78 | |||
| 0bb51aa71d | |||
| af2c1c87e0 | |||
| 8939debbc0 | |||
| 7591a0ccc6 | |||
| c3ff8182af | |||
| 5897c174d3 | |||
| f9a3f4c045 | |||
| a2cb895cdc | |||
| 2bebe93e47 | |||
| 28ec1869fc | |||
| 17f6d7a77b | |||
| 9e6e647ff8 | |||
| a2116e5eb5 | |||
| 564c9ef712 | |||
| 856abb71b7 | |||
| 0a30fdea69 | |||
| 4f125cf107 | |||
| 494d8be777 | |||
| cd9c750884 | |||
| 91d319804b | |||
| 180eae60f2 | |||
| d01f5c2777 | |||
| 294a90a807 | |||
| c3b4ae9c79 | |||
| 09188bedf7 | |||
| 4614b98e94 | |||
| 990bc620f7 | |||
| efb5a92571 | |||
| 8e0a96a44c | |||
| 43ff2f648c | |||
| 4816a09e3a | |||
| 3fea92c8b1 | |||
| 63f959c951 | |||
| 44ba6aadd9 | |||
| d88cf52b4e | |||
| 58a00ea24a | |||
| 712b23a4bb | |||
| baf836557c | |||
| 904b23eeac | |||
| 6aafe445f5 | |||
| ebd516855b | |||
| df4e04719e | |||
| 2440d922c6 |
@@ -61,7 +61,7 @@ $ docker pull controldns/ctrld
|
|||||||
Alternatively, if you know what you're doing you can download pre-compiled binaries from the [Releases](https://github.com/Control-D-Inc/ctrld/releases) section for the appropriate platform.
|
Alternatively, if you know what you're doing you can download pre-compiled binaries from the [Releases](https://github.com/Control-D-Inc/ctrld/releases) section for the appropriate platform.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
Lastly, you can build `ctrld` from source which requires `go1.19+`:
|
Lastly, you can build `ctrld` from source which requires `go1.20+`:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
$ go build ./cmd/ctrld
|
$ go build ./cmd/ctrld
|
||||||
|
|||||||
+6
-4
@@ -5,10 +5,11 @@ type ClientInfoCtxKey struct{}
|
|||||||
|
|
||||||
// ClientInfo represents ctrld's clients information.
|
// ClientInfo represents ctrld's clients information.
|
||||||
type ClientInfo struct {
|
type ClientInfo struct {
|
||||||
Mac string
|
Mac string
|
||||||
IP string
|
IP string
|
||||||
Hostname string
|
Hostname string
|
||||||
Self bool
|
Self bool
|
||||||
|
ClientIDPref string
|
||||||
}
|
}
|
||||||
|
|
||||||
// LeaseFileFormat specifies the format of DHCP lease file.
|
// LeaseFileFormat specifies the format of DHCP lease file.
|
||||||
@@ -17,4 +18,5 @@ type LeaseFileFormat string
|
|||||||
const (
|
const (
|
||||||
Dnsmasq LeaseFileFormat = "dnsmasq"
|
Dnsmasq LeaseFileFormat = "dnsmasq"
|
||||||
IscDhcpd LeaseFileFormat = "isc-dhcpd"
|
IscDhcpd LeaseFileFormat = "isc-dhcpd"
|
||||||
|
KeaDHCP4 LeaseFileFormat = "kea-dhcp4"
|
||||||
)
|
)
|
||||||
|
|||||||
+353
-102
@@ -144,7 +144,9 @@ func initCLI() {
|
|||||||
_ = runCmd.Flags().MarkHidden("homedir")
|
_ = runCmd.Flags().MarkHidden("homedir")
|
||||||
runCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`)
|
runCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`)
|
||||||
_ = runCmd.Flags().MarkHidden("iface")
|
_ = runCmd.Flags().MarkHidden("iface")
|
||||||
|
runCmd.Flags().StringVarP(&cdUpstreamProto, "proto", "", ctrld.ResolverTypeDOH, `Control D upstream type, either "doh" or "doh3"`)
|
||||||
|
|
||||||
|
runCmd.FParseErrWhitelist = cobra.FParseErrWhitelist{UnknownFlags: true}
|
||||||
rootCmd.AddCommand(runCmd)
|
rootCmd.AddCommand(runCmd)
|
||||||
|
|
||||||
startCmd := &cobra.Command{
|
startCmd := &cobra.Command{
|
||||||
@@ -158,6 +160,7 @@ func initCLI() {
|
|||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
checkStrFlagEmpty(cmd, cdUidFlagName)
|
checkStrFlagEmpty(cmd, cdUidFlagName)
|
||||||
checkStrFlagEmpty(cmd, cdOrgFlagName)
|
checkStrFlagEmpty(cmd, cdOrgFlagName)
|
||||||
|
validateCdAndNextDNSFlags()
|
||||||
sc := &service.Config{}
|
sc := &service.Config{}
|
||||||
*sc = *svcConfig
|
*sc = *svcConfig
|
||||||
osArgs := os.Args[2:]
|
osArgs := os.Args[2:]
|
||||||
@@ -176,6 +179,9 @@ func initCLI() {
|
|||||||
// Pass --cd flag to "ctrld run" command, so the provision token takes no effect.
|
// Pass --cd flag to "ctrld run" command, so the provision token takes no effect.
|
||||||
sc.Arguments = append(sc.Arguments, "--cd="+cdUID)
|
sc.Arguments = append(sc.Arguments, "--cd="+cdUID)
|
||||||
}
|
}
|
||||||
|
if cdUID != "" {
|
||||||
|
validateCdUpstreamProtocol()
|
||||||
|
}
|
||||||
|
|
||||||
p := &prog{
|
p := &prog{
|
||||||
router: router.New(&cfg, cdUID != ""),
|
router: router.New(&cfg, cdUID != ""),
|
||||||
@@ -201,7 +207,11 @@ func initCLI() {
|
|||||||
defaultConfigFile = filepath.Join(dir, defaultConfigFile)
|
defaultConfigFile = filepath.Join(dir, defaultConfigFile)
|
||||||
}
|
}
|
||||||
sc.Arguments = append(sc.Arguments, "--homedir="+dir)
|
sc.Arguments = append(sc.Arguments, "--homedir="+dir)
|
||||||
sockPath := filepath.Join(dir, ctrldLogUnixSock)
|
sockDir := dir
|
||||||
|
if d, err := socketDir(); err == nil {
|
||||||
|
sockDir = d
|
||||||
|
}
|
||||||
|
sockPath := filepath.Join(sockDir, ctrldLogUnixSock)
|
||||||
_ = os.Remove(sockPath)
|
_ = os.Remove(sockPath)
|
||||||
go func() {
|
go func() {
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -223,7 +233,7 @@ func initCLI() {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
tryReadingConfig(writeDefaultConfig)
|
tryReadingConfigWithNotice(writeDefaultConfig, true)
|
||||||
|
|
||||||
if err := v.Unmarshal(&cfg); err != nil {
|
if err := v.Unmarshal(&cfg); err != nil {
|
||||||
mainLog.Load().Fatal().Msgf("failed to unmarshal config: %v", err)
|
mainLog.Load().Fatal().Msgf("failed to unmarshal config: %v", err)
|
||||||
@@ -231,6 +241,10 @@ func initCLI() {
|
|||||||
|
|
||||||
initLogging()
|
initLogging()
|
||||||
|
|
||||||
|
if nextdns != "" {
|
||||||
|
removeNextDNSFromArgs(sc)
|
||||||
|
}
|
||||||
|
|
||||||
// Explicitly passing config, so on system where home directory could not be obtained,
|
// Explicitly passing config, so on system where home directory could not be obtained,
|
||||||
// or sub-process env is different with the parent, we still behave correctly and use
|
// or sub-process env is different with the parent, we still behave correctly and use
|
||||||
// the expected config file.
|
// the expected config file.
|
||||||
@@ -244,16 +258,20 @@ func initCLI() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if router.Name() != "" {
|
if router.Name() != "" && iface != "" {
|
||||||
mainLog.Load().Debug().Msg("cleaning up router before installing")
|
mainLog.Load().Debug().Msg("cleaning up router before installing")
|
||||||
_ = p.router.Cleanup()
|
_ = p.router.Cleanup()
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks := []task{
|
tasks := []task{
|
||||||
{s.Stop, false},
|
{s.Stop, false},
|
||||||
|
{func() error { return doGenerateNextDNSConfig(nextdns) }, true},
|
||||||
{s.Uninstall, false},
|
{s.Uninstall, false},
|
||||||
{s.Install, false},
|
{s.Install, false},
|
||||||
{s.Start, true},
|
{s.Start, true},
|
||||||
|
// Note that startCmd do not actually write ControlD config, but the config file was
|
||||||
|
// generated after s.Start, so we notice users here for consistent with nextdns mode.
|
||||||
|
{noticeWritingControlDConfig, false},
|
||||||
}
|
}
|
||||||
mainLog.Load().Notice().Msg("Starting service")
|
mainLog.Load().Notice().Msg("Starting service")
|
||||||
if doTasks(tasks) {
|
if doTasks(tasks) {
|
||||||
@@ -281,7 +299,7 @@ func initCLI() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
// Keep these flags in sync with runCmd above, except for "-d".
|
// Keep these flags in sync with runCmd above, except for "-d"/"--nextdns".
|
||||||
startCmd.Flags().StringVarP(&configPath, "config", "c", "", "Path to config file")
|
startCmd.Flags().StringVarP(&configPath, "config", "c", "", "Path to config file")
|
||||||
startCmd.Flags().StringVarP(&configBase64, "base64_config", "", "", "Base64 encoded config")
|
startCmd.Flags().StringVarP(&configBase64, "base64_config", "", "", "Base64 encoded config")
|
||||||
startCmd.Flags().StringVarP(&listenAddress, "listen", "", "", "Listener address and port, in format: address:port")
|
startCmd.Flags().StringVarP(&listenAddress, "listen", "", "", "Listener address and port, in format: address:port")
|
||||||
@@ -295,6 +313,8 @@ func initCLI() {
|
|||||||
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`)
|
||||||
|
startCmd.Flags().StringVarP(&nextdns, nextdnsFlagName, "", "", "NextDNS resolver id")
|
||||||
|
startCmd.Flags().StringVarP(&cdUpstreamProto, "proto", "", ctrld.ResolverTypeDOH, `Control D upstream type, either "doh" or "doh3"`)
|
||||||
|
|
||||||
routerCmd := &cobra.Command{
|
routerCmd := &cobra.Command{
|
||||||
Use: "setup",
|
Use: "setup",
|
||||||
@@ -367,6 +387,10 @@ func initCLI() {
|
|||||||
mainLog.Load().Error().Msg(err.Error())
|
mainLog.Load().Error().Msg(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if _, err := s.Status(); errors.Is(err, service.ErrNotInstalled) {
|
||||||
|
mainLog.Load().Warn().Msg("service not installed")
|
||||||
|
return
|
||||||
|
}
|
||||||
initLogging()
|
initLogging()
|
||||||
|
|
||||||
tasks := []task{
|
tasks := []task{
|
||||||
@@ -374,7 +398,7 @@ func initCLI() {
|
|||||||
{s.Start, true},
|
{s.Start, true},
|
||||||
}
|
}
|
||||||
if doTasks(tasks) {
|
if doTasks(tasks) {
|
||||||
dir, err := userHomeDir()
|
dir, err := socketDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mainLog.Load().Warn().Err(err).Msg("Service was restarted, but could not ping the control server")
|
mainLog.Load().Warn().Err(err).Msg("Service was restarted, but could not ping the control server")
|
||||||
return
|
return
|
||||||
@@ -388,6 +412,50 @@ func initCLI() {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reloadCmd := &cobra.Command{
|
||||||
|
PreRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
initConsoleLogging()
|
||||||
|
checkHasElevatedPrivilege()
|
||||||
|
},
|
||||||
|
Use: "reload",
|
||||||
|
Short: "Reload the ctrld service",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
dir, err := socketDir()
|
||||||
|
if err != nil {
|
||||||
|
mainLog.Load().Fatal().Err(err).Msg("failed to find ctrld home dir")
|
||||||
|
}
|
||||||
|
cc := newControlClient(filepath.Join(dir, ctrldControlUnixSock))
|
||||||
|
resp, err := cc.post(reloadPath, nil)
|
||||||
|
if err != nil {
|
||||||
|
mainLog.Load().Fatal().Err(err).Msg("failed to send reload signal to ctrld")
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
switch resp.StatusCode {
|
||||||
|
case http.StatusOK:
|
||||||
|
mainLog.Load().Notice().Msg("Service reloaded")
|
||||||
|
case http.StatusCreated:
|
||||||
|
s, err := newService(&prog{}, svcConfig)
|
||||||
|
if err != nil {
|
||||||
|
mainLog.Load().Error().Msg(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mainLog.Load().Warn().Msg("Service was reloaded, but new config requires service restart.")
|
||||||
|
mainLog.Load().Warn().Msg("Restarting service")
|
||||||
|
if _, err := s.Status(); errors.Is(err, service.ErrNotInstalled) {
|
||||||
|
mainLog.Load().Warn().Msg("Service not installed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
restartCmd.Run(cmd, args)
|
||||||
|
default:
|
||||||
|
buf, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
mainLog.Load().Fatal().Err(err).Msg("could not read response from control server")
|
||||||
|
}
|
||||||
|
mainLog.Load().Error().Err(err).Msgf("failed to reload ctrld: %s", string(buf))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
statusCmd := &cobra.Command{
|
statusCmd := &cobra.Command{
|
||||||
Use: "status",
|
Use: "status",
|
||||||
Short: "Show status of the ctrld service",
|
Short: "Show status of the ctrld service",
|
||||||
@@ -503,9 +571,10 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`,
|
|||||||
Short: "Manage ctrld service",
|
Short: "Manage ctrld service",
|
||||||
Args: cobra.OnlyValidArgs,
|
Args: cobra.OnlyValidArgs,
|
||||||
ValidArgs: []string{
|
ValidArgs: []string{
|
||||||
statusCmd.Use,
|
startCmd.Use,
|
||||||
stopCmd.Use,
|
stopCmd.Use,
|
||||||
restartCmd.Use,
|
restartCmd.Use,
|
||||||
|
reloadCmd.Use,
|
||||||
statusCmd.Use,
|
statusCmd.Use,
|
||||||
uninstallCmd.Use,
|
uninstallCmd.Use,
|
||||||
interfacesCmd.Use,
|
interfacesCmd.Use,
|
||||||
@@ -514,6 +583,7 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`,
|
|||||||
serviceCmd.AddCommand(startCmd)
|
serviceCmd.AddCommand(startCmd)
|
||||||
serviceCmd.AddCommand(stopCmd)
|
serviceCmd.AddCommand(stopCmd)
|
||||||
serviceCmd.AddCommand(restartCmd)
|
serviceCmd.AddCommand(restartCmd)
|
||||||
|
serviceCmd.AddCommand(reloadCmd)
|
||||||
serviceCmd.AddCommand(statusCmd)
|
serviceCmd.AddCommand(statusCmd)
|
||||||
serviceCmd.AddCommand(uninstallCmd)
|
serviceCmd.AddCommand(uninstallCmd)
|
||||||
serviceCmd.AddCommand(interfacesCmd)
|
serviceCmd.AddCommand(interfacesCmd)
|
||||||
@@ -568,6 +638,19 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`,
|
|||||||
}
|
}
|
||||||
rootCmd.AddCommand(restartCmdAlias)
|
rootCmd.AddCommand(restartCmdAlias)
|
||||||
|
|
||||||
|
reloadCmdAlias := &cobra.Command{
|
||||||
|
PreRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
initConsoleLogging()
|
||||||
|
checkHasElevatedPrivilege()
|
||||||
|
},
|
||||||
|
Use: "reload",
|
||||||
|
Short: "Reload the ctrld service",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
reloadCmd.Run(cmd, args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
rootCmd.AddCommand(reloadCmdAlias)
|
||||||
|
|
||||||
statusCmdAlias := &cobra.Command{
|
statusCmdAlias := &cobra.Command{
|
||||||
Use: "status",
|
Use: "status",
|
||||||
Short: "Show status of the ctrld service",
|
Short: "Show status of the ctrld service",
|
||||||
@@ -610,7 +693,7 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`,
|
|||||||
checkHasElevatedPrivilege()
|
checkHasElevatedPrivilege()
|
||||||
},
|
},
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
dir, err := userHomeDir()
|
dir, err := socketDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mainLog.Load().Fatal().Err(err).Msg("failed to find ctrld home dir")
|
mainLog.Load().Fatal().Err(err).Msg("failed to find ctrld home dir")
|
||||||
}
|
}
|
||||||
@@ -636,6 +719,10 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`,
|
|||||||
sort.Strings(s)
|
sort.Strings(s)
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
// If metrics is enabled, server set this for all clients, so we can check only the first one.
|
||||||
|
// Ideally, we may have a field in response to indicate that query count should be shown, but
|
||||||
|
// it would break earlier version of ctrld, which only look list of clients in response.
|
||||||
|
withQueryCount := len(clients) > 0 && clients[0].IncludeQueryCount
|
||||||
data := make([][]string, len(clients))
|
data := make([][]string, len(clients))
|
||||||
for i, c := range clients {
|
for i, c := range clients {
|
||||||
row := []string{
|
row := []string{
|
||||||
@@ -644,10 +731,17 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`,
|
|||||||
c.Mac,
|
c.Mac,
|
||||||
strings.Join(map2Slice(c.Source), ","),
|
strings.Join(map2Slice(c.Source), ","),
|
||||||
}
|
}
|
||||||
|
if withQueryCount {
|
||||||
|
row = append(row, strconv.FormatInt(c.QueryCount, 10))
|
||||||
|
}
|
||||||
data[i] = row
|
data[i] = row
|
||||||
}
|
}
|
||||||
table := tablewriter.NewWriter(os.Stdout)
|
table := tablewriter.NewWriter(os.Stdout)
|
||||||
table.SetHeader([]string{"IP", "Hostname", "Mac", "Discovered"})
|
headers := []string{"IP", "Hostname", "Mac", "Discovered"}
|
||||||
|
if withQueryCount {
|
||||||
|
headers = append(headers, "Queries")
|
||||||
|
}
|
||||||
|
table.SetHeader(headers)
|
||||||
table.SetAutoFormatHeaders(false)
|
table.SetAutoFormatHeaders(false)
|
||||||
table.AppendBulk(data)
|
table.AppendBulk(data)
|
||||||
table.Render()
|
table.Render()
|
||||||
@@ -670,6 +764,11 @@ func isMobile() bool {
|
|||||||
return runtime.GOOS == "android" || runtime.GOOS == "ios"
|
return runtime.GOOS == "android" || runtime.GOOS == "ios"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isAndroid reports whether the current OS is Android.
|
||||||
|
func isAndroid() bool {
|
||||||
|
return runtime.GOOS == "android"
|
||||||
|
}
|
||||||
|
|
||||||
// RunCobraCommand runs ctrld cli.
|
// RunCobraCommand runs ctrld cli.
|
||||||
func RunCobraCommand(cmd *cobra.Command) {
|
func RunCobraCommand(cmd *cobra.Command) {
|
||||||
noConfigStart = isNoConfigStart(cmd)
|
noConfigStart = isNoConfigStart(cmd)
|
||||||
@@ -688,6 +787,7 @@ func RunMobile(appConfig *AppConfig, appCallback *AppCallback, stopCh chan struc
|
|||||||
homedir = appConfig.HomeDir
|
homedir = appConfig.HomeDir
|
||||||
verbose = appConfig.Verbose
|
verbose = appConfig.Verbose
|
||||||
cdUID = appConfig.CdUID
|
cdUID = appConfig.CdUID
|
||||||
|
cdUpstreamProto = appConfig.UpstreamProto
|
||||||
logPath = appConfig.LogPath
|
logPath = appConfig.LogPath
|
||||||
run(appCallback, stopCh)
|
run(appCallback, stopCh)
|
||||||
}
|
}
|
||||||
@@ -699,17 +799,23 @@ func run(appCallback *AppCallback, stopCh chan struct{}) {
|
|||||||
}
|
}
|
||||||
waitCh := make(chan struct{})
|
waitCh := make(chan struct{})
|
||||||
p := &prog{
|
p := &prog{
|
||||||
waitCh: waitCh,
|
waitCh: waitCh,
|
||||||
stopCh: stopCh,
|
stopCh: stopCh,
|
||||||
cfg: &cfg,
|
reloadCh: make(chan struct{}),
|
||||||
appCallback: appCallback,
|
reloadDoneCh: make(chan struct{}),
|
||||||
|
cfg: &cfg,
|
||||||
|
appCallback: appCallback,
|
||||||
}
|
}
|
||||||
if homedir == "" {
|
if homedir == "" {
|
||||||
if dir, err := userHomeDir(); err == nil {
|
if dir, err := userHomeDir(); err == nil {
|
||||||
homedir = dir
|
homedir = dir
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sockPath := filepath.Join(homedir, ctrldLogUnixSock)
|
sockDir := homedir
|
||||||
|
if d, err := socketDir(); err == nil {
|
||||||
|
sockDir = d
|
||||||
|
}
|
||||||
|
sockPath := filepath.Join(sockDir, ctrldLogUnixSock)
|
||||||
if addr, err := net.ResolveUnixAddr("unix", sockPath); err == nil {
|
if addr, err := net.ResolveUnixAddr("unix", sockPath); err == nil {
|
||||||
if conn, err := net.Dial(addr.Network(), addr.String()); err == nil {
|
if conn, err := net.Dial(addr.Network(), addr.String()); err == nil {
|
||||||
lc := &logConn{conn: conn}
|
lc := &logConn{conn: conn}
|
||||||
@@ -740,9 +846,11 @@ func run(appCallback *AppCallback, stopCh chan struct{}) {
|
|||||||
|
|
||||||
readBase64Config(configBase64)
|
readBase64Config(configBase64)
|
||||||
processNoConfigFlags(noConfigStart)
|
processNoConfigFlags(noConfigStart)
|
||||||
|
p.mu.Lock()
|
||||||
if err := v.Unmarshal(&cfg); err != nil {
|
if err := v.Unmarshal(&cfg); err != nil {
|
||||||
mainLog.Load().Fatal().Msgf("failed to unmarshal config: %v", err)
|
mainLog.Load().Fatal().Msgf("failed to unmarshal config: %v", err)
|
||||||
}
|
}
|
||||||
|
p.mu.Unlock()
|
||||||
|
|
||||||
processLogAndCacheFlags()
|
processLogAndCacheFlags()
|
||||||
|
|
||||||
@@ -759,7 +867,7 @@ func run(appCallback *AppCallback, stopCh chan struct{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
p.router = router.New(&cfg, cdUID != "")
|
p.router = router.New(&cfg, cdUID != "")
|
||||||
cs, err := newControlServer(filepath.Join(homedir, ctrldControlUnixSock))
|
cs, err := newControlServer(filepath.Join(sockDir, ctrldControlUnixSock))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mainLog.Load().Warn().Err(err).Msg("could not create control server")
|
mainLog.Load().Warn().Err(err).Msg("could not create control server")
|
||||||
}
|
}
|
||||||
@@ -777,14 +885,47 @@ func run(appCallback *AppCallback, stopCh chan struct{}) {
|
|||||||
cdUID = uid
|
cdUID = uid
|
||||||
}
|
}
|
||||||
if cdUID != "" {
|
if cdUID != "" {
|
||||||
err := processCDFlags()
|
validateCdUpstreamProtocol()
|
||||||
if err != nil {
|
if err := processCDFlags(&cfg); err != nil {
|
||||||
appCallback.Exit(err.Error())
|
if isMobile() {
|
||||||
return
|
appCallback.Exit(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uninstallIfInvalidCdUID := func() {
|
||||||
|
cdLogger := mainLog.Load().With().Str("mode", "cd").Logger()
|
||||||
|
if uer, ok := err.(*controld.UtilityErrorResponse); ok && uer.ErrorField.Code == controld.InvalidConfigCode {
|
||||||
|
s, err := newService(&prog{}, svcConfig)
|
||||||
|
if err != nil {
|
||||||
|
cdLogger.Warn().Err(err).Msg("failed to create new service")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if netIface, _ := netInterface(iface); netIface != nil {
|
||||||
|
if err := restoreNetworkManager(); err != nil {
|
||||||
|
cdLogger.Error().Err(err).Msg("could not restore NetworkManager")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cdLogger.Debug().Str("iface", netIface.Name).Msg("Restoring DNS for interface")
|
||||||
|
if err := resetDNS(netIface); err != nil {
|
||||||
|
cdLogger.Warn().Err(err).Msg("something went wrong while restoring DNS")
|
||||||
|
} else {
|
||||||
|
cdLogger.Debug().Str("iface", netIface.Name).Msg("Restoring DNS successfully")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks := []task{{s.Uninstall, true}}
|
||||||
|
if doTasks(tasks) {
|
||||||
|
cdLogger.Info().Msg("uninstalled service")
|
||||||
|
}
|
||||||
|
cdLogger.Fatal().Err(uer).Msg("failed to fetch resolver config")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uninstallIfInvalidCdUID()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updated := updateListenerConfig()
|
updated := updateListenerConfig(&cfg)
|
||||||
|
|
||||||
if cdUID != "" {
|
if cdUID != "" {
|
||||||
processLogAndCacheFlags()
|
processLogAndCacheFlags()
|
||||||
@@ -812,7 +953,9 @@ func run(appCallback *AppCallback, stopCh chan struct{}) {
|
|||||||
initLoggingWithBackup(false)
|
initLoggingWithBackup(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
validateConfig(&cfg)
|
if err := validateConfig(&cfg); err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
initCache()
|
initCache()
|
||||||
|
|
||||||
if daemon {
|
if daemon {
|
||||||
@@ -859,19 +1002,21 @@ func run(appCallback *AppCallback, stopCh chan struct{}) {
|
|||||||
if cp := router.CertPool(); cp != nil {
|
if cp := router.CertPool(); cp != nil {
|
||||||
rootCertPool = cp
|
rootCertPool = cp
|
||||||
}
|
}
|
||||||
p.onStarted = append(p.onStarted, func() {
|
if iface != "" {
|
||||||
mainLog.Load().Debug().Msg("router setup on start")
|
p.onStarted = append(p.onStarted, func() {
|
||||||
if err := p.router.Setup(); err != nil {
|
mainLog.Load().Debug().Msg("router setup on start")
|
||||||
mainLog.Load().Error().Err(err).Msg("could not configure router")
|
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")
|
p.onStopped = append(p.onStopped, func() {
|
||||||
if err := p.router.Cleanup(); err != nil {
|
mainLog.Load().Debug().Msg("router cleanup on stop")
|
||||||
mainLog.Load().Error().Err(err).Msg("could not cleanup router")
|
if err := p.router.Cleanup(); err != nil {
|
||||||
}
|
mainLog.Load().Error().Err(err).Msg("could not cleanup router")
|
||||||
p.resetDNS()
|
}
|
||||||
})
|
p.resetDNS()
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
close(waitCh)
|
close(waitCh)
|
||||||
@@ -907,10 +1052,17 @@ func writeConfigFile() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func readConfigFile(writeDefaultConfig bool) bool {
|
// readConfigFile reads in config file.
|
||||||
|
//
|
||||||
|
// - It writes default config file if config file not found if writeDefaultConfig is true.
|
||||||
|
// - It emits notice message to user if notice is true.
|
||||||
|
func readConfigFile(writeDefaultConfig, notice bool) bool {
|
||||||
// If err == nil, there's a config supplied via `--config`, no default config written.
|
// If err == nil, there's a config supplied via `--config`, no default config written.
|
||||||
err := v.ReadInConfig()
|
err := v.ReadInConfig()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
if notice {
|
||||||
|
mainLog.Load().Notice().Msg("Reading config: " + v.ConfigFileUsed())
|
||||||
|
}
|
||||||
mainLog.Load().Info().Msg("loading config file from: " + v.ConfigFileUsed())
|
mainLog.Load().Info().Msg("loading config file from: " + v.ConfigFileUsed())
|
||||||
defaultConfigFile = v.ConfigFileUsed()
|
defaultConfigFile = v.ConfigFileUsed()
|
||||||
return true
|
return true
|
||||||
@@ -925,7 +1077,8 @@ 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()
|
nop := zerolog.Nop()
|
||||||
|
_, _ = tryUpdateListenerConfig(&cfg, &nop, true)
|
||||||
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 {
|
||||||
@@ -933,6 +1086,9 @@ func readConfigFile(writeDefaultConfig bool) bool {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
mainLog.Load().Fatal().Msgf("failed to get default config file path: %v", err)
|
mainLog.Load().Fatal().Msgf("failed to get default config file path: %v", err)
|
||||||
}
|
}
|
||||||
|
if cdUID == "" && nextdns == "" {
|
||||||
|
mainLog.Load().Notice().Msg("Generating controld default config: " + fp)
|
||||||
|
}
|
||||||
mainLog.Load().Info().Msg("writing default config file to: " + fp)
|
mainLog.Load().Info().Msg("writing default config file to: " + fp)
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -1015,7 +1171,7 @@ func processNoConfigFlags(noConfigStart bool) {
|
|||||||
v.Set("upstream", upstream)
|
v.Set("upstream", upstream)
|
||||||
}
|
}
|
||||||
|
|
||||||
func processCDFlags() error {
|
func processCDFlags(cfg *ctrld.Config) 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)
|
||||||
@@ -1031,44 +1187,17 @@ func processCDFlags() error {
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if uer, ok := err.(*controld.UtilityErrorResponse); ok && uer.ErrorField.Code == controld.InvalidConfigCode {
|
|
||||||
s, err := newService(&prog{}, svcConfig)
|
|
||||||
if err != nil {
|
|
||||||
logger.Warn().Err(err).Msg("failed to create new service")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if netIface, _ := netInterface(iface); netIface != nil {
|
|
||||||
if err := restoreNetworkManager(); err != nil {
|
|
||||||
logger.Error().Err(err).Msg("could not restore NetworkManager")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
logger.Debug().Str("iface", netIface.Name).Msg("Restoring DNS for interface")
|
|
||||||
if err := resetDNS(netIface); err != nil {
|
|
||||||
logger.Warn().Err(err).Msg("something went wrong while restoring DNS")
|
|
||||||
} else {
|
|
||||||
logger.Debug().Str("iface", netIface.Name).Msg("Restoring DNS successfully")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks := []task{{s.Uninstall, true}}
|
|
||||||
if doTasks(tasks) {
|
|
||||||
logger.Info().Msg("uninstalled service")
|
|
||||||
}
|
|
||||||
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 {
|
||||||
|
if isMobile() {
|
||||||
|
return err
|
||||||
|
}
|
||||||
logger.Warn().Err(err).Msg("could not fetch resolver config")
|
logger.Warn().Err(err).Msg("could not fetch resolver config")
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info().Msg("generating ctrld config from Control-D configuration")
|
logger.Info().Msg("generating ctrld config from Control-D configuration")
|
||||||
cfg = ctrld.Config{}
|
|
||||||
|
|
||||||
|
*cfg = ctrld.Config{}
|
||||||
// Fetch config, unmarshal to cfg.
|
// Fetch config, unmarshal to cfg.
|
||||||
if resolverConfig.Ctrld.CustomConfig != "" {
|
if resolverConfig.Ctrld.CustomConfig != "" {
|
||||||
logger.Info().Msg("using defined custom config of Control-D resolver")
|
logger.Info().Msg("using defined custom config of Control-D resolver")
|
||||||
@@ -1085,7 +1214,7 @@ func processCDFlags() error {
|
|||||||
cfg.Upstream = make(map[string]*ctrld.UpstreamConfig)
|
cfg.Upstream = make(map[string]*ctrld.UpstreamConfig)
|
||||||
cfg.Upstream["0"] = &ctrld.UpstreamConfig{
|
cfg.Upstream["0"] = &ctrld.UpstreamConfig{
|
||||||
Endpoint: resolverConfig.DOH,
|
Endpoint: resolverConfig.DOH,
|
||||||
Type: ctrld.ResolverTypeDOH,
|
Type: cdUpstreamProto,
|
||||||
Timeout: 5000,
|
Timeout: 5000,
|
||||||
}
|
}
|
||||||
rules := make([]ctrld.Rule, 0, len(resolverConfig.Exclude))
|
rules := make([]ctrld.Rule, 0, len(resolverConfig.Exclude))
|
||||||
@@ -1191,7 +1320,7 @@ func selfCheckStatus(s service.Service) service.Status {
|
|||||||
if status != service.StatusRunning {
|
if status != service.StatusRunning {
|
||||||
return status
|
return status
|
||||||
}
|
}
|
||||||
dir, err := userHomeDir()
|
dir, err := socketDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mainLog.Load().Error().Err(err).Msg("failed to check ctrld listener status: could not get home directory")
|
mainLog.Load().Error().Err(err).Msg("failed to check ctrld listener status: could not get home directory")
|
||||||
return service.StatusUnknown
|
return service.StatusUnknown
|
||||||
@@ -1213,6 +1342,11 @@ func selfCheckStatus(s service.Service) service.Status {
|
|||||||
return service.StatusUnknown
|
return service.StatusUnknown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Not a ctrld upstream, return status as-is.
|
||||||
|
if cfg.FirstUpstream().VerifyDomain() == "" {
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
mainLog.Load().Debug().Msg("ctrld listener is ready")
|
mainLog.Load().Debug().Msg("ctrld listener is ready")
|
||||||
mainLog.Load().Debug().Msg("performing self-check")
|
mainLog.Load().Debug().Msg("performing self-check")
|
||||||
bo := backoff.NewBackoff("self-check", logf, 10*time.Second)
|
bo := backoff.NewBackoff("self-check", logf, 10*time.Second)
|
||||||
@@ -1338,21 +1472,48 @@ func userHomeDir() (string, error) {
|
|||||||
return dir, nil
|
return dir, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// socketDir returns directory that ctrld will create socket file for running controlServer.
|
||||||
|
func socketDir() (string, error) {
|
||||||
|
switch {
|
||||||
|
case runtime.GOOS == "windows", isMobile():
|
||||||
|
return userHomeDir()
|
||||||
|
}
|
||||||
|
dir := "/var/run"
|
||||||
|
if ok, _ := dirWritable(dir); !ok {
|
||||||
|
return userHomeDir()
|
||||||
|
}
|
||||||
|
return dir, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// tryReadingConfig is like tryReadingConfigWithNotice, with notice set to false.
|
||||||
func tryReadingConfig(writeDefaultConfig bool) {
|
func tryReadingConfig(writeDefaultConfig bool) {
|
||||||
|
tryReadingConfigWithNotice(writeDefaultConfig, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// tryReadingConfigWithNotice tries reading in config files, either specified by user or from default
|
||||||
|
// locations. If notice is true, emitting a notice message to user which config file was read.
|
||||||
|
func tryReadingConfigWithNotice(writeDefaultConfig, notice bool) {
|
||||||
// --config is specified.
|
// --config is specified.
|
||||||
if configPath != "" {
|
if configPath != "" {
|
||||||
v.SetConfigFile(configPath)
|
v.SetConfigFile(configPath)
|
||||||
readConfigFile(false)
|
readConfigFile(false, notice)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// no config start or base64 config mode.
|
// no config start or base64 config mode.
|
||||||
if !writeDefaultConfig {
|
if !writeDefaultConfig {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
readConfig(writeDefaultConfig)
|
readConfigWithNotice(writeDefaultConfig, notice)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// readConfig calls readConfigWithNotice with notice set to false.
|
||||||
func readConfig(writeDefaultConfig bool) {
|
func readConfig(writeDefaultConfig bool) {
|
||||||
|
readConfigWithNotice(writeDefaultConfig, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// readConfigWithNotice calls readConfigFile with config file set to ctrld.toml
|
||||||
|
// or config.toml for compatible with earlier versions of ctrld.
|
||||||
|
func readConfigWithNotice(writeDefaultConfig, notice bool) {
|
||||||
configs := []struct {
|
configs := []struct {
|
||||||
name string
|
name string
|
||||||
written bool
|
written bool
|
||||||
@@ -1369,7 +1530,7 @@ func readConfig(writeDefaultConfig bool) {
|
|||||||
for _, config := range configs {
|
for _, config := range configs {
|
||||||
ctrld.SetConfigNameWithPath(v, config.name, dir)
|
ctrld.SetConfigNameWithPath(v, config.name, dir)
|
||||||
v.SetConfigFile(configPath)
|
v.SetConfigFile(configPath)
|
||||||
if readConfigFile(config.written) {
|
if readConfigFile(config.written, notice) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1401,18 +1562,17 @@ func uninstall(p *prog, s service.Service) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateConfig(cfg *ctrld.Config) {
|
func validateConfig(cfg *ctrld.Config) error {
|
||||||
err := ctrld.ValidateConfig(validator.New(), cfg)
|
if err := ctrld.ValidateConfig(validator.New(), cfg); err != nil {
|
||||||
if err == nil {
|
var ve validator.ValidationErrors
|
||||||
return
|
if errors.As(err, &ve) {
|
||||||
}
|
for _, fe := range ve {
|
||||||
var ve validator.ValidationErrors
|
mainLog.Load().Error().Msgf("invalid config: %s: %s", fe.Namespace(), fieldErrorMsg(fe))
|
||||||
if errors.As(err, &ve) {
|
}
|
||||||
for _, fe := range ve {
|
|
||||||
mainLog.Load().Error().Msgf("invalid config: %s: %s", fe.Namespace(), fieldErrorMsg(fe))
|
|
||||||
}
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
os.Exit(1)
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: Add more case here once new validation tag is used in ctrld.Config struct.
|
// NOTE: Add more case here once new validation tag is used in ctrld.Config struct.
|
||||||
@@ -1474,18 +1634,36 @@ type listenerConfigCheck struct {
|
|||||||
|
|
||||||
// mobileListenerPort returns hardcoded port for mobile platforms.
|
// mobileListenerPort returns hardcoded port for mobile platforms.
|
||||||
func mobileListenerPort() int {
|
func mobileListenerPort() int {
|
||||||
if runtime.GOOS == "ios" {
|
if isAndroid() {
|
||||||
return 53
|
return 5354
|
||||||
}
|
}
|
||||||
return 5354
|
return 53
|
||||||
|
}
|
||||||
|
|
||||||
|
// mobileListenerIp returns hardcoded listener ip for mobile platforms
|
||||||
|
func mobileListenerIp() string {
|
||||||
|
if isAndroid() {
|
||||||
|
return "0.0.0.0"
|
||||||
|
}
|
||||||
|
return "127.0.0.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
func updateListenerConfig() (updated bool) {
|
func updateListenerConfig(cfg *ctrld.Config) bool {
|
||||||
|
updated, _ := tryUpdateListenerConfig(cfg, nil, true)
|
||||||
|
return updated
|
||||||
|
}
|
||||||
|
|
||||||
|
// tryUpdateListenerConfig tries updating listener config with a working one.
|
||||||
|
// If fatal is true, and there's listen address conflicted, the function do
|
||||||
|
// fatal error.
|
||||||
|
func tryUpdateListenerConfig(cfg *ctrld.Config, infoLogger *zerolog.Logger, fatal bool) (updated, ok bool) {
|
||||||
|
ok = true
|
||||||
lcc := make(map[string]*listenerConfigCheck)
|
lcc := make(map[string]*listenerConfigCheck)
|
||||||
cdMode := cdUID != ""
|
cdMode := cdUID != ""
|
||||||
|
nextdnsMode := nextdns != ""
|
||||||
for n, listener := range cfg.Listener {
|
for n, listener := range cfg.Listener {
|
||||||
lcc[n] = &listenerConfigCheck{}
|
lcc[n] = &listenerConfigCheck{}
|
||||||
if listener.IP == "" {
|
if listener.IP == "" {
|
||||||
@@ -1497,12 +1675,17 @@ func updateListenerConfig() (updated bool) {
|
|||||||
lcc[n].Port = true
|
lcc[n].Port = true
|
||||||
}
|
}
|
||||||
// In cd mode, we always try to pick an ip:port pair to work.
|
// In cd mode, we always try to pick an ip:port pair to work.
|
||||||
if cdMode {
|
// Same if nextdns resolver is used.
|
||||||
|
if cdMode || nextdnsMode {
|
||||||
lcc[n].IP = true
|
lcc[n].IP = true
|
||||||
lcc[n].Port = true
|
lcc[n].Port = true
|
||||||
}
|
}
|
||||||
updated = updated || lcc[n].IP || lcc[n].Port
|
updated = updated || lcc[n].IP || lcc[n].Port
|
||||||
}
|
}
|
||||||
|
il := mainLog.Load()
|
||||||
|
if infoLogger != nil {
|
||||||
|
il = infoLogger
|
||||||
|
}
|
||||||
if isMobile() {
|
if isMobile() {
|
||||||
// On Mobile, only use first listener, ignore others.
|
// On Mobile, only use first listener, ignore others.
|
||||||
firstLn := cfg.FirstListener()
|
firstLn := cfg.FirstListener()
|
||||||
@@ -1511,9 +1694,8 @@ func updateListenerConfig() (updated bool) {
|
|||||||
delete(cfg.Listener, k)
|
delete(cfg.Listener, k)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// In cd mode, always use 127.0.0.1:5354.
|
|
||||||
if cdMode {
|
if cdMode {
|
||||||
firstLn.IP = "127.0.0.1" // Mobile platforms allows running listener only on loop back address.
|
firstLn.IP = mobileListenerIp()
|
||||||
firstLn.Port = mobileListenerPort()
|
firstLn.Port = mobileListenerPort()
|
||||||
// TODO: use clear(lcc) once upgrading to go 1.21
|
// TODO: use clear(lcc) once upgrading to go 1.21
|
||||||
for k := range lcc {
|
for k := range lcc {
|
||||||
@@ -1594,7 +1776,11 @@ func updateListenerConfig() (updated bool) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
if !check.IP && !check.Port {
|
if !check.IP && !check.Port {
|
||||||
logMsg(mainLog.Load().Fatal(), n, "failed to listen: %v", err)
|
if fatal {
|
||||||
|
logMsg(mainLog.Load().Fatal(), n, "failed to listen: %v", err)
|
||||||
|
}
|
||||||
|
ok = false
|
||||||
|
break
|
||||||
}
|
}
|
||||||
if tryAllPort53 {
|
if tryAllPort53 {
|
||||||
tryAllPort53 = false
|
tryAllPort53 = false
|
||||||
@@ -1605,7 +1791,7 @@ func updateListenerConfig() (updated bool) {
|
|||||||
listener.Port = 53
|
listener.Port = 53
|
||||||
}
|
}
|
||||||
if check.IP {
|
if check.IP {
|
||||||
logMsg(mainLog.Load().Warn(), n, "could not listen on address: %s, trying: %s", addr, net.JoinHostPort(listener.IP, strconv.Itoa(listener.Port)))
|
logMsg(il.Info(), n, "could not listen on address: %s, trying: %s", addr, net.JoinHostPort(listener.IP, strconv.Itoa(listener.Port)))
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1618,7 +1804,7 @@ func updateListenerConfig() (updated bool) {
|
|||||||
listener.Port = 53
|
listener.Port = 53
|
||||||
}
|
}
|
||||||
if check.IP {
|
if check.IP {
|
||||||
logMsg(mainLog.Load().Warn(), n, "could not listen on address: %s, trying localhost: %s", addr, net.JoinHostPort(listener.IP, strconv.Itoa(listener.Port)))
|
logMsg(il.Info(), n, "could not listen on address: %s, trying localhost: %s", addr, net.JoinHostPort(listener.IP, strconv.Itoa(listener.Port)))
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1630,7 +1816,7 @@ func updateListenerConfig() (updated bool) {
|
|||||||
if check.Port {
|
if check.Port {
|
||||||
listener.Port = 5354
|
listener.Port = 5354
|
||||||
}
|
}
|
||||||
logMsg(mainLog.Load().Warn(), n, "could not listen on address: %s, trying current ip with port 5354", addr)
|
logMsg(il.Info(), n, "could not listen on address: %s, trying current ip with port 5354", addr)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if tryPort5354 {
|
if tryPort5354 {
|
||||||
@@ -1641,7 +1827,7 @@ func updateListenerConfig() (updated bool) {
|
|||||||
if check.Port {
|
if check.Port {
|
||||||
listener.Port = 5354
|
listener.Port = 5354
|
||||||
}
|
}
|
||||||
logMsg(mainLog.Load().Warn(), n, "could not listen on address: %s, trying 0.0.0.0:5354", addr)
|
logMsg(il.Info(), n, "could not listen on address: %s, trying 0.0.0.0:5354", addr)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if check.IP && !isZeroIP { // for "0.0.0.0" or "::", we only need to try new port.
|
if check.IP && !isZeroIP { // for "0.0.0.0" or "::", we only need to try new port.
|
||||||
@@ -1655,12 +1841,19 @@ func updateListenerConfig() (updated bool) {
|
|||||||
listener.Port = oldPort
|
listener.Port = oldPort
|
||||||
}
|
}
|
||||||
if listener.IP == oldIP && listener.Port == oldPort {
|
if listener.IP == oldIP && listener.Port == oldPort {
|
||||||
logMsg(mainLog.Load().Fatal(), n, "could not listener on %s: %v", net.JoinHostPort(listener.IP, strconv.Itoa(listener.Port)), err)
|
if fatal {
|
||||||
|
logMsg(mainLog.Load().Fatal(), n, "could not listener on %s: %v", net.JoinHostPort(listener.IP, strconv.Itoa(listener.Port)), err)
|
||||||
|
}
|
||||||
|
ok = false
|
||||||
|
break
|
||||||
}
|
}
|
||||||
logMsg(mainLog.Load().Warn(), n, "could not listen on address: %s, pick a random ip+port", addr)
|
logMsg(il.Info(), n, "could not listen on address: %s, pick a random ip+port", addr)
|
||||||
attempts++
|
attempts++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Specific case for systemd-resolved.
|
// Specific case for systemd-resolved.
|
||||||
if useSystemdResolved {
|
if useSystemdResolved {
|
||||||
@@ -1670,7 +1863,7 @@ func updateListenerConfig() (updated bool) {
|
|||||||
// ip address, other than "127.0.0.1", so trying to listen on default route interface
|
// ip address, other than "127.0.0.1", so trying to listen on default route interface
|
||||||
// address instead.
|
// address instead.
|
||||||
if ip := net.ParseIP(listener.IP); ip != nil && ip.IsLoopback() && ip.String() != "127.0.0.1" {
|
if ip := net.ParseIP(listener.IP); ip != nil && ip.IsLoopback() && ip.String() != "127.0.0.1" {
|
||||||
logMsg(mainLog.Load().Warn(), n, "using loopback interface do not work with systemd-resolved")
|
logMsg(il.Info(), n, "using loopback interface do not work with systemd-resolved")
|
||||||
found := false
|
found := false
|
||||||
if netIface, _ := net.InterfaceByName(defaultIfaceName()); netIface != nil {
|
if netIface, _ := net.InterfaceByName(defaultIfaceName()); netIface != nil {
|
||||||
addrs, _ := netIface.Addrs()
|
addrs, _ := netIface.Addrs()
|
||||||
@@ -1680,7 +1873,7 @@ func updateListenerConfig() (updated bool) {
|
|||||||
if err := tryListen(addr); err == nil {
|
if err := tryListen(addr); err == nil {
|
||||||
found = true
|
found = true
|
||||||
listener.IP = netIP.IP.String()
|
listener.IP = netIP.IP.String()
|
||||||
logMsg(mainLog.Load().Warn(), n, "use %s as listener address", listener.IP)
|
logMsg(il.Info(), n, "use %s as listener address", listener.IP)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1742,12 +1935,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 == cdOrgFlagName {
|
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, cdOrgFlagName+"=") {
|
if strings.HasPrefix(x, "--"+cdOrgFlagName+"=") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
a = append(a, x)
|
a = append(a, x)
|
||||||
@@ -1795,6 +1988,64 @@ func checkStrFlagEmpty(cmd *cobra.Command, flagName string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if fl.Value.String() == "" {
|
if fl.Value.String() == "" {
|
||||||
mainLog.Load().Fatal().Msgf(`flag "--%s"" value must be non-empty`, fl.Name)
|
mainLog.Load().Fatal().Msgf(`flag "--%s" value must be non-empty`, fl.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateCdUpstreamProtocol() {
|
||||||
|
if cdUID == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch cdUpstreamProto {
|
||||||
|
case ctrld.ResolverTypeDOH, ctrld.ResolverTypeDOH3:
|
||||||
|
default:
|
||||||
|
mainLog.Load().Fatal().Msg(`flag "--protocol" must be "doh" or "doh3"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateCdAndNextDNSFlags() {
|
||||||
|
if (cdUID != "" || cdOrg != "") && nextdns != "" {
|
||||||
|
mainLog.Load().Fatal().Msgf("--%s/--%s could not be used with --%s", cdUidFlagName, cdOrgFlagName, nextdnsFlagName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeNextDNSFromArgs removes the --nextdns from command line arguments.
|
||||||
|
func removeNextDNSFromArgs(sc *service.Config) {
|
||||||
|
a := sc.Arguments[:0]
|
||||||
|
skip := false
|
||||||
|
for _, x := range sc.Arguments {
|
||||||
|
if skip {
|
||||||
|
skip = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// For "--nextdns XXX", skip it and mark next arg skipped.
|
||||||
|
if x == "--"+nextdnsFlagName {
|
||||||
|
skip = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// For "--nextdns=XXX", just skip it.
|
||||||
|
if strings.HasPrefix(x, "--"+nextdnsFlagName+"=") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
a = append(a, x)
|
||||||
|
}
|
||||||
|
sc.Arguments = a
|
||||||
|
}
|
||||||
|
|
||||||
|
// doGenerateNextDNSConfig generates a working config with nextdns resolver.
|
||||||
|
func doGenerateNextDNSConfig(uid string) error {
|
||||||
|
if uid == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
mainLog.Load().Notice().Msgf("Generating nextdns config: %s", defaultConfigFile)
|
||||||
|
generateNextDNSConfig(uid)
|
||||||
|
updateListenerConfig(&cfg)
|
||||||
|
return writeConfigFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
func noticeWritingControlDConfig() error {
|
||||||
|
if cdUID != "" {
|
||||||
|
mainLog.Load().Notice().Msgf("Generating controld config: %s", defaultConfigFile)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,14 +6,20 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
dto "github.com/prometheus/client_model/go"
|
||||||
|
|
||||||
|
"github.com/Control-D-Inc/ctrld"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
contentTypeJson = "application/json"
|
contentTypeJson = "application/json"
|
||||||
listClientsPath = "/clients"
|
listClientsPath = "/clients"
|
||||||
startedPath = "/started"
|
startedPath = "/started"
|
||||||
|
reloadPath = "/reload"
|
||||||
)
|
)
|
||||||
|
|
||||||
type controlServer struct {
|
type controlServer struct {
|
||||||
@@ -62,6 +68,25 @@ func (p *prog) registerControlServerHandler() {
|
|||||||
sort.Slice(clients, func(i, j int) bool {
|
sort.Slice(clients, func(i, j int) bool {
|
||||||
return clients[i].IP.Less(clients[j].IP)
|
return clients[i].IP.Less(clients[j].IP)
|
||||||
})
|
})
|
||||||
|
if p.cfg.Service.MetricsQueryStats {
|
||||||
|
for _, client := range clients {
|
||||||
|
client.IncludeQueryCount = true
|
||||||
|
dm := &dto.Metric{}
|
||||||
|
m, err := statsClientQueriesCount.MetricVec.GetMetricWithLabelValues(
|
||||||
|
client.IP.String(),
|
||||||
|
client.Mac,
|
||||||
|
client.Hostname,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
mainLog.Load().Debug().Err(err).Msgf("could not get metrics for client: %v", client)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := m.Write(dm); err == nil {
|
||||||
|
client.QueryCount = int64(dm.Counter.GetValue())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(&clients); err != nil {
|
if err := json.NewEncoder(w).Encode(&clients); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@@ -75,6 +100,52 @@ func (p *prog) registerControlServerHandler() {
|
|||||||
w.WriteHeader(http.StatusRequestTimeout)
|
w.WriteHeader(http.StatusRequestTimeout)
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
p.cs.register(reloadPath, http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
|
||||||
|
listeners := make(map[string]*ctrld.ListenerConfig)
|
||||||
|
p.mu.Lock()
|
||||||
|
for k, v := range p.cfg.Listener {
|
||||||
|
listeners[k] = &ctrld.ListenerConfig{
|
||||||
|
IP: v.IP,
|
||||||
|
Port: v.Port,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
oldSvc := p.cfg.Service
|
||||||
|
p.mu.Unlock()
|
||||||
|
if err := p.sendReloadSignal(); err != nil {
|
||||||
|
mainLog.Load().Err(err).Msg("could not send reload signal")
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-p.reloadDoneCh:
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
http.Error(w, "timeout waiting for ctrld reload", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
|
// Checking for cases that we could not do a reload.
|
||||||
|
|
||||||
|
// 1. Listener config ip or port changes.
|
||||||
|
for k, v := range p.cfg.Listener {
|
||||||
|
l := listeners[k]
|
||||||
|
if l == nil || l.IP != v.IP || l.Port != v.Port {
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Service config changes.
|
||||||
|
if !reflect.DeepEqual(oldSvc, p.cfg.Service) {
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, reload is done.
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func jsonResponse(next http.Handler) http.Handler {
|
func jsonResponse(next http.Handler) http.Handler {
|
||||||
|
|||||||
+404
-60
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
@@ -17,6 +18,7 @@ import (
|
|||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
"tailscale.com/net/interfaces"
|
"tailscale.com/net/interfaces"
|
||||||
"tailscale.com/net/netaddr"
|
"tailscale.com/net/netaddr"
|
||||||
|
"tailscale.com/net/tsaddr"
|
||||||
|
|
||||||
"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"
|
||||||
@@ -25,6 +27,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
staleTTL = 60 * time.Second
|
staleTTL = 60 * time.Second
|
||||||
|
localTTL = 3600 * time.Second
|
||||||
// EDNS0_OPTION_MAC is dnsmasq EDNS0 code for adding mac option.
|
// EDNS0_OPTION_MAC is dnsmasq EDNS0 code for adding mac option.
|
||||||
// https://thekelleys.org.uk/gitweb/?p=dnsmasq.git;a=blob;f=src/dns-protocol.h;h=76ac66a8c28317e9c121a74ab5fd0e20f6237dc8;hb=HEAD#l81
|
// https://thekelleys.org.uk/gitweb/?p=dnsmasq.git;a=blob;f=src/dns-protocol.h;h=76ac66a8c28317e9c121a74ab5fd0e20f6237dc8;hb=HEAD#l81
|
||||||
// This is also dns.EDNS0LOCALSTART, but define our own constant here for clarification.
|
// This is also dns.EDNS0LOCALSTART, but define our own constant here for clarification.
|
||||||
@@ -37,6 +40,38 @@ var osUpstreamConfig = &ctrld.UpstreamConfig{
|
|||||||
Timeout: 2000,
|
Timeout: 2000,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var privateUpstreamConfig = &ctrld.UpstreamConfig{
|
||||||
|
Name: "Private resolver",
|
||||||
|
Type: ctrld.ResolverTypePrivate,
|
||||||
|
Timeout: 2000,
|
||||||
|
}
|
||||||
|
|
||||||
|
// proxyRequest contains data for proxying a DNS query to upstream.
|
||||||
|
type proxyRequest struct {
|
||||||
|
msg *dns.Msg
|
||||||
|
ci *ctrld.ClientInfo
|
||||||
|
failoverRcodes []int
|
||||||
|
ufr *upstreamForResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// proxyResponse contains data for proxying a DNS response from upstream.
|
||||||
|
type proxyResponse struct {
|
||||||
|
answer *dns.Msg
|
||||||
|
cached bool
|
||||||
|
clientInfo bool
|
||||||
|
upstream string
|
||||||
|
}
|
||||||
|
|
||||||
|
// upstreamForResult represents the result of processing rules for a request.
|
||||||
|
type upstreamForResult struct {
|
||||||
|
upstreams []string
|
||||||
|
matchedPolicy string
|
||||||
|
matchedNetwork string
|
||||||
|
matchedRule string
|
||||||
|
matched bool
|
||||||
|
srcAddr string
|
||||||
|
}
|
||||||
|
|
||||||
func (p *prog) serveDNS(listenerNum string) error {
|
func (p *prog) serveDNS(listenerNum string) error {
|
||||||
listenerConfig := p.cfg.Listener[listenerNum]
|
listenerConfig := p.cfg.Listener[listenerNum]
|
||||||
// make sure ip is allocated
|
// make sure ip is allocated
|
||||||
@@ -44,36 +79,81 @@ func (p *prog) serveDNS(listenerNum string) error {
|
|||||||
mainLog.Load().Error().Err(allocErr).Str("ip", listenerConfig.IP).Msg("serveUDP: failed to allocate listen ip")
|
mainLog.Load().Error().Err(allocErr).Str("ip", listenerConfig.IP).Msg("serveUDP: failed to allocate listen ip")
|
||||||
return allocErr
|
return allocErr
|
||||||
}
|
}
|
||||||
var failoverRcodes []int
|
|
||||||
if listenerConfig.Policy != nil {
|
|
||||||
failoverRcodes = listenerConfig.Policy.FailoverRcodeNumbers
|
|
||||||
}
|
|
||||||
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()
|
||||||
|
if len(m.Question) == 0 {
|
||||||
|
answer := new(dns.Msg)
|
||||||
|
answer.SetRcode(m, dns.RcodeFormatError)
|
||||||
|
_ = w.WriteMsg(answer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reqId := requestID()
|
||||||
|
ctx := context.WithValue(context.Background(), ctrld.ReqIdCtxKey{}, reqId)
|
||||||
|
if !listenerConfig.AllowWanClients && isWanClient(w.RemoteAddr()) {
|
||||||
|
ctrld.Log(ctx, mainLog.Load().Debug(), "query refused, listener does not allow WAN clients: %s", w.RemoteAddr().String())
|
||||||
|
answer := new(dns.Msg)
|
||||||
|
answer.SetRcode(m, dns.RcodeRefused)
|
||||||
|
_ = w.WriteMsg(answer)
|
||||||
|
return
|
||||||
|
}
|
||||||
go p.detectLoop(m)
|
go p.detectLoop(m)
|
||||||
q := m.Question[0]
|
q := m.Question[0]
|
||||||
domain := canonicalName(q.Name)
|
domain := canonicalName(q.Name)
|
||||||
reqId := requestID()
|
|
||||||
remoteIP, _, _ := net.SplitHostPort(w.RemoteAddr().String())
|
remoteIP, _, _ := net.SplitHostPort(w.RemoteAddr().String())
|
||||||
ci := p.getClientInfo(remoteIP, m)
|
ci := p.getClientInfo(remoteIP, m)
|
||||||
|
ci.ClientIDPref = p.cfg.Service.ClientIDPref
|
||||||
|
stripClientSubnet(m)
|
||||||
remoteAddr := spoofRemoteAddr(w.RemoteAddr(), ci)
|
remoteAddr := spoofRemoteAddr(w.RemoteAddr(), ci)
|
||||||
fmtSrcToDest := fmtRemoteToLocal(listenerNum, remoteAddr.String(), w.LocalAddr().String())
|
fmtSrcToDest := fmtRemoteToLocal(listenerNum, ci.Hostname, remoteAddr.String())
|
||||||
t := time.Now()
|
t := time.Now()
|
||||||
ctx := context.WithValue(context.Background(), ctrld.ReqIdCtxKey{}, reqId)
|
ctrld.Log(ctx, mainLog.Load().Info(), "QUERY: %s: %s %s", fmtSrcToDest, dns.TypeToString[q.Qtype], domain)
|
||||||
ctrld.Log(ctx, mainLog.Load().Debug(), "%s received query: %s %s", fmtSrcToDest, dns.TypeToString[q.Qtype], domain)
|
ur := p.upstreamFor(ctx, listenerNum, listenerConfig, remoteAddr, ci.Mac, domain)
|
||||||
upstreams, matched := p.upstreamFor(ctx, listenerNum, listenerConfig, remoteAddr, domain)
|
|
||||||
|
labelValues := make([]string, 0, len(statsQueriesCountLabels))
|
||||||
|
labelValues = append(labelValues, net.JoinHostPort(listenerConfig.IP, strconv.Itoa(listenerConfig.Port)))
|
||||||
|
labelValues = append(labelValues, ci.IP)
|
||||||
|
labelValues = append(labelValues, ci.Mac)
|
||||||
|
labelValues = append(labelValues, ci.Hostname)
|
||||||
|
|
||||||
var answer *dns.Msg
|
var answer *dns.Msg
|
||||||
if !matched && listenerConfig.Restricted {
|
if !ur.matched && listenerConfig.Restricted {
|
||||||
|
ctrld.Log(ctx, mainLog.Load().Info(), "query refused, %s does not match any network policy", remoteAddr.String())
|
||||||
answer = new(dns.Msg)
|
answer = new(dns.Msg)
|
||||||
answer.SetRcode(m, dns.RcodeRefused)
|
answer.SetRcode(m, dns.RcodeRefused)
|
||||||
|
labelValues = append(labelValues, "") // no upstream
|
||||||
} else {
|
} else {
|
||||||
answer = p.proxy(ctx, upstreams, failoverRcodes, m, ci)
|
var failoverRcode []int
|
||||||
|
if listenerConfig.Policy != nil {
|
||||||
|
failoverRcode = listenerConfig.Policy.FailoverRcodeNumbers
|
||||||
|
}
|
||||||
|
pr := p.proxy(ctx, &proxyRequest{
|
||||||
|
msg: m,
|
||||||
|
ci: ci,
|
||||||
|
failoverRcodes: failoverRcode,
|
||||||
|
ufr: ur,
|
||||||
|
})
|
||||||
|
answer = pr.answer
|
||||||
rtt := time.Since(t)
|
rtt := time.Since(t)
|
||||||
ctrld.Log(ctx, mainLog.Load().Debug(), "received response of %d bytes in %s", answer.Len(), rtt)
|
ctrld.Log(ctx, mainLog.Load().Debug(), "received response of %d bytes in %s", answer.Len(), rtt)
|
||||||
|
upstream := pr.upstream
|
||||||
|
switch {
|
||||||
|
case pr.cached:
|
||||||
|
upstream = "cache"
|
||||||
|
case pr.clientInfo:
|
||||||
|
upstream = "client_info_table"
|
||||||
|
}
|
||||||
|
labelValues = append(labelValues, upstream)
|
||||||
}
|
}
|
||||||
|
labelValues = append(labelValues, dns.TypeToString[q.Qtype])
|
||||||
|
labelValues = append(labelValues, dns.RcodeToString[answer.Rcode])
|
||||||
|
go func() {
|
||||||
|
p.WithLabelValuesInc(statsQueriesCount, labelValues...)
|
||||||
|
p.WithLabelValuesInc(statsClientQueriesCount, []string{ci.IP, ci.Mac, ci.Hostname}...)
|
||||||
|
}()
|
||||||
if err := w.WriteMsg(answer); err != nil {
|
if err := w.WriteMsg(answer); err != nil {
|
||||||
ctrld.Log(ctx, mainLog.Load().Error().Err(err), "serveUDP: failed to send DNS response to client")
|
ctrld.Log(ctx, mainLog.Load().Error().Err(err), "serveDNS: failed to send DNS response to client")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -99,7 +179,7 @@ func (p *prog) serveDNS(listenerNum string) error {
|
|||||||
// addresses of the machine. So ctrld could receive queries from LAN clients.
|
// addresses of the machine. So ctrld could receive queries from LAN clients.
|
||||||
if needRFC1918Listeners(listenerConfig) {
|
if needRFC1918Listeners(listenerConfig) {
|
||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
for _, addr := range rfc1918Addresses() {
|
for _, addr := range ctrld.Rfc1918Addresses() {
|
||||||
func() {
|
func() {
|
||||||
listenAddr := net.JoinHostPort(addr, strconv.Itoa(listenerConfig.Port))
|
listenAddr := net.JoinHostPort(addr, strconv.Itoa(listenerConfig.Port))
|
||||||
s, errCh := runDNSServer(listenAddr, proto, handler)
|
s, errCh := runDNSServer(listenAddr, proto, handler)
|
||||||
@@ -146,27 +226,24 @@ func (p *prog) serveDNS(listenerNum string) error {
|
|||||||
// Though domain policy has higher priority than network policy, it is still
|
// Though domain policy has higher priority than network policy, it is still
|
||||||
// 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, srcMac, domain string) (res *upstreamForResult) {
|
||||||
upstreams := []string{upstreamPrefix + defaultUpstreamNum}
|
upstreams := []string{upstreamPrefix + defaultUpstreamNum}
|
||||||
matchedPolicy := "no policy"
|
matchedPolicy := "no policy"
|
||||||
matchedNetwork := "no network"
|
matchedNetwork := "no network"
|
||||||
matchedRule := "no rule"
|
matchedRule := "no rule"
|
||||||
matched := false
|
matched := false
|
||||||
|
res = &upstreamForResult{srcAddr: addr.String()}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if !matched && lc.Restricted {
|
res.upstreams = upstreams
|
||||||
ctrld.Log(ctx, mainLog.Load().Info(), "query refused, %s does not match any network policy", addr.String())
|
res.matched = matched
|
||||||
return
|
res.matchedPolicy = matchedPolicy
|
||||||
}
|
res.matchedNetwork = matchedNetwork
|
||||||
if matched {
|
res.matchedRule = matchedRule
|
||||||
ctrld.Log(ctx, mainLog.Load().Info(), "%s, %s, %s -> %v", matchedPolicy, matchedNetwork, matchedRule, upstreams)
|
|
||||||
} else {
|
|
||||||
ctrld.Log(ctx, mainLog.Load().Info(), "no explicit policy matched, using default routing -> %v", upstreams)
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if lc.Policy == nil {
|
if lc.Policy == nil {
|
||||||
return upstreams, false
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
do := func(policyUpstreams []string) {
|
do := func(policyUpstreams []string) {
|
||||||
@@ -202,6 +279,19 @@ networkRules:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macRules:
|
||||||
|
for _, rule := range lc.Policy.Macs {
|
||||||
|
for source, targets := range rule {
|
||||||
|
if source != "" && strings.EqualFold(source, srcMac) {
|
||||||
|
matchedPolicy = lc.Policy.Name
|
||||||
|
matchedNetwork = source
|
||||||
|
networkTargets = targets
|
||||||
|
matched = true
|
||||||
|
break macRules
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, rule := range lc.Policy.Rules {
|
for _, rule := range lc.Policy.Rules {
|
||||||
// There's only one entry per rule, config validation ensures this.
|
// There's only one entry per rule, config validation ensures this.
|
||||||
for source, targets := range rule {
|
for source, targets := range rule {
|
||||||
@@ -213,7 +303,7 @@ networkRules:
|
|||||||
matchedRule = source
|
matchedRule = source
|
||||||
do(targets)
|
do(targets)
|
||||||
matched = true
|
matched = true
|
||||||
return upstreams, matched
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,31 +312,147 @@ networkRules:
|
|||||||
do(networkTargets)
|
do(networkTargets)
|
||||||
}
|
}
|
||||||
|
|
||||||
return upstreams, matched
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []int, msg *dns.Msg, ci *ctrld.ClientInfo) *dns.Msg {
|
func (p *prog) proxyPrivatePtrLookup(ctx context.Context, msg *dns.Msg) *dns.Msg {
|
||||||
|
cDomainName := msg.Question[0].Name
|
||||||
|
locked := p.ptrLoopGuard.TryLock(cDomainName)
|
||||||
|
defer p.ptrLoopGuard.Unlock(cDomainName)
|
||||||
|
if !locked {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ip := ipFromARPA(cDomainName)
|
||||||
|
if name := p.ciTable.LookupHostname(ip.String(), ""); name != "" {
|
||||||
|
answer := new(dns.Msg)
|
||||||
|
answer.SetReply(msg)
|
||||||
|
answer.Compress = true
|
||||||
|
answer.Answer = []dns.RR{&dns.PTR{
|
||||||
|
Hdr: dns.RR_Header{
|
||||||
|
Name: msg.Question[0].Name,
|
||||||
|
Rrtype: dns.TypePTR,
|
||||||
|
Class: dns.ClassINET,
|
||||||
|
},
|
||||||
|
Ptr: dns.Fqdn(name),
|
||||||
|
}}
|
||||||
|
ctrld.Log(ctx, mainLog.Load().Info(), "private PTR lookup, using client info table")
|
||||||
|
ctrld.Log(ctx, mainLog.Load().Debug(), "client info: %v", ctrld.ClientInfo{
|
||||||
|
Mac: p.ciTable.LookupMac(ip.String()),
|
||||||
|
IP: ip.String(),
|
||||||
|
Hostname: name,
|
||||||
|
})
|
||||||
|
return answer
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *prog) proxyLanHostnameQuery(ctx context.Context, msg *dns.Msg) *dns.Msg {
|
||||||
|
q := msg.Question[0]
|
||||||
|
hostname := strings.TrimSuffix(q.Name, ".")
|
||||||
|
locked := p.lanLoopGuard.TryLock(hostname)
|
||||||
|
defer p.lanLoopGuard.Unlock(hostname)
|
||||||
|
if !locked {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if ip := p.ciTable.LookupIPByHostname(hostname, q.Qtype == dns.TypeAAAA); ip != nil {
|
||||||
|
answer := new(dns.Msg)
|
||||||
|
answer.SetReply(msg)
|
||||||
|
answer.Compress = true
|
||||||
|
switch {
|
||||||
|
case ip.Is4():
|
||||||
|
answer.Answer = []dns.RR{&dns.A{
|
||||||
|
Hdr: dns.RR_Header{
|
||||||
|
Name: msg.Question[0].Name,
|
||||||
|
Rrtype: dns.TypeA,
|
||||||
|
Class: dns.ClassINET,
|
||||||
|
Ttl: uint32(localTTL.Seconds()),
|
||||||
|
},
|
||||||
|
A: ip.AsSlice(),
|
||||||
|
}}
|
||||||
|
case ip.Is6():
|
||||||
|
answer.Answer = []dns.RR{&dns.AAAA{
|
||||||
|
Hdr: dns.RR_Header{
|
||||||
|
Name: msg.Question[0].Name,
|
||||||
|
Rrtype: dns.TypeAAAA,
|
||||||
|
Class: dns.ClassINET,
|
||||||
|
Ttl: uint32(localTTL.Seconds()),
|
||||||
|
},
|
||||||
|
AAAA: ip.AsSlice(),
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
ctrld.Log(ctx, mainLog.Load().Info(), "lan hostname lookup, using client info table")
|
||||||
|
ctrld.Log(ctx, mainLog.Load().Debug(), "client info: %v", ctrld.ClientInfo{
|
||||||
|
Mac: p.ciTable.LookupMac(ip.String()),
|
||||||
|
IP: ip.String(),
|
||||||
|
Hostname: hostname,
|
||||||
|
})
|
||||||
|
return answer
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *prog) proxy(ctx context.Context, req *proxyRequest) *proxyResponse {
|
||||||
var staleAnswer *dns.Msg
|
var staleAnswer *dns.Msg
|
||||||
|
upstreams := req.ufr.upstreams
|
||||||
serveStaleCache := p.cache != nil && p.cfg.Service.CacheServeStale
|
serveStaleCache := p.cache != nil && p.cfg.Service.CacheServeStale
|
||||||
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{upstreamOS}
|
upstreams = []string{upstreamOS}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
res := &proxyResponse{}
|
||||||
|
|
||||||
|
// LAN/PTR lookup flow:
|
||||||
|
//
|
||||||
|
// 1. If there's matching rule, follow it.
|
||||||
|
// 2. Try from client info table.
|
||||||
|
// 3. Try private resolver.
|
||||||
|
// 4. Try remote upstream.
|
||||||
|
isLanOrPtrQuery := false
|
||||||
|
if req.ufr.matched {
|
||||||
|
ctrld.Log(ctx, mainLog.Load().Debug(), "%s, %s, %s -> %v", req.ufr.matchedPolicy, req.ufr.matchedNetwork, req.ufr.matchedRule, upstreams)
|
||||||
|
} else {
|
||||||
|
switch {
|
||||||
|
case isPrivatePtrLookup(req.msg):
|
||||||
|
isLanOrPtrQuery = true
|
||||||
|
if answer := p.proxyPrivatePtrLookup(ctx, req.msg); answer != nil {
|
||||||
|
res.answer = answer
|
||||||
|
res.clientInfo = true
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
upstreams, upstreamConfigs = p.upstreamsAndUpstreamConfigForLanAndPtr(upstreams, upstreamConfigs)
|
||||||
|
ctrld.Log(ctx, mainLog.Load().Debug(), "private PTR lookup, using upstreams: %v", upstreams)
|
||||||
|
case isLanHostnameQuery(req.msg):
|
||||||
|
isLanOrPtrQuery = true
|
||||||
|
if answer := p.proxyLanHostnameQuery(ctx, req.msg); answer != nil {
|
||||||
|
res.answer = answer
|
||||||
|
res.clientInfo = true
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
upstreams, upstreamConfigs = p.upstreamsAndUpstreamConfigForLanAndPtr(upstreams, upstreamConfigs)
|
||||||
|
ctrld.Log(ctx, mainLog.Load().Debug(), "lan hostname lookup, using upstreams: %v", upstreams)
|
||||||
|
default:
|
||||||
|
ctrld.Log(ctx, mainLog.Load().Debug(), "no explicit policy matched, using default routing -> %v", upstreams)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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 && req.msg.Question[0].Qtype != dns.TypePTR {
|
||||||
for _, upstream := range upstreams {
|
for _, upstream := range upstreams {
|
||||||
cachedValue := p.cache.Get(dnscache.NewKey(msg, upstream))
|
cachedValue := p.cache.Get(dnscache.NewKey(req.msg, upstream))
|
||||||
if cachedValue == nil {
|
if cachedValue == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
answer := cachedValue.Msg.Copy()
|
answer := cachedValue.Msg.Copy()
|
||||||
answer.SetRcode(msg, answer.Rcode)
|
answer.SetRcode(req.msg, answer.Rcode)
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
if cachedValue.Expire.After(now) {
|
if cachedValue.Expire.After(now) {
|
||||||
ctrld.Log(ctx, mainLog.Load().Debug(), "hit cached response")
|
ctrld.Log(ctx, mainLog.Load().Debug(), "hit cached response")
|
||||||
setCachedAnswerTTL(answer, now, cachedValue.Expire)
|
setCachedAnswerTTL(answer, now, cachedValue.Expire)
|
||||||
return answer
|
res.answer = answer
|
||||||
|
res.cached = true
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
staleAnswer = answer
|
staleAnswer = answer
|
||||||
}
|
}
|
||||||
@@ -268,9 +474,9 @@ func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []i
|
|||||||
return dnsResolver.Resolve(resolveCtx, msg)
|
return dnsResolver.Resolve(resolveCtx, msg)
|
||||||
}
|
}
|
||||||
resolve := func(n int, upstreamConfig *ctrld.UpstreamConfig, msg *dns.Msg) *dns.Msg {
|
resolve := func(n int, upstreamConfig *ctrld.UpstreamConfig, msg *dns.Msg) *dns.Msg {
|
||||||
if upstreamConfig.UpstreamSendClientInfo() && ci != nil {
|
if upstreamConfig.UpstreamSendClientInfo() && req.ci != nil {
|
||||||
ctrld.Log(ctx, mainLog.Load().Debug(), "including client info with the request")
|
ctrld.Log(ctx, mainLog.Load().Debug(), "including client info with the request")
|
||||||
ctx = context.WithValue(ctx, ctrld.ClientInfoCtxKey{}, ci)
|
ctx = context.WithValue(ctx, ctrld.ClientInfoCtxKey{}, req.ci)
|
||||||
}
|
}
|
||||||
answer, err := resolve1(n, upstreamConfig, msg)
|
answer, err := resolve1(n, upstreamConfig, msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -281,6 +487,11 @@ func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []i
|
|||||||
go p.um.checkUpstream(upstreams[n], upstreamConfig)
|
go p.um.checkUpstream(upstreams[n], upstreamConfig)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// For timeout error (i.e: context deadline exceed), force re-bootstrapping.
|
||||||
|
var e net.Error
|
||||||
|
if errors.As(err, &e) && e.Timeout() {
|
||||||
|
upstreamConfig.ReBootstrap()
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return answer
|
return answer
|
||||||
@@ -297,17 +508,25 @@ func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []i
|
|||||||
ctrld.Log(ctx, mainLog.Load().Warn(), "%s is down", upstreams[n])
|
ctrld.Log(ctx, mainLog.Load().Warn(), "%s is down", upstreams[n])
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
answer := resolve(n, upstreamConfig, msg)
|
answer := resolve(n, upstreamConfig, req.msg)
|
||||||
if answer == nil {
|
if answer == nil {
|
||||||
if serveStaleCache && staleAnswer != nil {
|
if serveStaleCache && staleAnswer != nil {
|
||||||
ctrld.Log(ctx, mainLog.Load().Debug(), "serving stale cached response")
|
ctrld.Log(ctx, mainLog.Load().Debug(), "serving stale cached response")
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
setCachedAnswerTTL(staleAnswer, now, now.Add(staleTTL))
|
setCachedAnswerTTL(staleAnswer, now, now.Add(staleTTL))
|
||||||
return staleAnswer
|
res.answer = staleAnswer
|
||||||
|
res.cached = true
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if answer.Rcode != dns.RcodeSuccess && len(upstreamConfigs) > 1 && containRcode(failoverRcodes, answer.Rcode) {
|
// We are doing LAN/PTR lookup using private resolver, so always process next one.
|
||||||
|
// Except for the last, we want to send response instead of saying all upstream failed.
|
||||||
|
if answer.Rcode != dns.RcodeSuccess && isLanOrPtrQuery && n != len(upstreamConfigs)-1 {
|
||||||
|
ctrld.Log(ctx, mainLog.Load().Debug(), "no response from %s, process to next upstream", upstreams[n])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if answer.Rcode != dns.RcodeSuccess && len(upstreamConfigs) > 1 && containRcode(req.failoverRcodes, answer.Rcode) {
|
||||||
ctrld.Log(ctx, mainLog.Load().Debug(), "failover rcode matched, process to next upstream")
|
ctrld.Log(ctx, mainLog.Load().Debug(), "failover rcode matched, process to next upstream")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -315,7 +534,7 @@ func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []i
|
|||||||
// set compression, as it is not set by default when unpacking
|
// set compression, as it is not set by default when unpacking
|
||||||
answer.Compress = true
|
answer.Compress = true
|
||||||
|
|
||||||
if p.cache != nil {
|
if p.cache != nil && req.msg.Question[0].Qtype != dns.TypePTR {
|
||||||
ttl := ttlFromMsg(answer)
|
ttl := ttlFromMsg(answer)
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
expired := now.Add(time.Duration(ttl) * time.Second)
|
expired := now.Add(time.Duration(ttl) * time.Second)
|
||||||
@@ -323,15 +542,33 @@ func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []i
|
|||||||
expired = now.Add(time.Duration(cachedTTL) * time.Second)
|
expired = now.Add(time.Duration(cachedTTL) * time.Second)
|
||||||
}
|
}
|
||||||
setCachedAnswerTTL(answer, now, expired)
|
setCachedAnswerTTL(answer, now, expired)
|
||||||
p.cache.Add(dnscache.NewKey(msg, upstreams[n]), dnscache.NewValue(answer, expired))
|
p.cache.Add(dnscache.NewKey(req.msg, upstreams[n]), dnscache.NewValue(answer, expired))
|
||||||
ctrld.Log(ctx, mainLog.Load().Debug(), "add cached response")
|
ctrld.Log(ctx, mainLog.Load().Debug(), "add cached response")
|
||||||
}
|
}
|
||||||
return answer
|
hostname := ""
|
||||||
|
if req.ci != nil {
|
||||||
|
hostname = req.ci.Hostname
|
||||||
|
}
|
||||||
|
ctrld.Log(ctx, mainLog.Load().Info(), "REPLY: %s -> %s (%s): %s", upstreams[n], req.ufr.srcAddr, hostname, dns.RcodeToString[answer.Rcode])
|
||||||
|
res.answer = answer
|
||||||
|
res.upstream = upstreamConfig.Endpoint
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
ctrld.Log(ctx, mainLog.Load().Error(), "all %v endpoints failed", upstreams)
|
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(req.msg, dns.RcodeServerFailure)
|
||||||
return answer
|
res.answer = answer
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *prog) upstreamsAndUpstreamConfigForLanAndPtr(upstreams []string, upstreamConfigs []*ctrld.UpstreamConfig) ([]string, []*ctrld.UpstreamConfig) {
|
||||||
|
if len(p.localUpstreams) > 0 {
|
||||||
|
tmp := make([]string, 0, len(p.localUpstreams)+len(upstreams))
|
||||||
|
tmp = append(tmp, p.localUpstreams...)
|
||||||
|
tmp = append(tmp, upstreams...)
|
||||||
|
return tmp, p.upstreamConfigsFromUpstreamNumbers(tmp)
|
||||||
|
}
|
||||||
|
return append([]string{upstreamOS}, upstreams...), append([]*ctrld.UpstreamConfig{privateUpstreamConfig}, upstreamConfigs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *prog) upstreamConfigsFromUpstreamNumbers(upstreams []string) []*ctrld.UpstreamConfig {
|
func (p *prog) upstreamConfigsFromUpstreamNumbers(upstreams []string) []*ctrld.UpstreamConfig {
|
||||||
@@ -377,8 +614,8 @@ func wildcardMatches(wildcard, domain string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func fmtRemoteToLocal(listenerNum, remote, local string) string {
|
func fmtRemoteToLocal(listenerNum, hostname, remote string) string {
|
||||||
return fmt.Sprintf("%s -> listener.%s: %s:", remote, listenerNum, local)
|
return fmt.Sprintf("%s (%s) -> listener.%s", remote, hostname, listenerNum)
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestID() string {
|
func requestID() string {
|
||||||
@@ -454,6 +691,23 @@ func ipAndMacFromMsg(msg *dns.Msg) (string, string) {
|
|||||||
return ip, mac
|
return ip, mac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stripClientSubnet removes EDNS0_SUBNET from DNS message if the IP is RFC1918 or loopback address,
|
||||||
|
// passing them to upstream is pointless, these cannot be used by anything on the WAN.
|
||||||
|
func stripClientSubnet(msg *dns.Msg) {
|
||||||
|
if opt := msg.IsEdns0(); opt != nil {
|
||||||
|
opts := make([]dns.EDNS0, 0, len(opt.Option))
|
||||||
|
for _, s := range opt.Option {
|
||||||
|
if e, ok := s.(*dns.EDNS0_SUBNET); ok && (e.Address.IsPrivate() || e.Address.IsLoopback()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
opts = append(opts, s)
|
||||||
|
}
|
||||||
|
if len(opts) != len(opt.Option) {
|
||||||
|
opt.Option = opts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func spoofRemoteAddr(addr net.Addr, ci *ctrld.ClientInfo) net.Addr {
|
func spoofRemoteAddr(addr net.Addr, ci *ctrld.ClientInfo) net.Addr {
|
||||||
if ci != nil && ci.IP != "" {
|
if ci != nil && ci.IP != "" {
|
||||||
switch addr := addr.(type) {
|
switch addr := addr.(type) {
|
||||||
@@ -531,23 +785,44 @@ func (p *prog) getClientInfo(remoteIP string, msg *dns.Msg) *ctrld.ClientInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If MAC is still empty here, that mean the requests are made from virtual interface,
|
// If MAC is still empty here, that mean the requests are made from virtual interface,
|
||||||
// like VPN/Wireguard clients, so we use whatever MAC address associated with remoteIP
|
// like VPN/Wireguard clients, so we use ci.IP as hostname to distinguish those clients.
|
||||||
// (most likely 127.0.0.1), and ci.IP as hostname, so we can distinguish those clients.
|
|
||||||
if ci.Mac == "" {
|
if ci.Mac == "" {
|
||||||
ci.Mac = p.ciTable.LookupMac(remoteIP)
|
|
||||||
if hostname := p.ciTable.LookupHostname(ci.IP, ""); hostname != "" {
|
if hostname := p.ciTable.LookupHostname(ci.IP, ""); hostname != "" {
|
||||||
ci.Hostname = hostname
|
ci.Hostname = hostname
|
||||||
} else {
|
} else {
|
||||||
ci.Hostname = ci.IP
|
// Only use IP as hostname for IPv4 clients.
|
||||||
p.ciTable.StoreVPNClient(ci)
|
// For Android devices, when it joins the network, it uses ctrld to resolve
|
||||||
|
// its private DNS once and never reaches ctrld again. For each time, it uses
|
||||||
|
// a different IPv6 address, which causes hundreds/thousands different client
|
||||||
|
// IDs created for the same device, which is pointless.
|
||||||
|
//
|
||||||
|
// TODO(cuonglm): investigate whether this can be a false positive for other clients?
|
||||||
|
if !ctrldnet.IsIPv6(ci.IP) {
|
||||||
|
ci.Hostname = ci.IP
|
||||||
|
p.ciTable.StoreVPNClient(ci)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ci.Hostname = p.ciTable.LookupHostname(ci.IP, ci.Mac)
|
ci.Hostname = p.ciTable.LookupHostname(ci.IP, ci.Mac)
|
||||||
}
|
}
|
||||||
ci.Self = queryFromSelf(ci.IP)
|
ci.Self = queryFromSelf(ci.IP)
|
||||||
|
p.spoofLoopbackIpInClientInfo(ci)
|
||||||
return ci
|
return ci
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// spoofLoopbackIpInClientInfo replaces loopback IPs in client info.
|
||||||
|
//
|
||||||
|
// - Preference IPv4.
|
||||||
|
// - Preference RFC1918.
|
||||||
|
func (p *prog) spoofLoopbackIpInClientInfo(ci *ctrld.ClientInfo) {
|
||||||
|
if ip := net.ParseIP(ci.IP); ip == nil || !ip.IsLoopback() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ip := p.ciTable.LookupRFC1918IPv4(ci.Mac); ip != "" {
|
||||||
|
ci.IP = ip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// queryFromSelf reports whether the input IP is from device running ctrld.
|
// queryFromSelf reports whether the input IP is from device running ctrld.
|
||||||
func queryFromSelf(ip string) bool {
|
func queryFromSelf(ip string) bool {
|
||||||
netIP := netip.MustParseAddr(ip)
|
netIP := netip.MustParseAddr(ip)
|
||||||
@@ -578,17 +853,86 @@ func needRFC1918Listeners(lc *ctrld.ListenerConfig) bool {
|
|||||||
return lc.IP == "127.0.0.1" && lc.Port == 53
|
return lc.IP == "127.0.0.1" && lc.Port == 53
|
||||||
}
|
}
|
||||||
|
|
||||||
func rfc1918Addresses() []string {
|
// ipFromARPA parses a FQDN arpa domain and return the IP address if valid.
|
||||||
var res []string
|
func ipFromARPA(arpa string) net.IP {
|
||||||
interfaces.ForeachInterface(func(i interfaces.Interface, prefixes []netip.Prefix) {
|
if arpa, ok := strings.CutSuffix(arpa, ".in-addr.arpa."); ok {
|
||||||
addrs, _ := i.Addrs()
|
if ptrIP := net.ParseIP(arpa); ptrIP != nil {
|
||||||
for _, addr := range addrs {
|
return net.IP{ptrIP[15], ptrIP[14], ptrIP[13], ptrIP[12]}
|
||||||
ipNet, ok := addr.(*net.IPNet)
|
|
||||||
if !ok || !ipNet.IP.IsPrivate() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
res = append(res, ipNet.IP.String())
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
return res
|
if arpa, ok := strings.CutSuffix(arpa, ".ip6.arpa."); ok {
|
||||||
|
l := net.IPv6len * 2
|
||||||
|
base := 16
|
||||||
|
ip := make(net.IP, net.IPv6len)
|
||||||
|
for i := 0; i < l && arpa != ""; i++ {
|
||||||
|
idx := strings.LastIndexByte(arpa, '.')
|
||||||
|
off := idx + 1
|
||||||
|
if idx == -1 {
|
||||||
|
idx = 0
|
||||||
|
off = 0
|
||||||
|
} else if idx == len(arpa)-1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
n, err := strconv.ParseUint(arpa[off:], base, 8)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
b := byte(n)
|
||||||
|
ii := i / 2
|
||||||
|
if i&1 == 1 {
|
||||||
|
b |= ip[ii] << 4
|
||||||
|
}
|
||||||
|
ip[ii] = b
|
||||||
|
arpa = arpa[:idx]
|
||||||
|
}
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isPrivatePtrLookup reports whether DNS message is an PTR query for LAN/CGNAT network.
|
||||||
|
func isPrivatePtrLookup(m *dns.Msg) bool {
|
||||||
|
if m == nil || len(m.Question) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
q := m.Question[0]
|
||||||
|
if ip := ipFromARPA(q.Name); ip != nil {
|
||||||
|
if addr, ok := netip.AddrFromSlice(ip); ok {
|
||||||
|
return addr.IsPrivate() ||
|
||||||
|
addr.IsLoopback() ||
|
||||||
|
addr.IsLinkLocalUnicast() ||
|
||||||
|
tsaddr.CGNATRange().Contains(addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// isLanHostnameQuery reports whether DNS message is an A/AAAA query with LAN hostname.
|
||||||
|
func isLanHostnameQuery(m *dns.Msg) bool {
|
||||||
|
if m == nil || len(m.Question) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
q := m.Question[0]
|
||||||
|
switch q.Qtype {
|
||||||
|
case dns.TypeA, dns.TypeAAAA:
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
name := strings.TrimSuffix(q.Name, ".")
|
||||||
|
return !strings.Contains(name, ".") ||
|
||||||
|
strings.HasSuffix(name, ".domain") ||
|
||||||
|
strings.HasSuffix(name, ".lan")
|
||||||
|
}
|
||||||
|
|
||||||
|
// isWanClient reports whether the input is a WAN address.
|
||||||
|
func isWanClient(na net.Addr) bool {
|
||||||
|
var ip netip.Addr
|
||||||
|
if ap, err := netip.ParseAddrPort(na.String()); err == nil {
|
||||||
|
ip = ap.Addr()
|
||||||
|
}
|
||||||
|
return !ip.IsLoopback() &&
|
||||||
|
!ip.IsPrivate() &&
|
||||||
|
!ip.IsLinkLocalUnicast() &&
|
||||||
|
!ip.IsLinkLocalMulticast() &&
|
||||||
|
!tsaddr.CGNATRange().Contains(ip)
|
||||||
}
|
}
|
||||||
|
|||||||
+211
-14
@@ -67,8 +67,11 @@ func Test_canonicalName(t *testing.T) {
|
|||||||
|
|
||||||
func Test_prog_upstreamFor(t *testing.T) {
|
func Test_prog_upstreamFor(t *testing.T) {
|
||||||
cfg := testhelper.SampleConfig(t)
|
cfg := testhelper.SampleConfig(t)
|
||||||
prog := &prog{cfg: cfg}
|
p := &prog{cfg: cfg}
|
||||||
for _, nc := range prog.cfg.Network {
|
p.um = newUpstreamMonitor(p.cfg)
|
||||||
|
p.lanLoopGuard = newLoopGuard()
|
||||||
|
p.ptrLoopGuard = newLoopGuard()
|
||||||
|
for _, nc := range p.cfg.Network {
|
||||||
for _, cidr := range nc.Cidrs {
|
for _, cidr := range nc.Cidrs {
|
||||||
_, ipNet, err := net.ParseCIDR(cidr)
|
_, ipNet, err := net.ParseCIDR(cidr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -81,6 +84,7 @@ func Test_prog_upstreamFor(t *testing.T) {
|
|||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
ip string
|
ip string
|
||||||
|
mac string
|
||||||
defaultUpstreamNum string
|
defaultUpstreamNum string
|
||||||
lc *ctrld.ListenerConfig
|
lc *ctrld.ListenerConfig
|
||||||
domain string
|
domain string
|
||||||
@@ -88,11 +92,14 @@ func Test_prog_upstreamFor(t *testing.T) {
|
|||||||
matched bool
|
matched bool
|
||||||
testLogMsg string
|
testLogMsg string
|
||||||
}{
|
}{
|
||||||
{"Policy map matches", "192.168.0.1:0", "0", prog.cfg.Listener["0"], "abc.xyz", []string{"upstream.1", "upstream.0"}, true, ""},
|
{"Policy map matches", "192.168.0.1:0", "", "0", p.cfg.Listener["0"], "abc.xyz", []string{"upstream.1", "upstream.0"}, true, ""},
|
||||||
{"Policy split matches", "192.168.0.1:0", "0", prog.cfg.Listener["0"], "abc.ru", []string{"upstream.1"}, true, ""},
|
{"Policy split matches", "192.168.0.1:0", "", "0", p.cfg.Listener["0"], "abc.ru", []string{"upstream.1"}, true, ""},
|
||||||
{"Policy map for other network matches", "192.168.1.2:0", "0", prog.cfg.Listener["0"], "abc.xyz", []string{"upstream.0"}, true, ""},
|
{"Policy map for other network matches", "192.168.1.2:0", "", "0", p.cfg.Listener["0"], "abc.xyz", []string{"upstream.0"}, true, ""},
|
||||||
{"No policy map for listener", "192.168.1.2:0", "1", prog.cfg.Listener["1"], "abc.ru", []string{"upstream.1"}, false, ""},
|
{"No policy map for listener", "192.168.1.2:0", "", "1", p.cfg.Listener["1"], "abc.ru", []string{"upstream.1"}, false, ""},
|
||||||
{"unenforced loging", "192.168.1.2:0", "0", prog.cfg.Listener["0"], "abc.ru", []string{"upstream.1"}, true, "My Policy, network.1 (unenforced), *.ru -> [upstream.1]"},
|
{"unenforced loging", "192.168.1.2:0", "", "0", p.cfg.Listener["0"], "abc.ru", []string{"upstream.1"}, true, "My Policy, network.1 (unenforced), *.ru -> [upstream.1]"},
|
||||||
|
{"Policy Macs matches upper", "192.168.0.1:0", "14:45:A0:67:83:0A", "0", p.cfg.Listener["0"], "abc.xyz", []string{"upstream.2"}, true, "14:45:a0:67:83:0a"},
|
||||||
|
{"Policy Macs matches lower", "192.168.0.1:0", "14:54:4a:8e:08:2d", "0", p.cfg.Listener["0"], "abc.xyz", []string{"upstream.2"}, true, "14:54:4a:8e:08:2d"},
|
||||||
|
{"Policy Macs matches case-insensitive", "192.168.0.1:0", "14:54:4A:8E:08:2D", "0", p.cfg.Listener["0"], "abc.xyz", []string{"upstream.2"}, true, "14:54:4a:8e:08:2d"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
@@ -111,9 +118,13 @@ func Test_prog_upstreamFor(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, addr)
|
require.NotNil(t, addr)
|
||||||
ctx := context.WithValue(context.Background(), ctrld.ReqIdCtxKey{}, requestID())
|
ctx := context.WithValue(context.Background(), ctrld.ReqIdCtxKey{}, requestID())
|
||||||
upstreams, matched := prog.upstreamFor(ctx, tc.defaultUpstreamNum, tc.lc, addr, tc.domain)
|
ufr := p.upstreamFor(ctx, tc.defaultUpstreamNum, tc.lc, addr, tc.mac, tc.domain)
|
||||||
assert.Equal(t, tc.matched, matched)
|
p.proxy(ctx, &proxyRequest{
|
||||||
assert.Equal(t, tc.upstreams, upstreams)
|
msg: newDnsMsgWithHostname("foo", dns.TypeA),
|
||||||
|
ufr: ufr,
|
||||||
|
})
|
||||||
|
assert.Equal(t, tc.matched, ufr.matched)
|
||||||
|
assert.Equal(t, tc.upstreams, ufr.upstreams)
|
||||||
if tc.testLogMsg != "" {
|
if tc.testLogMsg != "" {
|
||||||
assert.Contains(t, logOutput.String(), tc.testLogMsg)
|
assert.Contains(t, logOutput.String(), tc.testLogMsg)
|
||||||
}
|
}
|
||||||
@@ -149,11 +160,35 @@ func TestCache(t *testing.T) {
|
|||||||
answer2.SetRcode(msg, dns.RcodeRefused)
|
answer2.SetRcode(msg, dns.RcodeRefused)
|
||||||
prog.cache.Add(dnscache.NewKey(msg, "upstream.0"), dnscache.NewValue(answer2, time.Now().Add(time.Minute)))
|
prog.cache.Add(dnscache.NewKey(msg, "upstream.0"), dnscache.NewValue(answer2, time.Now().Add(time.Minute)))
|
||||||
|
|
||||||
got1 := prog.proxy(context.Background(), []string{"upstream.1"}, nil, msg, nil)
|
req1 := &proxyRequest{
|
||||||
got2 := prog.proxy(context.Background(), []string{"upstream.0"}, nil, msg, nil)
|
msg: msg,
|
||||||
|
ci: nil,
|
||||||
|
failoverRcodes: nil,
|
||||||
|
ufr: &upstreamForResult{
|
||||||
|
upstreams: []string{"upstream.1"},
|
||||||
|
matchedPolicy: "",
|
||||||
|
matchedNetwork: "",
|
||||||
|
matchedRule: "",
|
||||||
|
matched: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
req2 := &proxyRequest{
|
||||||
|
msg: msg,
|
||||||
|
ci: nil,
|
||||||
|
failoverRcodes: nil,
|
||||||
|
ufr: &upstreamForResult{
|
||||||
|
upstreams: []string{"upstream.0"},
|
||||||
|
matchedPolicy: "",
|
||||||
|
matchedNetwork: "",
|
||||||
|
matchedRule: "",
|
||||||
|
matched: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
got1 := prog.proxy(context.Background(), req1)
|
||||||
|
got2 := prog.proxy(context.Background(), req2)
|
||||||
assert.NotSame(t, got1, got2)
|
assert.NotSame(t, got1, got2)
|
||||||
assert.Equal(t, answer1.Rcode, got1.Rcode)
|
assert.Equal(t, answer1.Rcode, got1.answer.Rcode)
|
||||||
assert.Equal(t, answer2.Rcode, got2.Rcode)
|
assert.Equal(t, answer2.Rcode, got2.answer.Rcode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_ipAndMacFromMsg(t *testing.T) {
|
func Test_ipAndMacFromMsg(t *testing.T) {
|
||||||
@@ -234,3 +269,165 @@ func Test_remoteAddrFromMsg(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_ipFromARPA(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
IP string
|
||||||
|
ARPA string
|
||||||
|
}{
|
||||||
|
{"1.2.3.4", "4.3.2.1.in-addr.arpa."},
|
||||||
|
{"245.110.36.114", "114.36.110.245.in-addr.arpa."},
|
||||||
|
{"::ffff:12.34.56.78", "78.56.34.12.in-addr.arpa."},
|
||||||
|
{"::1", "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa."},
|
||||||
|
{"1::", "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.0.0.ip6.arpa."},
|
||||||
|
{"1234:567::89a:bcde", "e.d.c.b.a.9.8.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.7.6.5.0.4.3.2.1.ip6.arpa."},
|
||||||
|
{"1234:567:fefe:bcbc:adad:9e4a:89a:bcde", "e.d.c.b.a.9.8.0.a.4.e.9.d.a.d.a.c.b.c.b.e.f.e.f.7.6.5.0.4.3.2.1.ip6.arpa."},
|
||||||
|
{"", "asd.in-addr.arpa."},
|
||||||
|
{"", "asd.ip6.arpa."},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.IP, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if got := ipFromARPA(tc.ARPA); !got.Equal(net.ParseIP(tc.IP)) {
|
||||||
|
t.Errorf("unexpected ip, want: %s, got: %s", tc.IP, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDnsMsgWithClientIP(ip string) *dns.Msg {
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetQuestion("example.com.", dns.TypeA)
|
||||||
|
o := &dns.OPT{Hdr: dns.RR_Header{Name: ".", Rrtype: dns.TypeOPT}}
|
||||||
|
o.Option = append(o.Option, &dns.EDNS0_SUBNET{Address: net.ParseIP(ip)})
|
||||||
|
m.Extra = append(m.Extra, o)
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_stripClientSubnet(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
msg *dns.Msg
|
||||||
|
wantSubnet bool
|
||||||
|
}{
|
||||||
|
{"no edns0", new(dns.Msg), false},
|
||||||
|
{"loopback IP v4", newDnsMsgWithClientIP("127.0.0.1"), false},
|
||||||
|
{"loopback IP v6", newDnsMsgWithClientIP("::1"), false},
|
||||||
|
{"private IP v4", newDnsMsgWithClientIP("192.168.1.123"), false},
|
||||||
|
{"private IP v6", newDnsMsgWithClientIP("fd12:3456:789a:1::1"), false},
|
||||||
|
{"public IP", newDnsMsgWithClientIP("1.1.1.1"), true},
|
||||||
|
{"invalid IP", newDnsMsgWithClientIP(""), true},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
stripClientSubnet(tc.msg)
|
||||||
|
hasSubnet := false
|
||||||
|
if opt := tc.msg.IsEdns0(); opt != nil {
|
||||||
|
for _, s := range opt.Option {
|
||||||
|
if _, ok := s.(*dns.EDNS0_SUBNET); ok {
|
||||||
|
hasSubnet = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tc.wantSubnet != hasSubnet {
|
||||||
|
t.Errorf("unexpected result, want: %v, got: %v", tc.wantSubnet, hasSubnet)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDnsMsgWithHostname(hostname string, typ uint16) *dns.Msg {
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetQuestion(hostname, typ)
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_isLanHostnameQuery(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
msg *dns.Msg
|
||||||
|
isLanHostnameQuery bool
|
||||||
|
}{
|
||||||
|
{"A", newDnsMsgWithHostname("foo", dns.TypeA), true},
|
||||||
|
{"AAAA", newDnsMsgWithHostname("foo", dns.TypeAAAA), true},
|
||||||
|
{"A not LAN", newDnsMsgWithHostname("example.com", dns.TypeA), false},
|
||||||
|
{"AAAA not LAN", newDnsMsgWithHostname("example.com", dns.TypeAAAA), false},
|
||||||
|
{"Not A or AAAA", newDnsMsgWithHostname("foo", dns.TypeTXT), false},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if got := isLanHostnameQuery(tc.msg); tc.isLanHostnameQuery != got {
|
||||||
|
t.Errorf("unexpected result, want: %v, got: %v", tc.isLanHostnameQuery, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDnsMsgPtr(ip string, t *testing.T) *dns.Msg {
|
||||||
|
t.Helper()
|
||||||
|
m := new(dns.Msg)
|
||||||
|
ptr, err := dns.ReverseAddr(ip)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
m.SetQuestion(ptr, dns.TypePTR)
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_isPrivatePtrLookup(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
msg *dns.Msg
|
||||||
|
isPrivatePtrLookup bool
|
||||||
|
}{
|
||||||
|
// RFC 1918 allocates 10.0.0.0/8, 172.16.0.0/12, and 192.168.0.0/16 as
|
||||||
|
{"10.0.0.0/8", newDnsMsgPtr("10.0.0.123", t), true},
|
||||||
|
{"172.16.0.0/12", newDnsMsgPtr("172.16.0.123", t), true},
|
||||||
|
{"192.168.0.0/16", newDnsMsgPtr("192.168.1.123", t), true},
|
||||||
|
{"CGNAT", newDnsMsgPtr("100.66.27.28", t), true},
|
||||||
|
{"Loopback", newDnsMsgPtr("127.0.0.1", t), true},
|
||||||
|
{"Link Local Unicast", newDnsMsgPtr("fe80::69f6:e16e:8bdb:433f", t), true},
|
||||||
|
{"Public IP", newDnsMsgPtr("8.8.8.8", t), false},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if got := isPrivatePtrLookup(tc.msg); tc.isPrivatePtrLookup != got {
|
||||||
|
t.Errorf("unexpected result, want: %v, got: %v", tc.isPrivatePtrLookup, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_isWanClient(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
addr net.Addr
|
||||||
|
isWanClient bool
|
||||||
|
}{
|
||||||
|
// RFC 1918 allocates 10.0.0.0/8, 172.16.0.0/12, and 192.168.0.0/16 as
|
||||||
|
{"10.0.0.0/8", &net.UDPAddr{IP: net.ParseIP("10.0.0.123")}, false},
|
||||||
|
{"172.16.0.0/12", &net.UDPAddr{IP: net.ParseIP("172.16.0.123")}, false},
|
||||||
|
{"192.168.0.0/16", &net.UDPAddr{IP: net.ParseIP("192.168.1.123")}, false},
|
||||||
|
{"CGNAT", &net.UDPAddr{IP: net.ParseIP("100.66.27.28")}, false},
|
||||||
|
{"Loopback", &net.UDPAddr{IP: net.ParseIP("127.0.0.1")}, false},
|
||||||
|
{"Link Local Unicast", &net.UDPAddr{IP: net.ParseIP("fe80::69f6:e16e:8bdb:433f")}, false},
|
||||||
|
{"Public", &net.UDPAddr{IP: net.ParseIP("8.8.8.8")}, true},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if got := isWanClient(tc.addr); tc.isWanClient != got {
|
||||||
|
t.Errorf("unexpected result, want: %v, got: %v", tc.isWanClient, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+5
-4
@@ -11,8 +11,9 @@ type AppCallback struct {
|
|||||||
|
|
||||||
// AppConfig allows overwriting ctrld cli flags from mobile platforms.
|
// AppConfig allows overwriting ctrld cli flags from mobile platforms.
|
||||||
type AppConfig struct {
|
type AppConfig struct {
|
||||||
CdUID string
|
CdUID string
|
||||||
HomeDir string
|
HomeDir string
|
||||||
Verbose int
|
UpstreamProto string
|
||||||
LogPath string
|
Verbose int
|
||||||
|
LogPath string
|
||||||
}
|
}
|
||||||
|
|||||||
+43
-2
@@ -3,6 +3,7 @@ package cli
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
@@ -15,6 +16,36 @@ const (
|
|||||||
loopTestQtype = dns.TypeTXT
|
loopTestQtype = dns.TypeTXT
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// newLoopGuard returns new loopGuard.
|
||||||
|
func newLoopGuard() *loopGuard {
|
||||||
|
return &loopGuard{inflight: make(map[string]struct{})}
|
||||||
|
}
|
||||||
|
|
||||||
|
// loopGuard guards against DNS loop, ensuring only one query
|
||||||
|
// for a given domain is processed at a time.
|
||||||
|
type loopGuard struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
inflight map[string]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TryLock marks the domain as being processed.
|
||||||
|
func (lg *loopGuard) TryLock(domain string) bool {
|
||||||
|
lg.mu.Lock()
|
||||||
|
defer lg.mu.Unlock()
|
||||||
|
if _, inflight := lg.inflight[domain]; !inflight {
|
||||||
|
lg.inflight[domain] = struct{}{}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlock marks the domain as being done.
|
||||||
|
func (lg *loopGuard) Unlock(domain string) {
|
||||||
|
lg.mu.Lock()
|
||||||
|
defer lg.mu.Unlock()
|
||||||
|
delete(lg.inflight, domain)
|
||||||
|
}
|
||||||
|
|
||||||
// isLoop reports whether the given upstream config is detected as having DNS loop.
|
// isLoop reports whether the given upstream config is detected as having DNS loop.
|
||||||
func (p *prog) isLoop(uc *ctrld.UpstreamConfig) bool {
|
func (p *prog) isLoop(uc *ctrld.UpstreamConfig) bool {
|
||||||
p.loopMu.Lock()
|
p.loopMu.Lock()
|
||||||
@@ -56,7 +87,15 @@ func (p *prog) checkDnsLoop() {
|
|||||||
mainLog.Load().Debug().Msg("start checking DNS loop")
|
mainLog.Load().Debug().Msg("start checking DNS loop")
|
||||||
upstream := make(map[string]*ctrld.UpstreamConfig)
|
upstream := make(map[string]*ctrld.UpstreamConfig)
|
||||||
p.loopMu.Lock()
|
p.loopMu.Lock()
|
||||||
for _, uc := range p.cfg.Upstream {
|
for n, uc := range p.cfg.Upstream {
|
||||||
|
if p.um.isDown("upstream." + n) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Do not send test query to external upstream.
|
||||||
|
if !canBeLocalUpstream(uc.Domain) {
|
||||||
|
mainLog.Load().Debug().Msgf("skipping external: upstream.%s", n)
|
||||||
|
continue
|
||||||
|
}
|
||||||
uid := uc.UID()
|
uid := uc.UID()
|
||||||
p.loop[uid] = false
|
p.loop[uid] = false
|
||||||
upstream[uid] = uc
|
upstream[uid] = uc
|
||||||
@@ -79,13 +118,15 @@ func (p *prog) checkDnsLoop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// checkDnsLoopTicker performs p.checkDnsLoop every minute.
|
// checkDnsLoopTicker performs p.checkDnsLoop every minute.
|
||||||
func (p *prog) checkDnsLoopTicker() {
|
func (p *prog) checkDnsLoopTicker(ctx context.Context) {
|
||||||
timer := time.NewTicker(time.Minute)
|
timer := time.NewTicker(time.Minute)
|
||||||
defer timer.Stop()
|
defer timer.Stop()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-p.stopCh:
|
case <-p.stopCh:
|
||||||
return
|
return
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
case <-timer.C:
|
case <-timer.C:
|
||||||
p.checkDnsLoop()
|
p.checkDnsLoop()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_loopGuard(t *testing.T) {
|
||||||
|
lg := newLoopGuard()
|
||||||
|
key := "foo"
|
||||||
|
|
||||||
|
var i atomic.Int64
|
||||||
|
var started atomic.Int64
|
||||||
|
n := 1000
|
||||||
|
do := func() {
|
||||||
|
locked := lg.TryLock(key)
|
||||||
|
defer lg.Unlock(key)
|
||||||
|
started.Add(1)
|
||||||
|
for started.Load() < 2 {
|
||||||
|
// Wait until at least 2 goroutines started, otherwise, on system with heavy load,
|
||||||
|
// or having only 1 CPU, all goroutines can be scheduled to run consequently.
|
||||||
|
}
|
||||||
|
if locked {
|
||||||
|
i.Add(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(n)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
do()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
if i.Load() == int64(n) {
|
||||||
|
t.Fatalf("i must not be increased %d times", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-3
@@ -32,6 +32,8 @@ var (
|
|||||||
cdDev bool
|
cdDev bool
|
||||||
iface string
|
iface string
|
||||||
ifaceStartStop string
|
ifaceStartStop string
|
||||||
|
nextdns string
|
||||||
|
cdUpstreamProto string
|
||||||
|
|
||||||
mainLog atomic.Pointer[zerolog.Logger]
|
mainLog atomic.Pointer[zerolog.Logger]
|
||||||
consoleWriter zerolog.ConsoleWriter
|
consoleWriter zerolog.ConsoleWriter
|
||||||
@@ -39,8 +41,9 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
cdUidFlagName = "cd"
|
cdUidFlagName = "cd"
|
||||||
cdOrgFlagName = "cd-org"
|
cdOrgFlagName = "cd-org"
|
||||||
|
nextdnsFlagName = "nextdns"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -93,6 +96,7 @@ func initConsoleLogging() {
|
|||||||
|
|
||||||
// initLogging initializes global logging setup.
|
// initLogging initializes global logging setup.
|
||||||
func initLogging() {
|
func initLogging() {
|
||||||
|
zerolog.TimeFieldFormat = time.RFC3339 + ".000"
|
||||||
initLoggingWithBackup(true)
|
initLoggingWithBackup(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +135,7 @@ func initLoggingWithBackup(doBackup bool) {
|
|||||||
}
|
}
|
||||||
writers = append(writers, consoleWriter)
|
writers = append(writers, consoleWriter)
|
||||||
multi := zerolog.MultiLevelWriter(writers...)
|
multi := zerolog.MultiLevelWriter(writers...)
|
||||||
l := mainLog.Load().Output(multi).With().Timestamp().Logger()
|
l := mainLog.Load().Output(multi).With().Logger()
|
||||||
mainLog.Store(&l)
|
mainLog.Store(&l)
|
||||||
// TODO: find a better way.
|
// TODO: find a better way.
|
||||||
ctrld.ProxyLogger.Store(&l)
|
ctrld.ProxyLogger.Store(&l)
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/collectors"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
"github.com/prometheus/prom2json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// metricsServer represents a server to expose Prometheus metrics via HTTP.
|
||||||
|
type metricsServer struct {
|
||||||
|
server *http.Server
|
||||||
|
mux *http.ServeMux
|
||||||
|
reg *prometheus.Registry
|
||||||
|
addr string
|
||||||
|
started bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// newMetricsServer returns new metrics server.
|
||||||
|
func newMetricsServer(addr string, reg *prometheus.Registry) (*metricsServer, error) {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
ms := &metricsServer{
|
||||||
|
server: &http.Server{Handler: mux},
|
||||||
|
mux: mux,
|
||||||
|
reg: reg,
|
||||||
|
}
|
||||||
|
ms.addr = addr
|
||||||
|
ms.registerMetricsServerHandler()
|
||||||
|
return ms, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// register adds handlers for given pattern.
|
||||||
|
func (ms *metricsServer) register(pattern string, handler http.Handler) {
|
||||||
|
ms.mux.Handle(pattern, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerMetricsServerHandler adds handlers for metrics server.
|
||||||
|
func (ms *metricsServer) registerMetricsServerHandler() {
|
||||||
|
ms.register("/metrics", promhttp.HandlerFor(
|
||||||
|
ms.reg,
|
||||||
|
promhttp.HandlerOpts{
|
||||||
|
EnableOpenMetrics: true,
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
ms.register("/metrics/json", jsonResponse(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
g := prometheus.ToTransactionalGatherer(ms.reg)
|
||||||
|
mfs, done, err := g.Gather()
|
||||||
|
defer done()
|
||||||
|
if err != nil {
|
||||||
|
msg := "could not gather metrics"
|
||||||
|
mainLog.Load().Warn().Err(err).Msg(msg)
|
||||||
|
http.Error(w, msg, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result := make([]*prom2json.Family, 0, len(mfs))
|
||||||
|
for _, mf := range mfs {
|
||||||
|
result = append(result, prom2json.NewFamily(mf))
|
||||||
|
}
|
||||||
|
if err := json.NewEncoder(w).Encode(result); err != nil {
|
||||||
|
msg := "could not marshal metrics result"
|
||||||
|
mainLog.Load().Warn().Err(err).Msg(msg)
|
||||||
|
http.Error(w, msg, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
// start runs the metricsServer.
|
||||||
|
func (ms *metricsServer) start() error {
|
||||||
|
listener, err := net.Listen("tcp", ms.addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
go ms.server.Serve(listener)
|
||||||
|
ms.started = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop shutdowns the metricsServer within 2 seconds timeout.
|
||||||
|
func (ms *metricsServer) stop() error {
|
||||||
|
if !ms.started {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*1)
|
||||||
|
defer cancel()
|
||||||
|
return ms.server.Shutdown(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// runMetricsServer initializes metrics stats and runs the metrics server if enabled.
|
||||||
|
func (p *prog) runMetricsServer(ctx context.Context, reloadCh chan struct{}) {
|
||||||
|
if !p.metricsEnabled() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset all stats.
|
||||||
|
statsVersion.Reset()
|
||||||
|
statsQueriesCount.Reset()
|
||||||
|
statsClientQueriesCount.Reset()
|
||||||
|
|
||||||
|
reg := prometheus.NewRegistry()
|
||||||
|
// Register queries count stats if enabled.
|
||||||
|
if cfg.Service.MetricsQueryStats {
|
||||||
|
reg.MustRegister(statsQueriesCount)
|
||||||
|
reg.MustRegister(statsClientQueriesCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := p.cfg.Service.MetricsListener
|
||||||
|
ms, err := newMetricsServer(addr, reg)
|
||||||
|
if err != nil {
|
||||||
|
mainLog.Load().Warn().Err(err).Msg("could not create new metrics server")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Only start listener address if defined.
|
||||||
|
if addr != "" {
|
||||||
|
// Go runtime stats.
|
||||||
|
reg.MustRegister(collectors.NewBuildInfoCollector())
|
||||||
|
reg.MustRegister(collectors.NewGoCollector(
|
||||||
|
collectors.WithGoCollectorRuntimeMetrics(collectors.MetricsAll),
|
||||||
|
))
|
||||||
|
// ctrld stats.
|
||||||
|
reg.MustRegister(statsVersion)
|
||||||
|
statsVersion.WithLabelValues(commit, runtime.Version(), curVersion()).Inc()
|
||||||
|
reg.MustRegister(statsTimeStart)
|
||||||
|
statsTimeStart.Set(float64(time.Now().Unix()))
|
||||||
|
mainLog.Load().Debug().Msgf("starting metrics server on: %s", addr)
|
||||||
|
if err := ms.start(); err != nil {
|
||||||
|
mainLog.Load().Warn().Err(err).Msg("could not start metrics server")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-p.stopCh:
|
||||||
|
case <-ctx.Done():
|
||||||
|
case <-reloadCh:
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ms.stop(); err != nil {
|
||||||
|
mainLog.Load().Warn().Err(err).Msg("could not stop metrics server")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"github.com/vishvananda/netlink"
|
"github.com/vishvananda/netlink"
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *prog) watchLinkState() {
|
func (p *prog) watchLinkState(ctx context.Context) {
|
||||||
ch := make(chan netlink.LinkUpdate)
|
ch := make(chan netlink.LinkUpdate)
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
defer close(done)
|
defer close(done)
|
||||||
@@ -13,14 +15,19 @@ func (p *prog) watchLinkState() {
|
|||||||
mainLog.Load().Warn().Err(err).Msg("could not subscribe link")
|
mainLog.Load().Warn().Err(err).Msg("could not subscribe link")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for lu := range ch {
|
for {
|
||||||
if lu.Change == 0xFFFFFFFF {
|
select {
|
||||||
continue
|
case <-ctx.Done():
|
||||||
}
|
return
|
||||||
if lu.Change&unix.IFF_UP != 0 {
|
case lu := <-ch:
|
||||||
mainLog.Load().Debug().Msgf("link state changed, re-bootstrapping")
|
if lu.Change == 0xFFFFFFFF {
|
||||||
for _, uc := range p.cfg.Upstream {
|
continue
|
||||||
uc.ReBootstrap()
|
}
|
||||||
|
if lu.Change&unix.IFF_UP != 0 {
|
||||||
|
mainLog.Load().Debug().Msgf("link state changed, re-bootstrapping")
|
||||||
|
for _, uc := range p.cfg.Upstream {
|
||||||
|
uc.ReBootstrap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,6 @@
|
|||||||
|
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
func (p *prog) watchLinkState() {}
|
import "context"
|
||||||
|
|
||||||
|
func (p *prog) watchLinkState(ctx context.Context) {}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/Control-D-Inc/ctrld"
|
||||||
|
)
|
||||||
|
|
||||||
|
const nextdnsURL = "https://dns.nextdns.io"
|
||||||
|
|
||||||
|
func generateNextDNSConfig(uid string) {
|
||||||
|
if uid == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mainLog.Load().Info().Msg("generating ctrld config for NextDNS resolver")
|
||||||
|
cfg = ctrld.Config{
|
||||||
|
Listener: map[string]*ctrld.ListenerConfig{
|
||||||
|
"0": {
|
||||||
|
IP: "0.0.0.0",
|
||||||
|
Port: 53,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Upstream: map[string]*ctrld.UpstreamConfig{
|
||||||
|
"0": {
|
||||||
|
Type: ctrld.ResolverTypeDOH3,
|
||||||
|
Endpoint: fmt.Sprintf("%s/%s", nextdnsURL, uid),
|
||||||
|
Timeout: 5000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
+67
-1
@@ -9,10 +9,12 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
"github.com/insomniacslk/dhcp/dhcpv4/nclient4"
|
"github.com/insomniacslk/dhcp/dhcpv4/nclient4"
|
||||||
"github.com/insomniacslk/dhcp/dhcpv6"
|
"github.com/insomniacslk/dhcp/dhcpv6"
|
||||||
"github.com/insomniacslk/dhcp/dhcpv6/client6"
|
"github.com/insomniacslk/dhcp/dhcpv6/client6"
|
||||||
@@ -23,7 +25,10 @@ import (
|
|||||||
"github.com/Control-D-Inc/ctrld/internal/resolvconffile"
|
"github.com/Control-D-Inc/ctrld/internal/resolvconffile"
|
||||||
)
|
)
|
||||||
|
|
||||||
const resolvConfBackupFailedMsg = "open /etc/resolv.pre-ctrld-backup.conf: read-only file system"
|
const (
|
||||||
|
resolvConfPath = "/etc/resolv.conf"
|
||||||
|
resolvConfBackupFailedMsg = "open /etc/resolv.pre-ctrld-backup.conf: read-only file system"
|
||||||
|
)
|
||||||
|
|
||||||
// allocate loopback ip
|
// allocate loopback ip
|
||||||
// sudo ip a add 127.0.0.2/24 dev lo
|
// sudo ip a add 127.0.0.2/24 dev lo
|
||||||
@@ -64,6 +69,11 @@ func setDNS(iface *net.Interface, nameservers []string) error {
|
|||||||
Nameservers: ns,
|
Nameservers: ns,
|
||||||
SearchDomains: []dnsname.FQDN{},
|
SearchDomains: []dnsname.FQDN{},
|
||||||
}
|
}
|
||||||
|
defer func() {
|
||||||
|
if r.Mode() == "direct" {
|
||||||
|
go watchResolveConf(osConfig)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
trySystemdResolve := false
|
trySystemdResolve := false
|
||||||
for i := 0; i < maxSetDNSAttempts; i++ {
|
for i := 0; i < maxSetDNSAttempts; i++ {
|
||||||
@@ -299,3 +309,59 @@ func sliceIndex[S ~[]E, E comparable](s S, v E) int {
|
|||||||
}
|
}
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// watchResolveConf watches any changes to /etc/resolv.conf file,
|
||||||
|
// and reverting to the original config set by ctrld.
|
||||||
|
func watchResolveConf(oc dns.OSConfig) {
|
||||||
|
mainLog.Load().Debug().Msg("start watching /etc/resolv.conf file")
|
||||||
|
watcher, err := fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
mainLog.Load().Warn().Err(err).Msg("could not create watcher for /etc/resolv.conf")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// We watch /etc instead of /etc/resolv.conf directly,
|
||||||
|
// see: https://github.com/fsnotify/fsnotify#watching-a-file-doesnt-work-well
|
||||||
|
watchDir := filepath.Dir(resolvConfPath)
|
||||||
|
if err := watcher.Add(watchDir); err != nil {
|
||||||
|
mainLog.Load().Warn().Err(err).Msg("could not add /etc/resolv.conf to watcher list")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := dns.NewOSConfigurator(func(format string, args ...any) {}, "lo") // interface name does not matter.
|
||||||
|
if err != nil {
|
||||||
|
mainLog.Load().Error().Err(err).Msg("failed to create DNS OS configurator")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event, ok := <-watcher.Events:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if event.Name != resolvConfPath { // skip if not /etc/resolv.conf changes.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) {
|
||||||
|
mainLog.Load().Debug().Msg("/etc/resolv.conf changes detected, reverting to ctrld setting")
|
||||||
|
if err := watcher.Remove(watchDir); err != nil {
|
||||||
|
mainLog.Load().Error().Err(err).Msg("failed to pause watcher")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := r.SetDNS(oc); err != nil {
|
||||||
|
mainLog.Load().Error().Err(err).Msg("failed to revert /etc/resolv.conf changes")
|
||||||
|
}
|
||||||
|
if err := watcher.Add(watchDir); err != nil {
|
||||||
|
mainLog.Load().Error().Err(err).Msg("failed to continue running watcher")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case err, ok := <-watcher.Errors:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mainLog.Load().Err(err).Msg("could not get event for /etc/resolv.conf")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+265
-78
@@ -2,6 +2,7 @@ package cli
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
@@ -16,7 +17,9 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/kardianos/service"
|
"github.com/kardianos/service"
|
||||||
|
"github.com/spf13/viper"
|
||||||
"tailscale.com/net/interfaces"
|
"tailscale.com/net/interfaces"
|
||||||
|
"tailscale.com/net/tsaddr"
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld"
|
"github.com/Control-D-Inc/ctrld"
|
||||||
"github.com/Control-D-Inc/ctrld/internal/clientinfo"
|
"github.com/Control-D-Inc/ctrld/internal/clientinfo"
|
||||||
@@ -30,6 +33,7 @@ const (
|
|||||||
ctrldControlUnixSock = "ctrld_control.sock"
|
ctrldControlUnixSock = "ctrld_control.sock"
|
||||||
upstreamPrefix = "upstream."
|
upstreamPrefix = "upstream."
|
||||||
upstreamOS = upstreamPrefix + "os"
|
upstreamOS = upstreamPrefix + "os"
|
||||||
|
upstreamPrivate = upstreamPrefix + "private"
|
||||||
)
|
)
|
||||||
|
|
||||||
var logf = func(format string, args ...any) {
|
var logf = func(format string, args ...any) {
|
||||||
@@ -45,19 +49,25 @@ var svcConfig = &service.Config{
|
|||||||
var useSystemdResolved = false
|
var useSystemdResolved = false
|
||||||
|
|
||||||
type prog struct {
|
type prog struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
waitCh chan struct{}
|
waitCh chan struct{}
|
||||||
stopCh chan struct{}
|
stopCh chan struct{}
|
||||||
logConn net.Conn
|
reloadCh chan struct{} // For Windows.
|
||||||
cs *controlServer
|
reloadDoneCh chan struct{}
|
||||||
|
logConn net.Conn
|
||||||
|
cs *controlServer
|
||||||
|
|
||||||
cfg *ctrld.Config
|
cfg *ctrld.Config
|
||||||
appCallback *AppCallback
|
localUpstreams []string
|
||||||
cache dnscache.Cacher
|
ptrNameservers []string
|
||||||
sema semaphore
|
appCallback *AppCallback
|
||||||
ciTable *clientinfo.Table
|
cache dnscache.Cacher
|
||||||
um *upstreamMonitor
|
sema semaphore
|
||||||
router router.Router
|
ciTable *clientinfo.Table
|
||||||
|
um *upstreamMonitor
|
||||||
|
router router.Router
|
||||||
|
ptrLoopGuard *loopGuard
|
||||||
|
lanLoopGuard *loopGuard
|
||||||
|
|
||||||
loopMu sync.Mutex
|
loopMu sync.Mutex
|
||||||
loop map[string]bool
|
loop map[string]bool
|
||||||
@@ -69,11 +79,106 @@ type prog struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *prog) Start(s service.Service) error {
|
func (p *prog) Start(s service.Service) error {
|
||||||
p.cfg = &cfg
|
go p.runWait()
|
||||||
go p.run()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// runWait runs ctrld components, waiting for signal to reload.
|
||||||
|
func (p *prog) runWait() {
|
||||||
|
p.mu.Lock()
|
||||||
|
p.cfg = &cfg
|
||||||
|
p.mu.Unlock()
|
||||||
|
reloadSigCh := make(chan os.Signal, 1)
|
||||||
|
notifyReloadSigCh(reloadSigCh)
|
||||||
|
|
||||||
|
reload := false
|
||||||
|
logger := mainLog.Load()
|
||||||
|
for {
|
||||||
|
reloadCh := make(chan struct{})
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
p.run(reload, reloadCh)
|
||||||
|
reload = true
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case sig := <-reloadSigCh:
|
||||||
|
logger.Notice().Msgf("got signal: %s, reloading...", sig.String())
|
||||||
|
case <-p.reloadCh:
|
||||||
|
logger.Notice().Msg("reloading...")
|
||||||
|
case <-p.stopCh:
|
||||||
|
close(reloadCh)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
waitOldRunDone := func() {
|
||||||
|
close(reloadCh)
|
||||||
|
<-done
|
||||||
|
}
|
||||||
|
newCfg := &ctrld.Config{}
|
||||||
|
v := viper.NewWithOptions(viper.KeyDelimiter("::"))
|
||||||
|
ctrld.InitConfig(v, "ctrld")
|
||||||
|
if configPath != "" {
|
||||||
|
v.SetConfigFile(configPath)
|
||||||
|
}
|
||||||
|
if err := v.ReadInConfig(); err != nil {
|
||||||
|
logger.Err(err).Msg("could not read new config")
|
||||||
|
waitOldRunDone()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := v.Unmarshal(&newCfg); err != nil {
|
||||||
|
logger.Err(err).Msg("could not unmarshal new config")
|
||||||
|
waitOldRunDone()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if cdUID != "" {
|
||||||
|
if err := processCDFlags(newCfg); err != nil {
|
||||||
|
logger.Err(err).Msg("could not fetch ControlD config")
|
||||||
|
waitOldRunDone()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
waitOldRunDone()
|
||||||
|
|
||||||
|
p.mu.Lock()
|
||||||
|
curListener := p.cfg.Listener
|
||||||
|
p.mu.Unlock()
|
||||||
|
|
||||||
|
for n, lc := range newCfg.Listener {
|
||||||
|
curLc := curListener[n]
|
||||||
|
if curLc == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if lc.IP == "" {
|
||||||
|
lc.IP = curLc.IP
|
||||||
|
}
|
||||||
|
if lc.Port == 0 {
|
||||||
|
lc.Port = curLc.Port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := validateConfig(newCfg); err != nil {
|
||||||
|
logger.Err(err).Msg("invalid config")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// This needs to be done here, otherwise, the DNS handler may observe an invalid
|
||||||
|
// upstream config because its initialization function have not been called yet.
|
||||||
|
mainLog.Load().Debug().Msg("setup upstream with new config")
|
||||||
|
p.setupUpstream(newCfg)
|
||||||
|
|
||||||
|
p.mu.Lock()
|
||||||
|
*p.cfg = *newCfg
|
||||||
|
p.mu.Unlock()
|
||||||
|
|
||||||
|
logger.Notice().Msg("reloading config successfully")
|
||||||
|
select {
|
||||||
|
case p.reloadDoneCh <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (p *prog) preRun() {
|
func (p *prog) preRun() {
|
||||||
if !service.Interactive() {
|
if !service.Interactive() {
|
||||||
p.setDNS()
|
p.setDNS()
|
||||||
@@ -87,14 +192,54 @@ func (p *prog) preRun() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *prog) run() {
|
func (p *prog) setupUpstream(cfg *ctrld.Config) {
|
||||||
|
localUpstreams := make([]string, 0, len(cfg.Upstream))
|
||||||
|
ptrNameservers := make([]string, 0, len(cfg.Upstream))
|
||||||
|
for n := range cfg.Upstream {
|
||||||
|
uc := cfg.Upstream[n]
|
||||||
|
uc.Init()
|
||||||
|
if uc.BootstrapIP == "" {
|
||||||
|
uc.SetupBootstrapIP()
|
||||||
|
mainLog.Load().Info().Msgf("bootstrap IPs for upstream.%s: %q", n, uc.BootstrapIPs())
|
||||||
|
} else {
|
||||||
|
mainLog.Load().Info().Str("bootstrap_ip", uc.BootstrapIP).Msgf("using bootstrap IP for upstream.%s", n)
|
||||||
|
}
|
||||||
|
uc.SetCertPool(rootCertPool)
|
||||||
|
go uc.Ping()
|
||||||
|
|
||||||
|
if canBeLocalUpstream(uc.Domain) {
|
||||||
|
localUpstreams = append(localUpstreams, upstreamPrefix+n)
|
||||||
|
}
|
||||||
|
if uc.IsDiscoverable() {
|
||||||
|
ptrNameservers = append(ptrNameservers, uc.Endpoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.localUpstreams = localUpstreams
|
||||||
|
p.ptrNameservers = ptrNameservers
|
||||||
|
}
|
||||||
|
|
||||||
|
// run runs the ctrld main components.
|
||||||
|
//
|
||||||
|
// The reload boolean indicates that the function is run when ctrld first start
|
||||||
|
// or when ctrld receive reloading signal. Platform specifics setup is only done
|
||||||
|
// on started, mean reload is "false".
|
||||||
|
//
|
||||||
|
// The reloadCh is used to signal ctrld listeners that ctrld is going to be reloaded,
|
||||||
|
// so all listeners could be terminated and re-spawned again.
|
||||||
|
func (p *prog) run(reload bool, reloadCh chan struct{}) {
|
||||||
// Wait the caller to signal that we can do our logic.
|
// Wait the caller to signal that we can do our logic.
|
||||||
<-p.waitCh
|
<-p.waitCh
|
||||||
p.preRun()
|
if !reload {
|
||||||
|
p.preRun()
|
||||||
|
}
|
||||||
numListeners := len(p.cfg.Listener)
|
numListeners := len(p.cfg.Listener)
|
||||||
p.started = make(chan struct{}, numListeners)
|
if !reload {
|
||||||
|
p.started = make(chan struct{}, numListeners)
|
||||||
|
}
|
||||||
p.onStartedDone = make(chan struct{})
|
p.onStartedDone = make(chan struct{})
|
||||||
p.loop = make(map[string]bool)
|
p.loop = make(map[string]bool)
|
||||||
|
p.lanLoopGuard = newLoopGuard()
|
||||||
|
p.ptrLoopGuard = newLoopGuard()
|
||||||
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 {
|
||||||
@@ -103,15 +248,7 @@ func (p *prog) run() {
|
|||||||
p.cache = cacher
|
p.cache = cacher
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
p.sema = &chanSemaphore{ready: make(chan struct{}, defaultSemaphoreCap)}
|
|
||||||
if mcr := p.cfg.Service.MaxConcurrentRequests; mcr != nil {
|
|
||||||
n := *mcr
|
|
||||||
if n == 0 {
|
|
||||||
p.sema = &noopSemaphore{}
|
|
||||||
} else {
|
|
||||||
p.sema = &chanSemaphore{ready: make(chan struct{}, n)}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Add(len(p.cfg.Listener))
|
wg.Add(len(p.cfg.Listener))
|
||||||
|
|
||||||
@@ -127,79 +264,119 @@ func (p *prog) run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
p.um = newUpstreamMonitor(p.cfg)
|
p.um = newUpstreamMonitor(p.cfg)
|
||||||
for n := range p.cfg.Upstream {
|
|
||||||
uc := p.cfg.Upstream[n]
|
if !reload {
|
||||||
uc.Init()
|
p.sema = &chanSemaphore{ready: make(chan struct{}, defaultSemaphoreCap)}
|
||||||
if uc.BootstrapIP == "" {
|
if mcr := p.cfg.Service.MaxConcurrentRequests; mcr != nil {
|
||||||
uc.SetupBootstrapIP()
|
n := *mcr
|
||||||
mainLog.Load().Info().Msgf("bootstrap IPs for upstream.%s: %q", n, uc.BootstrapIPs())
|
if n == 0 {
|
||||||
} else {
|
p.sema = &noopSemaphore{}
|
||||||
mainLog.Load().Info().Str("bootstrap_ip", uc.BootstrapIP).Msgf("using bootstrap IP for upstream.%s", n)
|
} else {
|
||||||
|
p.sema = &chanSemaphore{ready: make(chan struct{}, n)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.setupUpstream(p.cfg)
|
||||||
|
p.ciTable = clientinfo.NewTable(&cfg, defaultRouteIP(), cdUID, p.ptrNameservers)
|
||||||
|
if leaseFile := p.cfg.Service.DHCPLeaseFile; leaseFile != "" {
|
||||||
|
mainLog.Load().Debug().Msgf("watching custom lease file: %s", leaseFile)
|
||||||
|
format := ctrld.LeaseFileFormat(p.cfg.Service.DHCPLeaseFileFormat)
|
||||||
|
p.ciTable.AddLeaseFile(leaseFile, format)
|
||||||
}
|
}
|
||||||
uc.SetCertPool(rootCertPool)
|
|
||||||
go uc.Ping()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
p.ciTable = clientinfo.NewTable(&cfg, defaultRouteIP(), cdUID)
|
// context for managing spawn goroutines.
|
||||||
if leaseFile := p.cfg.Service.DHCPLeaseFile; leaseFile != "" {
|
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||||
mainLog.Load().Debug().Msgf("watching custom lease file: %s", leaseFile)
|
defer cancelFunc()
|
||||||
format := ctrld.LeaseFileFormat(p.cfg.Service.DHCPLeaseFileFormat)
|
|
||||||
p.ciTable.AddLeaseFile(leaseFile, format)
|
|
||||||
}
|
|
||||||
// Newer versions of android and iOS denies permission which breaks connectivity.
|
// Newer versions of android and iOS denies permission which breaks connectivity.
|
||||||
if !isMobile() {
|
if !isMobile() && !reload {
|
||||||
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
p.ciTable.Init()
|
p.ciTable.Init()
|
||||||
p.ciTable.RefreshLoop(p.stopCh)
|
p.ciTable.RefreshLoop(ctx)
|
||||||
}()
|
}()
|
||||||
go p.watchLinkState()
|
go p.watchLinkState(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
for listenerNum := range p.cfg.Listener {
|
for listenerNum := range p.cfg.Listener {
|
||||||
p.cfg.Listener[listenerNum].Init()
|
p.cfg.Listener[listenerNum].Init()
|
||||||
go func(listenerNum string) {
|
if !reload {
|
||||||
defer wg.Done()
|
go func(listenerNum string) {
|
||||||
listenerConfig := p.cfg.Listener[listenerNum]
|
listenerConfig := p.cfg.Listener[listenerNum]
|
||||||
upstreamConfig := p.cfg.Upstream[listenerNum]
|
upstreamConfig := p.cfg.Upstream[listenerNum]
|
||||||
if upstreamConfig == nil {
|
if upstreamConfig == nil {
|
||||||
mainLog.Load().Warn().Msgf("no default upstream for: [listener.%s]", listenerNum)
|
mainLog.Load().Warn().Msgf("no default upstream for: [listener.%s]", listenerNum)
|
||||||
|
}
|
||||||
|
addr := net.JoinHostPort(listenerConfig.IP, strconv.Itoa(listenerConfig.Port))
|
||||||
|
mainLog.Load().Info().Msgf("starting DNS server on listener.%s: %s", listenerNum, addr)
|
||||||
|
if err := p.serveDNS(listenerNum); err != nil {
|
||||||
|
mainLog.Load().Fatal().Err(err).Msgf("unable to start dns proxy on listener.%s", listenerNum)
|
||||||
|
}
|
||||||
|
}(listenerNum)
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
cancelFunc()
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case <-p.stopCh:
|
||||||
|
case <-ctx.Done():
|
||||||
|
case <-reloadCh:
|
||||||
}
|
}
|
||||||
addr := net.JoinHostPort(listenerConfig.IP, strconv.Itoa(listenerConfig.Port))
|
}()
|
||||||
mainLog.Load().Info().Msgf("starting DNS server on listener.%s: %s", listenerNum, addr)
|
|
||||||
if err := p.serveDNS(listenerNum); err != nil {
|
|
||||||
mainLog.Load().Fatal().Err(err).Msgf("unable to start dns proxy on listener.%s", listenerNum)
|
|
||||||
}
|
|
||||||
}(listenerNum)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < numListeners; i++ {
|
if !reload {
|
||||||
<-p.started
|
for i := 0; i < numListeners; i++ {
|
||||||
|
<-p.started
|
||||||
|
}
|
||||||
|
for _, f := range p.onStarted {
|
||||||
|
f()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for _, f := range p.onStarted {
|
|
||||||
f()
|
|
||||||
}
|
|
||||||
// Check for possible DNS loop.
|
|
||||||
p.checkDnsLoop()
|
|
||||||
close(p.onStartedDone)
|
close(p.onStartedDone)
|
||||||
|
|
||||||
// Start check DNS loop ticker.
|
wg.Add(1)
|
||||||
go p.checkDnsLoopTicker()
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
// Check for possible DNS loop.
|
||||||
|
p.checkDnsLoop()
|
||||||
|
// Start check DNS loop ticker.
|
||||||
|
p.checkDnsLoopTicker(ctx)
|
||||||
|
}()
|
||||||
|
|
||||||
// Stop writing log to unix socket.
|
wg.Add(1)
|
||||||
consoleWriter.Out = os.Stdout
|
// Prometheus exporter goroutine.
|
||||||
initLoggingWithBackup(false)
|
go func() {
|
||||||
if p.logConn != nil {
|
defer wg.Done()
|
||||||
_ = p.logConn.Close()
|
p.runMetricsServer(ctx, reloadCh)
|
||||||
}
|
}()
|
||||||
if p.cs != nil {
|
|
||||||
p.registerControlServerHandler()
|
if !reload {
|
||||||
if err := p.cs.start(); err != nil {
|
// Stop writing log to unix socket.
|
||||||
mainLog.Load().Warn().Err(err).Msg("could not start control server")
|
consoleWriter.Out = os.Stdout
|
||||||
|
initLoggingWithBackup(false)
|
||||||
|
if p.logConn != nil {
|
||||||
|
_ = p.logConn.Close()
|
||||||
|
}
|
||||||
|
if p.cs != nil {
|
||||||
|
p.registerControlServerHandler()
|
||||||
|
if err := p.cs.start(); err != nil {
|
||||||
|
mainLog.Load().Warn().Err(err).Msg("could not start control server")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// metricsEnabled reports whether prometheus exporter is enabled/disabled.
|
||||||
|
func (p *prog) metricsEnabled() bool {
|
||||||
|
return p.cfg.Service.MetricsQueryStats || p.cfg.Service.MetricsListener != ""
|
||||||
|
}
|
||||||
|
|
||||||
func (p *prog) Stop(s service.Service) error {
|
func (p *prog) Stop(s service.Service) error {
|
||||||
mainLog.Load().Info().Msg("Service stopped")
|
mainLog.Load().Info().Msg("Service stopped")
|
||||||
close(p.stopCh)
|
close(p.stopCh)
|
||||||
@@ -276,7 +453,7 @@ func (p *prog) setDNS() {
|
|||||||
|
|
||||||
nameservers := []string{ns}
|
nameservers := []string{ns}
|
||||||
if needRFC1918Listeners(lc) {
|
if needRFC1918Listeners(lc) {
|
||||||
nameservers = append(nameservers, rfc1918Addresses()...)
|
nameservers = append(nameservers, ctrld.Rfc1918Addresses()...)
|
||||||
}
|
}
|
||||||
if err := setDNS(netIface, nameservers); err != nil {
|
if err := setDNS(netIface, nameservers); err != nil {
|
||||||
logger.Error().Err(err).Msgf("could not set DNS for interface")
|
logger.Error().Err(err).Msgf("could not set DNS for interface")
|
||||||
@@ -360,6 +537,7 @@ var (
|
|||||||
windowsENETUNREACH = syscall.Errno(10051)
|
windowsENETUNREACH = syscall.Errno(10051)
|
||||||
windowsEINVAL = syscall.Errno(10022)
|
windowsEINVAL = syscall.Errno(10022)
|
||||||
windowsEADDRINUSE = syscall.Errno(10048)
|
windowsEADDRINUSE = syscall.Errno(10048)
|
||||||
|
windowsEHOSTUNREACH = syscall.Errno(10065)
|
||||||
)
|
)
|
||||||
|
|
||||||
func errUrlNetworkError(err error) bool {
|
func errUrlNetworkError(err error) bool {
|
||||||
@@ -382,7 +560,8 @@ func errNetworkError(err error) bool {
|
|||||||
errors.Is(opErr.Err, syscall.ENETUNREACH),
|
errors.Is(opErr.Err, syscall.ENETUNREACH),
|
||||||
errors.Is(opErr.Err, windowsENETUNREACH),
|
errors.Is(opErr.Err, windowsENETUNREACH),
|
||||||
errors.Is(opErr.Err, windowsEINVAL),
|
errors.Is(opErr.Err, windowsEINVAL),
|
||||||
errors.Is(opErr.Err, windowsECONNREFUSED):
|
errors.Is(opErr.Err, windowsECONNREFUSED),
|
||||||
|
errors.Is(opErr.Err, windowsEHOSTUNREACH):
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -462,3 +641,11 @@ func defaultRouteIP() string {
|
|||||||
mainLog.Load().Debug().Str("ip", ip).Msg("found LAN interface IP")
|
mainLog.Load().Debug().Str("ip", ip).Msg("found LAN interface IP")
|
||||||
return ip
|
return ip
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// canBeLocalUpstream reports whether the IP address can be used as a local upstream.
|
||||||
|
func canBeLocalUpstream(addr string) bool {
|
||||||
|
if ip, err := netip.ParseAddr(addr); err == nil {
|
||||||
|
return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || tsaddr.CGNATRange().Contains(ip)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ func setDependencies(svc *service.Config) {
|
|||||||
"Wants=NetworkManager-wait-online.service",
|
"Wants=NetworkManager-wait-online.service",
|
||||||
"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",
|
|
||||||
"Wants=nss-lookup.target",
|
"Wants=nss-lookup.target",
|
||||||
"After=nss-lookup.target",
|
"After=nss-lookup.target",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import "github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
|
const (
|
||||||
|
metricsLabelListener = "listener"
|
||||||
|
metricsLabelClientSourceIP = "client_source_ip"
|
||||||
|
metricsLabelClientMac = "client_mac"
|
||||||
|
metricsLabelClientHostname = "client_hostname"
|
||||||
|
metricsLabelUpstream = "upstream"
|
||||||
|
metricsLabelRRType = "rr_type"
|
||||||
|
metricsLabelRCode = "rcode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// statsVersion represent ctrld version.
|
||||||
|
var statsVersion = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||||
|
Name: "ctrld_build_info",
|
||||||
|
Help: "Version of ctrld process.",
|
||||||
|
}, []string{"gitref", "goversion", "version"})
|
||||||
|
|
||||||
|
// statsTimeStart represents start time of ctrld service.
|
||||||
|
var statsTimeStart = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||||
|
Name: "ctrld_time_seconds",
|
||||||
|
Help: "Start time of the ctrld process since unix epoch in seconds.",
|
||||||
|
})
|
||||||
|
|
||||||
|
var statsQueriesCountLabels = []string{
|
||||||
|
metricsLabelListener,
|
||||||
|
metricsLabelClientSourceIP,
|
||||||
|
metricsLabelClientMac,
|
||||||
|
metricsLabelClientHostname,
|
||||||
|
metricsLabelUpstream,
|
||||||
|
metricsLabelRRType,
|
||||||
|
metricsLabelRCode,
|
||||||
|
}
|
||||||
|
|
||||||
|
// statsQueriesCount counts total number of queries.
|
||||||
|
var statsQueriesCount = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||||
|
Name: "ctrld_queries_count",
|
||||||
|
Help: "Total number of queries.",
|
||||||
|
}, statsQueriesCountLabels)
|
||||||
|
|
||||||
|
// statsClientQueriesCount counts total number of queries of a client.
|
||||||
|
//
|
||||||
|
// The labels "client_source_ip", "client_mac", "client_hostname" are unbounded,
|
||||||
|
// thus this stat is highly inefficient if there are many devices.
|
||||||
|
var statsClientQueriesCount = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||||
|
Name: "ctrld_client_queries_count",
|
||||||
|
Help: "Total number queries of a client.",
|
||||||
|
}, []string{metricsLabelClientSourceIP, metricsLabelClientMac, metricsLabelClientHostname})
|
||||||
|
|
||||||
|
// WithLabelValuesInc increases prometheus counter by 1 if query stats is enabled.
|
||||||
|
func (p *prog) WithLabelValuesInc(c *prometheus.CounterVec, lvs ...string) {
|
||||||
|
if p.cfg.Service.MetricsQueryStats {
|
||||||
|
c.WithLabelValues(lvs...).Inc()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func notifyReloadSigCh(ch chan os.Signal) {
|
||||||
|
signal.Notify(ch, syscall.SIGUSR1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *prog) sendReloadSignal() error {
|
||||||
|
return syscall.Kill(syscall.Getpid(), syscall.SIGUSR1)
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func notifyReloadSigCh(ch chan os.Signal) {}
|
||||||
|
|
||||||
|
func (p *prog) sendReloadSignal() error {
|
||||||
|
select {
|
||||||
|
case p.reloadCh <- struct{}{}:
|
||||||
|
return nil
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
}
|
||||||
|
return errors.New("timeout while sending reload signal")
|
||||||
|
}
|
||||||
+39
-25
@@ -3,11 +3,9 @@ package cli
|
|||||||
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"
|
||||||
)
|
)
|
||||||
@@ -15,53 +13,60 @@ import (
|
|||||||
const (
|
const (
|
||||||
// maxFailureRequest is the maximum failed queries allowed before an upstream is marked as down.
|
// maxFailureRequest is the maximum failed queries allowed before an upstream is marked as down.
|
||||||
maxFailureRequest = 100
|
maxFailureRequest = 100
|
||||||
// checkUpstreamMaxBackoff is the max backoff time when checking upstream status.
|
// checkUpstreamBackoffSleep is the time interval between each upstream checks.
|
||||||
checkUpstreamMaxBackoff = 2 * time.Minute
|
checkUpstreamBackoffSleep = 2 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
// upstreamMonitor performs monitoring upstreams health.
|
// upstreamMonitor performs monitoring upstreams health.
|
||||||
type upstreamMonitor struct {
|
type upstreamMonitor struct {
|
||||||
cfg *ctrld.Config
|
cfg *ctrld.Config
|
||||||
|
|
||||||
down map[string]*atomic.Bool
|
mu sync.Mutex
|
||||||
failureReq map[string]*atomic.Uint64
|
checking map[string]bool
|
||||||
|
down map[string]bool
|
||||||
mu sync.Mutex
|
failureReq map[string]uint64
|
||||||
checking map[string]bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newUpstreamMonitor(cfg *ctrld.Config) *upstreamMonitor {
|
func newUpstreamMonitor(cfg *ctrld.Config) *upstreamMonitor {
|
||||||
um := &upstreamMonitor{
|
um := &upstreamMonitor{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
down: make(map[string]*atomic.Bool),
|
|
||||||
failureReq: make(map[string]*atomic.Uint64),
|
|
||||||
checking: make(map[string]bool),
|
checking: make(map[string]bool),
|
||||||
|
down: make(map[string]bool),
|
||||||
|
failureReq: make(map[string]uint64),
|
||||||
}
|
}
|
||||||
for n := range cfg.Upstream {
|
for n := range cfg.Upstream {
|
||||||
upstream := upstreamPrefix + n
|
upstream := upstreamPrefix + n
|
||||||
um.down[upstream] = new(atomic.Bool)
|
um.reset(upstream)
|
||||||
um.failureReq[upstream] = new(atomic.Uint64)
|
|
||||||
}
|
}
|
||||||
um.down[upstreamOS] = new(atomic.Bool)
|
um.reset(upstreamOS)
|
||||||
um.failureReq[upstreamOS] = new(atomic.Uint64)
|
|
||||||
return um
|
return um
|
||||||
}
|
}
|
||||||
|
|
||||||
// increaseFailureCount increase failed queries count for an upstream by 1.
|
// increaseFailureCount increase failed queries count for an upstream by 1.
|
||||||
func (um *upstreamMonitor) increaseFailureCount(upstream string) {
|
func (um *upstreamMonitor) increaseFailureCount(upstream string) {
|
||||||
failedCount := um.failureReq[upstream].Add(1)
|
um.mu.Lock()
|
||||||
um.down[upstream].Store(failedCount >= maxFailureRequest)
|
defer um.mu.Unlock()
|
||||||
|
|
||||||
|
um.failureReq[upstream] += 1
|
||||||
|
failedCount := um.failureReq[upstream]
|
||||||
|
um.down[upstream] = failedCount >= maxFailureRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
// isDown reports whether the given upstream is being marked as down.
|
// isDown reports whether the given upstream is being marked as down.
|
||||||
func (um *upstreamMonitor) isDown(upstream string) bool {
|
func (um *upstreamMonitor) isDown(upstream string) bool {
|
||||||
return um.down[upstream].Load()
|
um.mu.Lock()
|
||||||
|
defer um.mu.Unlock()
|
||||||
|
|
||||||
|
return um.down[upstream]
|
||||||
}
|
}
|
||||||
|
|
||||||
// reset marks an upstream as up and set failed queries counter to zero.
|
// reset marks an upstream as up and set failed queries counter to zero.
|
||||||
func (um *upstreamMonitor) reset(upstream string) {
|
func (um *upstreamMonitor) reset(upstream string) {
|
||||||
um.failureReq[upstream].Store(0)
|
um.mu.Lock()
|
||||||
um.down[upstream].Store(false)
|
defer um.mu.Unlock()
|
||||||
|
|
||||||
|
um.failureReq[upstream] = 0
|
||||||
|
um.down[upstream] = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkUpstream checks the given upstream status, periodically sending query to upstream
|
// checkUpstream checks the given upstream status, periodically sending query to upstream
|
||||||
@@ -75,8 +80,12 @@ func (um *upstreamMonitor) checkUpstream(upstream string, uc *ctrld.UpstreamConf
|
|||||||
}
|
}
|
||||||
um.checking[upstream] = true
|
um.checking[upstream] = true
|
||||||
um.mu.Unlock()
|
um.mu.Unlock()
|
||||||
|
defer func() {
|
||||||
|
um.mu.Lock()
|
||||||
|
um.checking[upstream] = false
|
||||||
|
um.mu.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
bo := backoff.NewBackoff("checkUpstream", logf, checkUpstreamMaxBackoff)
|
|
||||||
resolver, err := ctrld.NewResolver(uc)
|
resolver, err := ctrld.NewResolver(uc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mainLog.Load().Warn().Err(err).Msg("could not check upstream")
|
mainLog.Load().Warn().Err(err).Msg("could not check upstream")
|
||||||
@@ -84,15 +93,20 @@ func (um *upstreamMonitor) checkUpstream(upstream string, uc *ctrld.UpstreamConf
|
|||||||
}
|
}
|
||||||
msg := new(dns.Msg)
|
msg := new(dns.Msg)
|
||||||
msg.SetQuestion(".", dns.TypeNS)
|
msg.SetQuestion(".", dns.TypeNS)
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
for {
|
check := func() error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||||
|
defer cancel()
|
||||||
|
uc.ReBootstrap()
|
||||||
_, err := resolver.Resolve(ctx, msg)
|
_, err := resolver.Resolve(ctx, msg)
|
||||||
if err == nil {
|
return err
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
if err := check(); err == nil {
|
||||||
mainLog.Load().Debug().Msgf("upstream %q is online", uc.Endpoint)
|
mainLog.Load().Debug().Msgf("upstream %q is online", uc.Endpoint)
|
||||||
um.reset(upstream)
|
um.reset(upstream)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
bo.BackOff(ctx, err)
|
time.Sleep(checkUpstreamBackoffSleep)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,14 +28,15 @@ type AppCallback interface {
|
|||||||
// Start configures utility with config.toml from provided directory.
|
// Start configures utility with config.toml from provided directory.
|
||||||
// This function will block until Stop is called
|
// This function will block until Stop is called
|
||||||
// Check port availability prior to calling it.
|
// Check port availability prior to calling it.
|
||||||
func (c *Controller) Start(CdUID string, HomeDir string, logLevel int, logPath string) {
|
func (c *Controller) Start(CdUID string, HomeDir string, UpstreamProto string, logLevel int, logPath string) {
|
||||||
if c.stopCh == nil {
|
if c.stopCh == nil {
|
||||||
c.stopCh = make(chan struct{})
|
c.stopCh = make(chan struct{})
|
||||||
c.Config = cli.AppConfig{
|
c.Config = cli.AppConfig{
|
||||||
CdUID: CdUID,
|
CdUID: CdUID,
|
||||||
HomeDir: HomeDir,
|
HomeDir: HomeDir,
|
||||||
Verbose: logLevel,
|
UpstreamProto: UpstreamProto,
|
||||||
LogPath: logPath,
|
Verbose: logLevel,
|
||||||
|
LogPath: logPath,
|
||||||
}
|
}
|
||||||
appCallback := mapCallback(c.AppCallback)
|
appCallback := mapCallback(c.AppCallback)
|
||||||
cli.RunMobile(&c.Config, &appCallback, c.stopCh)
|
cli.RunMobile(&c.Config, &appCallback, c.stopCh)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
@@ -26,6 +27,7 @@ import (
|
|||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"golang.org/x/sync/singleflight"
|
"golang.org/x/sync/singleflight"
|
||||||
"tailscale.com/logtail/backoff"
|
"tailscale.com/logtail/backoff"
|
||||||
|
"tailscale.com/net/tsaddr"
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/dnsrcode"
|
"github.com/Control-D-Inc/ctrld/internal/dnsrcode"
|
||||||
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
||||||
@@ -82,6 +84,16 @@ func InitConfig(v *viper.Viper, name string) {
|
|||||||
"0": {
|
"0": {
|
||||||
IP: "",
|
IP: "",
|
||||||
Port: 0,
|
Port: 0,
|
||||||
|
Policy: &ListenerPolicyConfig{
|
||||||
|
Name: "Main Policy",
|
||||||
|
Networks: []Rule{
|
||||||
|
{"network.0": []string{"upstream.0"}},
|
||||||
|
},
|
||||||
|
Rules: []Rule{
|
||||||
|
{"example.com": []string{"upstream.0"}},
|
||||||
|
{"*.ads.com": []string{"upstream.1"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
v.SetDefault("network", map[string]*NetworkConfig{
|
v.SetDefault("network", map[string]*NetworkConfig{
|
||||||
@@ -167,22 +179,26 @@ func (c *Config) FirstUpstream() *UpstreamConfig {
|
|||||||
|
|
||||||
// ServiceConfig specifies the general ctrld config.
|
// ServiceConfig specifies the general ctrld config.
|
||||||
type ServiceConfig struct {
|
type ServiceConfig struct {
|
||||||
LogLevel string `mapstructure:"log_level" toml:"log_level,omitempty"`
|
LogLevel string `mapstructure:"log_level" toml:"log_level,omitempty"`
|
||||||
LogPath string `mapstructure:"log_path" toml:"log_path,omitempty"`
|
LogPath string `mapstructure:"log_path" toml:"log_path,omitempty"`
|
||||||
CacheEnable bool `mapstructure:"cache_enable" toml:"cache_enable,omitempty"`
|
CacheEnable bool `mapstructure:"cache_enable" toml:"cache_enable,omitempty"`
|
||||||
CacheSize int `mapstructure:"cache_size" toml:"cache_size,omitempty"`
|
CacheSize int `mapstructure:"cache_size" toml:"cache_size,omitempty"`
|
||||||
CacheTTLOverride int `mapstructure:"cache_ttl_override" toml:"cache_ttl_override,omitempty"`
|
CacheTTLOverride int `mapstructure:"cache_ttl_override" toml:"cache_ttl_override,omitempty"`
|
||||||
CacheServeStale bool `mapstructure:"cache_serve_stale" toml:"cache_serve_stale,omitempty"`
|
CacheServeStale bool `mapstructure:"cache_serve_stale" toml:"cache_serve_stale,omitempty"`
|
||||||
MaxConcurrentRequests *int `mapstructure:"max_concurrent_requests" toml:"max_concurrent_requests,omitempty" validate:"omitempty,gte=0"`
|
MaxConcurrentRequests *int `mapstructure:"max_concurrent_requests" toml:"max_concurrent_requests,omitempty" validate:"omitempty,gte=0"`
|
||||||
DHCPLeaseFile string `mapstructure:"dhcp_lease_file_path" toml:"dhcp_lease_file_path" validate:"omitempty,file"`
|
DHCPLeaseFile string `mapstructure:"dhcp_lease_file_path" toml:"dhcp_lease_file_path" validate:"omitempty,file"`
|
||||||
DHCPLeaseFileFormat string `mapstructure:"dhcp_lease_file_format" toml:"dhcp_lease_file_format" validate:"required_unless=DHCPLeaseFile '',omitempty,oneof=dnsmasq isc-dhcp"`
|
DHCPLeaseFileFormat string `mapstructure:"dhcp_lease_file_format" toml:"dhcp_lease_file_format" validate:"required_unless=DHCPLeaseFile '',omitempty,oneof=dnsmasq isc-dhcp"`
|
||||||
DiscoverMDNS *bool `mapstructure:"discover_mdns" toml:"discover_mdns,omitempty"`
|
DiscoverMDNS *bool `mapstructure:"discover_mdns" toml:"discover_mdns,omitempty"`
|
||||||
DiscoverARP *bool `mapstructure:"discover_arp" toml:"discover_dhcp,omitempty"`
|
DiscoverARP *bool `mapstructure:"discover_arp" toml:"discover_arp,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"`
|
DiscoverHosts *bool `mapstructure:"discover_hosts" toml:"discover_hosts,omitempty"`
|
||||||
Daemon bool `mapstructure:"-" toml:"-"`
|
DiscoverRefreshInterval int `mapstructure:"discover_refresh_interval" toml:"discover_refresh_interval,omitempty"`
|
||||||
AllocateIP bool `mapstructure:"-" toml:"-"`
|
ClientIDPref string `mapstructure:"client_id_preference" toml:"client_id_preference,omitempty" validate:"omitempty,oneof=host mac"`
|
||||||
|
MetricsQueryStats bool `mapstructure:"metrics_query_stats" toml:"metrics_query_stats,omitempty"`
|
||||||
|
MetricsListener string `mapstructure:"metrics_listener" toml:"metrics_listener,omitempty"`
|
||||||
|
Daemon bool `mapstructure:"-" toml:"-"`
|
||||||
|
AllocateIP bool `mapstructure:"-" toml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NetworkConfig specifies configuration for networks where ctrld will handle requests.
|
// NetworkConfig specifies configuration for networks where ctrld will handle requests.
|
||||||
@@ -204,6 +220,9 @@ type UpstreamConfig struct {
|
|||||||
// The caller should not access this field directly.
|
// The caller should not access this field directly.
|
||||||
// Use UpstreamSendClientInfo instead.
|
// Use UpstreamSendClientInfo instead.
|
||||||
SendClientInfo *bool `mapstructure:"send_client_info" toml:"send_client_info,omitempty"`
|
SendClientInfo *bool `mapstructure:"send_client_info" toml:"send_client_info,omitempty"`
|
||||||
|
// The caller should not access this field directly.
|
||||||
|
// Use IsDiscoverable instead.
|
||||||
|
Discoverable *bool `mapstructure:"discoverable" toml:"discoverable"`
|
||||||
|
|
||||||
g singleflight.Group
|
g singleflight.Group
|
||||||
rebootstrap atomic.Bool
|
rebootstrap atomic.Bool
|
||||||
@@ -224,10 +243,11 @@ type UpstreamConfig struct {
|
|||||||
|
|
||||||
// ListenerConfig specifies the networks configuration that ctrld will run on.
|
// ListenerConfig specifies the networks configuration that ctrld will run on.
|
||||||
type ListenerConfig struct {
|
type ListenerConfig struct {
|
||||||
IP string `mapstructure:"ip" toml:"ip,omitempty" validate:"iporempty"`
|
IP string `mapstructure:"ip" toml:"ip,omitempty" validate:"iporempty"`
|
||||||
Port int `mapstructure:"port" toml:"port,omitempty" validate:"gte=0"`
|
Port int `mapstructure:"port" toml:"port,omitempty" validate:"gte=0"`
|
||||||
Restricted bool `mapstructure:"restricted" toml:"restricted,omitempty"`
|
Restricted bool `mapstructure:"restricted" toml:"restricted,omitempty"`
|
||||||
Policy *ListenerPolicyConfig `mapstructure:"policy" toml:"policy,omitempty"`
|
AllowWanClients bool `mapstructure:"allow_wan_clients" toml:"allow_wan_clients,omitempty"`
|
||||||
|
Policy *ListenerPolicyConfig `mapstructure:"policy" toml:"policy,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsDirectDnsListener reports whether ctrld can be a direct listener on port 53.
|
// IsDirectDnsListener reports whether ctrld can be a direct listener on port 53.
|
||||||
@@ -253,6 +273,7 @@ type ListenerPolicyConfig struct {
|
|||||||
Name string `mapstructure:"name" toml:"name,omitempty"`
|
Name string `mapstructure:"name" toml:"name,omitempty"`
|
||||||
Networks []Rule `mapstructure:"networks" toml:"networks,omitempty,inline,multiline" validate:"dive,len=1"`
|
Networks []Rule `mapstructure:"networks" toml:"networks,omitempty,inline,multiline" validate:"dive,len=1"`
|
||||||
Rules []Rule `mapstructure:"rules" toml:"rules,omitempty,inline,multiline" validate:"dive,len=1"`
|
Rules []Rule `mapstructure:"rules" toml:"rules,omitempty,inline,multiline" validate:"dive,len=1"`
|
||||||
|
Macs []Rule `mapstructure:"macs" toml:"macs,omitempty,inline,multiline" validate:"dive,len=1"`
|
||||||
FailoverRcodes []string `mapstructure:"failover_rcodes" toml:"failover_rcodes,omitempty" validate:"dive,dnsrcode"`
|
FailoverRcodes []string `mapstructure:"failover_rcodes" toml:"failover_rcodes,omitempty" validate:"dive,dnsrcode"`
|
||||||
FailoverRcodeNumbers []int `mapstructure:"-" toml:"-"`
|
FailoverRcodeNumbers []int `mapstructure:"-" toml:"-"`
|
||||||
}
|
}
|
||||||
@@ -322,13 +343,28 @@ func (uc *UpstreamConfig) UpstreamSendClientInfo() bool {
|
|||||||
}
|
}
|
||||||
switch uc.Type {
|
switch uc.Type {
|
||||||
case ResolverTypeDOH, ResolverTypeDOH3:
|
case ResolverTypeDOH, ResolverTypeDOH3:
|
||||||
if uc.isControlD() {
|
if uc.isControlD() || uc.isNextDNS() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsDiscoverable reports whether the upstream can be used for PTR discovery.
|
||||||
|
// The caller must ensure uc.Init() was called before calling this.
|
||||||
|
func (uc *UpstreamConfig) IsDiscoverable() bool {
|
||||||
|
if uc.Discoverable != nil {
|
||||||
|
return *uc.Discoverable
|
||||||
|
}
|
||||||
|
switch uc.Type {
|
||||||
|
case ResolverTypeOS, ResolverTypeLegacy, ResolverTypePrivate:
|
||||||
|
if ip, err := netip.ParseAddr(uc.Domain); err == nil {
|
||||||
|
return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || tsaddr.CGNATRange().Contains(ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// BootstrapIPs returns the bootstrap IPs list of upstreams.
|
// BootstrapIPs returns the bootstrap IPs list of upstreams.
|
||||||
func (uc *UpstreamConfig) BootstrapIPs() []string {
|
func (uc *UpstreamConfig) BootstrapIPs() []string {
|
||||||
return uc.bootstrapIPs
|
return uc.bootstrapIPs
|
||||||
@@ -394,8 +430,9 @@ func (uc *UpstreamConfig) ReBootstrap() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, _, _ = uc.g.Do("ReBootstrap", func() (any, error) {
|
_, _, _ = uc.g.Do("ReBootstrap", func() (any, error) {
|
||||||
ProxyLogger.Load().Debug().Msg("re-bootstrapping upstream ip")
|
if uc.rebootstrap.CompareAndSwap(false, true) {
|
||||||
uc.rebootstrap.Store(true)
|
ProxyLogger.Load().Debug().Msg("re-bootstrapping upstream ip")
|
||||||
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -519,6 +556,16 @@ func (uc *UpstreamConfig) isControlD() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (uc *UpstreamConfig) isNextDNS() bool {
|
||||||
|
domain := uc.Domain
|
||||||
|
if domain == "" {
|
||||||
|
if u, err := url.Parse(uc.Endpoint); err == nil {
|
||||||
|
domain = u.Hostname()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return domain == "dns.nextdns.io"
|
||||||
|
}
|
||||||
|
|
||||||
func (uc *UpstreamConfig) dohTransport(dnsType uint16) http.RoundTripper {
|
func (uc *UpstreamConfig) dohTransport(dnsType uint16) http.RoundTripper {
|
||||||
uc.transportOnce.Do(func() {
|
uc.transportOnce.Do(func() {
|
||||||
uc.SetupTransport()
|
uc.SetupTransport()
|
||||||
|
|||||||
@@ -279,6 +279,61 @@ func TestUpstreamConfig_UpstreamSendClientInfo(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUpstreamConfig_IsDiscoverable(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
uc *UpstreamConfig
|
||||||
|
discoverable bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"loopback",
|
||||||
|
&UpstreamConfig{Endpoint: "127.0.0.1", Type: ResolverTypeLegacy},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rfc1918",
|
||||||
|
&UpstreamConfig{Endpoint: "192.168.1.1", Type: ResolverTypeLegacy},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CGNAT",
|
||||||
|
&UpstreamConfig{Endpoint: "100.66.67.68", Type: ResolverTypeLegacy},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Public IP",
|
||||||
|
&UpstreamConfig{Endpoint: "8.8.8.8", Type: ResolverTypeLegacy},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"override discoverable",
|
||||||
|
&UpstreamConfig{Endpoint: "127.0.0.1", Type: ResolverTypeLegacy, Discoverable: ptrBool(false)},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"override non-public",
|
||||||
|
&UpstreamConfig{Endpoint: "1.1.1.1", Type: ResolverTypeLegacy, Discoverable: ptrBool(true)},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"non-legacy upstream",
|
||||||
|
&UpstreamConfig{Endpoint: "https://192.168.1.1/custom-doh", Type: ResolverTypeDOH},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
tc.uc.Init()
|
||||||
|
if got := tc.uc.IsDiscoverable(); got != tc.discoverable {
|
||||||
|
t.Errorf("unexpected result, want: %v, got: %v", tc.discoverable, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func ptrBool(b bool) *bool {
|
func ptrBool(b bool) *bool {
|
||||||
return &b
|
return &b
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-11
@@ -10,13 +10,10 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
"github.com/quic-go/quic-go"
|
"github.com/quic-go/quic-go"
|
||||||
"github.com/quic-go/quic-go/http3"
|
"github.com/quic-go/quic-go/http3"
|
||||||
|
|
||||||
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (uc *UpstreamConfig) setupDOH3Transport() {
|
func (uc *UpstreamConfig) setupDOH3Transport() {
|
||||||
@@ -29,9 +26,7 @@ func (uc *UpstreamConfig) setupDOH3Transport() {
|
|||||||
uc.http3RoundTripper = uc.newDOH3Transport(uc.bootstrapIPs6)
|
uc.http3RoundTripper = uc.newDOH3Transport(uc.bootstrapIPs6)
|
||||||
case IpStackSplit:
|
case IpStackSplit:
|
||||||
uc.http3RoundTripper4 = uc.newDOH3Transport(uc.bootstrapIPs4)
|
uc.http3RoundTripper4 = uc.newDOH3Transport(uc.bootstrapIPs4)
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
if hasIPv6() {
|
||||||
defer cancel()
|
|
||||||
if ctrldnet.IPv6Available(ctx) {
|
|
||||||
uc.http3RoundTripper6 = uc.newDOH3Transport(uc.bootstrapIPs6)
|
uc.http3RoundTripper6 = uc.newDOH3Transport(uc.bootstrapIPs6)
|
||||||
} else {
|
} else {
|
||||||
uc.http3RoundTripper6 = uc.http3RoundTripper4
|
uc.http3RoundTripper6 = uc.http3RoundTripper4
|
||||||
@@ -127,11 +122,6 @@ func (d *quicParallelDialer) Dial(ctx context.Context, addrs []string, tlsCfg *t
|
|||||||
close(ch)
|
close(ch)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
udpConn, err := net.ListenUDP("udp", nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, addr := range addrs {
|
for _, addr := range addrs {
|
||||||
go func(addr string) {
|
go func(addr string) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
@@ -140,6 +130,11 @@ func (d *quicParallelDialer) Dial(ctx context.Context, addrs []string, tlsCfg *t
|
|||||||
ch <- ¶llelDialerResult{conn: nil, err: err}
|
ch <- ¶llelDialerResult{conn: nil, err: err}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
udpConn, err := net.ListenUDP("udp", nil)
|
||||||
|
if err != nil {
|
||||||
|
ch <- ¶llelDialerResult{conn: nil, err: err}
|
||||||
|
return
|
||||||
|
}
|
||||||
conn, err := quic.DialEarly(ctx, udpConn, remoteAddr, tlsCfg, cfg)
|
conn, err := quic.DialEarly(ctx, udpConn, remoteAddr, tlsCfg, cfg)
|
||||||
select {
|
select {
|
||||||
case ch <- ¶llelDialerResult{conn: conn, err: err}:
|
case ch <- ¶llelDialerResult{conn: conn, err: err}:
|
||||||
@@ -147,6 +142,9 @@ func (d *quicParallelDialer) Dial(ctx context.Context, addrs []string, tlsCfg *t
|
|||||||
if conn != nil {
|
if conn != nil {
|
||||||
conn.CloseWithError(quic.ApplicationErrorCode(http3.ErrCodeNoError), "")
|
conn.CloseWithError(quic.ApplicationErrorCode(http3.ErrCodeNoError), "")
|
||||||
}
|
}
|
||||||
|
if udpConn != nil {
|
||||||
|
udpConn.Close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}(addr)
|
}(addr)
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-1
@@ -54,7 +54,12 @@ func TestLoadDefaultConfig(t *testing.T) {
|
|||||||
cfg := defaultConfig(t)
|
cfg := defaultConfig(t)
|
||||||
validate := validator.New()
|
validate := validator.New()
|
||||||
require.NoError(t, ctrld.ValidateConfig(validate, cfg))
|
require.NoError(t, ctrld.ValidateConfig(validate, cfg))
|
||||||
assert.Len(t, cfg.Listener, 1)
|
if assert.Len(t, cfg.Listener, 1) {
|
||||||
|
l0 := cfg.Listener["0"]
|
||||||
|
require.NotNil(t, l0.Policy)
|
||||||
|
assert.Len(t, l0.Policy.Networks, 1)
|
||||||
|
assert.Len(t, l0.Policy.Rules, 2)
|
||||||
|
}
|
||||||
assert.Len(t, cfg.Upstream, 2)
|
assert.Len(t, cfg.Upstream, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +101,7 @@ func TestConfigValidation(t *testing.T) {
|
|||||||
{"lease file format required if lease file exist", configWithExistedLeaseFile(t), true},
|
{"lease file format required if lease file exist", configWithExistedLeaseFile(t), true},
|
||||||
{"invalid lease file format", configWithInvalidLeaseFileFormat(t), true},
|
{"invalid lease file format", configWithInvalidLeaseFileFormat(t), true},
|
||||||
{"invalid doh/doh3 endpoint", configWithInvalidDoHEndpoint(t), true},
|
{"invalid doh/doh3 endpoint", configWithInvalidDoHEndpoint(t), true},
|
||||||
|
{"invalid client id pref", configWithInvalidClientIDPref(t), true},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
@@ -115,6 +121,29 @@ func TestConfigValidation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfigDiscoverOverride(t *testing.T) {
|
||||||
|
v := viper.NewWithOptions(viper.KeyDelimiter("::"))
|
||||||
|
ctrld.InitConfig(v, "test_config_discover_override")
|
||||||
|
v.SetConfigType("toml")
|
||||||
|
configStr := `
|
||||||
|
[service]
|
||||||
|
discover_arp = false
|
||||||
|
discover_dhcp = false
|
||||||
|
discover_hosts = false
|
||||||
|
discover_mdns = false
|
||||||
|
discover_ptr = false
|
||||||
|
`
|
||||||
|
require.NoError(t, v.ReadConfig(strings.NewReader(configStr)))
|
||||||
|
cfg := ctrld.Config{}
|
||||||
|
require.NoError(t, v.Unmarshal(&cfg))
|
||||||
|
|
||||||
|
require.False(t, *cfg.Service.DiscoverARP)
|
||||||
|
require.False(t, *cfg.Service.DiscoverDHCP)
|
||||||
|
require.False(t, *cfg.Service.DiscoverHosts)
|
||||||
|
require.False(t, *cfg.Service.DiscoverMDNS)
|
||||||
|
require.False(t, *cfg.Service.DiscoverPtr)
|
||||||
|
}
|
||||||
|
|
||||||
func defaultConfig(t *testing.T) *ctrld.Config {
|
func defaultConfig(t *testing.T) *ctrld.Config {
|
||||||
v := viper.New()
|
v := viper.New()
|
||||||
ctrld.InitConfig(v, "test_load_default_config")
|
ctrld.InitConfig(v, "test_load_default_config")
|
||||||
@@ -233,3 +262,9 @@ func configWithInvalidDoHEndpoint(t *testing.T) *ctrld.Config {
|
|||||||
cfg.Upstream["0"].Type = ctrld.ResolverTypeDOH
|
cfg.Upstream["0"].Type = ctrld.ResolverTypeDOH
|
||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func configWithInvalidClientIDPref(t *testing.T) *ctrld.Config {
|
||||||
|
cfg := defaultConfig(t)
|
||||||
|
cfg.Service.ClientIDPref = "foo"
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|||||||
+96
-5
@@ -14,7 +14,7 @@ The config file allows for advanced configuration of the `ctrld` utility to cove
|
|||||||
|
|
||||||
|
|
||||||
## Config Location
|
## Config Location
|
||||||
`ctrld` uses [TOML](toml_link) format for its configuration file. Default configuration file is `ctrld.toml` found in following order:
|
`ctrld` uses [TOML][toml_link] format for its configuration file. Default configuration file is `ctrld.toml` found in following order:
|
||||||
|
|
||||||
- `/etc/controld` on *nix.
|
- `/etc/controld` on *nix.
|
||||||
- User's home directory on Windows.
|
- User's home directory on Windows.
|
||||||
@@ -200,6 +200,14 @@ Perform LAN client discovery using hosts file.
|
|||||||
- Required: no
|
- Required: no
|
||||||
- Default: true
|
- Default: true
|
||||||
|
|
||||||
|
### discover_refresh_interval
|
||||||
|
Time in seconds between each discovery refresh loop to update new client information data.
|
||||||
|
The default value is 120 seconds, lower this value to make the discovery process run more aggressively.
|
||||||
|
|
||||||
|
- Type: integer
|
||||||
|
- Required: no
|
||||||
|
- Default: 120
|
||||||
|
|
||||||
### 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.
|
||||||
|
|
||||||
@@ -215,6 +223,31 @@ DHCP leases file format.
|
|||||||
- Valid values: `dnsmasq`, `isc-dhcp`
|
- Valid values: `dnsmasq`, `isc-dhcp`
|
||||||
- Default: ""
|
- Default: ""
|
||||||
|
|
||||||
|
### client_id_preference
|
||||||
|
Decide how the client ID is generated
|
||||||
|
|
||||||
|
If `host` -> client id will only use the hostname i.e.`hash(hostname)`.
|
||||||
|
If `mac` -> client id will only use the MAC address `hash(mac)`.
|
||||||
|
Else -> client ID will use both Mac and Hostname i.e. `hash(mac + host)
|
||||||
|
- Type: string
|
||||||
|
- Required: no
|
||||||
|
- Valid values: `mac`, `host`
|
||||||
|
- Default: ""
|
||||||
|
|
||||||
|
### metrics_query_stats
|
||||||
|
If set to `true`, collect and export the query counters, and show them in `clients list` command.
|
||||||
|
|
||||||
|
- Type: boolean
|
||||||
|
- Required: no
|
||||||
|
- Default: false
|
||||||
|
|
||||||
|
### metrics_listener
|
||||||
|
Specifying the `ip` and `port` of the metrics server.
|
||||||
|
|
||||||
|
- Type: string
|
||||||
|
- Required: no
|
||||||
|
- Default: ""
|
||||||
|
|
||||||
## Upstream
|
## Upstream
|
||||||
The `[upstream]` section specifies the DNS upstream servers that `ctrld` will forward DNS requests to.
|
The `[upstream]` section specifies the DNS upstream servers that `ctrld` will forward DNS requests to.
|
||||||
|
|
||||||
@@ -319,6 +352,24 @@ If `ip_stack` is empty, or undefined:
|
|||||||
- Default value is `both` for non-Control D resolvers.
|
- Default value is `both` for non-Control D resolvers.
|
||||||
- Default value is `split` for Control D resolvers.
|
- Default value is `split` for Control D resolvers.
|
||||||
|
|
||||||
|
### send_client_info
|
||||||
|
Specifying whether to include client info when sending query to upstream. **This will only work with `doh` or `doh3` type upstreams.**
|
||||||
|
|
||||||
|
- Type: boolean
|
||||||
|
- Required: no
|
||||||
|
- Default:
|
||||||
|
- `true` for ControlD upstreams.
|
||||||
|
- `false` for other upstreams.
|
||||||
|
|
||||||
|
### discoverable
|
||||||
|
Specifying whether the upstream can be used for PTR discovery.
|
||||||
|
|
||||||
|
- Type: boolean
|
||||||
|
- Required: no
|
||||||
|
- Default:
|
||||||
|
- `true` for loopback/RFC1918/CGNAT IP address.
|
||||||
|
- `false` for public IP address.
|
||||||
|
|
||||||
## Network
|
## Network
|
||||||
The `[network]` section defines networks from which DNS queries can originate from. These are used in policies. You can define multiple networks, and each one can have multiple cidrs.
|
The `[network]` section defines networks from which DNS queries can originate from. These are used in policies. You can define multiple networks, and each one can have multiple cidrs.
|
||||||
|
|
||||||
@@ -376,7 +427,14 @@ Port number that the listener will listen on for incoming requests. If `port` is
|
|||||||
- Default: 0 or 53 or 5354 (depending on platform)
|
- Default: 0 or 53 or 5354 (depending on platform)
|
||||||
|
|
||||||
### restricted
|
### restricted
|
||||||
If set to `true` makes the listener `REFUSE` DNS queries from all source IP addresses that are not explicitly defined in the policy using a `network`.
|
If set to `true`, makes the listener `REFUSED` DNS queries from all source IP addresses that are not explicitly defined in the policy using a `network`.
|
||||||
|
|
||||||
|
- Type: bool
|
||||||
|
- Required: no
|
||||||
|
- Default: false
|
||||||
|
|
||||||
|
### allow_wan_clients
|
||||||
|
The listener will refuse DNS queries from WAN IPs using `REFUSED` RCODE by default. Set to `true` to disable this behavior, but this is not recommended.
|
||||||
|
|
||||||
- Type: bool
|
- Type: bool
|
||||||
- Required: no
|
- Required: no
|
||||||
@@ -386,7 +444,15 @@ If set to `true` makes the listener `REFUSE` DNS queries from all source IP addr
|
|||||||
Allows `ctrld` to set policy rules to determine which upstreams the requests will be forwarded to.
|
Allows `ctrld` to set policy rules to determine which upstreams the requests will be forwarded to.
|
||||||
If no `policy` is defined or the requests do not match any policy rules, it will be forwarded to corresponding upstream of the listener. For example, the request to `listener.0` will be forwarded to `upstream.0`.
|
If no `policy` is defined or the requests do not match any policy rules, it will be forwarded to corresponding upstream of the listener. For example, the request to `listener.0` will be forwarded to `upstream.0`.
|
||||||
|
|
||||||
The policy `rule` syntax is a simple `toml` inline table with exactly one key/value pair per rule. `key` is either the `network` or a domain. Value is the list of the upstreams. For example:
|
The policy `rule` syntax is a simple `toml` inline table with exactly one key/value pair per rule. `key` is either:
|
||||||
|
|
||||||
|
- Network.
|
||||||
|
- Domain.
|
||||||
|
- Mac Address.
|
||||||
|
|
||||||
|
Value is the list of the upstreams.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[listener.0.policy]
|
[listener.0.policy]
|
||||||
@@ -400,12 +466,18 @@ rules = [
|
|||||||
{"*.local" = ["upstream.1"]},
|
{"*.local" = ["upstream.1"]},
|
||||||
{"test.com" = ["upstream.2", "upstream.1"]},
|
{"test.com" = ["upstream.2", "upstream.1"]},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
macs = [
|
||||||
|
{"14:54:4a:8e:08:2d" = ["upstream.3"]},
|
||||||
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
Above policy will:
|
Above policy will:
|
||||||
- Forward requests on `listener.0` from `network.0` to `upstream.1`.
|
|
||||||
- Forward requests on `listener.0` for `.local` suffixed domains to `upstream.1`.
|
- Forward requests on `listener.0` for `.local` suffixed domains to `upstream.1`.
|
||||||
- Forward requests on `listener.0` for `test.com` to `upstream.2`. If timeout is reached, retry on `upstream.1`.
|
- Forward requests on `listener.0` for `test.com` to `upstream.2`. If timeout is reached, retry on `upstream.1`.
|
||||||
|
- Forward requests on `listener.0` from client with Mac `14:54:4a:8e:08:2d` to `upstream.3`.
|
||||||
|
- Forward requests on `listener.0` from `network.0` to `upstream.1`.
|
||||||
- All other requests on `listener.0` that do not match above conditions will be forwarded to `upstream.0`.
|
- All other requests on `listener.0` that do not match above conditions will be forwarded to `upstream.0`.
|
||||||
|
|
||||||
An empty upstream would not route the request to any defined upstreams, and use the OS default resolver.
|
An empty upstream would not route the request to any defined upstreams, and use the OS default resolver.
|
||||||
@@ -419,6 +491,18 @@ rules = [
|
|||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Note that the order of matching preference:
|
||||||
|
|
||||||
|
```
|
||||||
|
rules => macs => networks
|
||||||
|
```
|
||||||
|
|
||||||
|
And within each policy, the rules are processed from top to bottom.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
#### name
|
#### name
|
||||||
`name` is the name for the policy.
|
`name` is the name for the policy.
|
||||||
|
|
||||||
@@ -440,6 +524,13 @@ rules = [
|
|||||||
- Required: no
|
- Required: no
|
||||||
- Default: []
|
- Default: []
|
||||||
|
|
||||||
|
### macs:
|
||||||
|
`macs` is the list of mac rules within the policy. Mac address value is case-insensitive.
|
||||||
|
|
||||||
|
- Type: array of macs
|
||||||
|
- Required: no
|
||||||
|
- Default: []
|
||||||
|
|
||||||
### failover_rcodes
|
### failover_rcodes
|
||||||
For non success response, `failover_rcodes` allows the request to be forwarded to next upstream, if the response `RCODE` matches any value defined in `failover_rcodes`.
|
For non success response, `failover_rcodes` allows the request to be forwarded to next upstream, if the response `RCODE` matches any value defined in `failover_rcodes`.
|
||||||
|
|
||||||
@@ -460,7 +551,7 @@ networks = [
|
|||||||
|
|
||||||
If `upstream.0` returns a NXDOMAIN response, the request will be forwarded to `upstream.1` instead of returning immediately to the client.
|
If `upstream.0` returns a NXDOMAIN response, the request will be forwarded to `upstream.1` instead of returning immediately to the client.
|
||||||
|
|
||||||
See all available DNS Rcodes value [here](rcode_link).
|
See all available DNS Rcodes value [here][rcode_link].
|
||||||
|
|
||||||
[toml_link]: https://toml.io/en
|
[toml_link]: https://toml.io/en
|
||||||
[rcode_link]: https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-6
|
[rcode_link]: https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-6
|
||||||
|
|||||||
@@ -18,11 +18,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
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"
|
dohOsHeader = "x-cd-os"
|
||||||
headerApplicationDNS = "application/dns-message"
|
dohClientIDPrefHeader = "x-cd-cpref"
|
||||||
|
headerApplicationDNS = "application/dns-message"
|
||||||
)
|
)
|
||||||
|
|
||||||
// EncodeOsNameMap provides mapping from OS name to a shorter string, used for encoding x-cd-os value.
|
// EncodeOsNameMap provides mapping from OS name to a shorter string, used for encoding x-cd-os value.
|
||||||
@@ -76,7 +77,6 @@ func newDohResolver(uc *UpstreamConfig) *dohResolver {
|
|||||||
endpoint: uc.u,
|
endpoint: uc.u,
|
||||||
isDoH3: uc.Type == ResolverTypeDOH3,
|
isDoH3: uc.Type == ResolverTypeDOH3,
|
||||||
http3RoundTripper: uc.http3RoundTripper,
|
http3RoundTripper: uc.http3RoundTripper,
|
||||||
sendClientInfo: uc.UpstreamSendClientInfo(),
|
|
||||||
uc: uc,
|
uc: uc,
|
||||||
}
|
}
|
||||||
return r
|
return r
|
||||||
@@ -87,9 +87,9 @@ type dohResolver struct {
|
|||||||
endpoint *url.URL
|
endpoint *url.URL
|
||||||
isDoH3 bool
|
isDoH3 bool
|
||||||
http3RoundTripper http.RoundTripper
|
http3RoundTripper http.RoundTripper
|
||||||
sendClientInfo bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve performs DNS query with given DNS message using DOH protocol.
|
||||||
func (r *dohResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) {
|
func (r *dohResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) {
|
||||||
data, err := msg.Pack()
|
data, err := msg.Pack()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -106,7 +106,7 @@ func (r *dohResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, erro
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not create request: %w", err)
|
return nil, fmt.Errorf("could not create request: %w", err)
|
||||||
}
|
}
|
||||||
addHeader(ctx, req, r.sendClientInfo)
|
addHeader(ctx, req, r.uc)
|
||||||
dnsTyp := uint16(0)
|
dnsTyp := uint16(0)
|
||||||
if len(msg.Question) > 0 {
|
if len(msg.Question) > 0 {
|
||||||
dnsTyp = msg.Question[0].Qtype
|
dnsTyp = msg.Question[0].Qtype
|
||||||
@@ -146,30 +146,67 @@ func (r *dohResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, erro
|
|||||||
return answer, nil
|
return answer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func addHeader(ctx context.Context, req *http.Request, sendClientInfo bool) {
|
// addHeader adds necessary HTTP header to request based on upstream config.
|
||||||
req.Header.Set("Content-Type", headerApplicationDNS)
|
func addHeader(ctx context.Context, req *http.Request, uc *UpstreamConfig) {
|
||||||
req.Header.Set("Accept", headerApplicationDNS)
|
|
||||||
req.Header.Set(dohOsHeader, dohOsHeaderValue())
|
|
||||||
|
|
||||||
printed := false
|
printed := false
|
||||||
if sendClientInfo {
|
dohHeader := make(http.Header)
|
||||||
|
if uc.UpstreamSendClientInfo() {
|
||||||
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 != ""
|
printed = ci.Mac != "" || ci.IP != "" || ci.Hostname != ""
|
||||||
if ci.Mac != "" {
|
switch {
|
||||||
req.Header.Set(dohMacHeader, ci.Mac)
|
case uc.isControlD():
|
||||||
}
|
dohHeader = newControlDHeaders(ci)
|
||||||
if ci.IP != "" {
|
case uc.isNextDNS():
|
||||||
req.Header.Set(dohIPHeader, ci.IP)
|
dohHeader = newNextDNSHeaders(ci)
|
||||||
}
|
|
||||||
if ci.Hostname != "" {
|
|
||||||
req.Header.Set(dohHostHeader, ci.Hostname)
|
|
||||||
}
|
|
||||||
if ci.Self {
|
|
||||||
req.Header.Set(dohOsHeader, dohOsHeaderValue())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if printed {
|
if printed {
|
||||||
Log(ctx, ProxyLogger.Load().Debug().Interface("header", req.Header), "sending request header")
|
Log(ctx, ProxyLogger.Load().Debug(), "sending request header: %v", dohHeader)
|
||||||
}
|
}
|
||||||
|
dohHeader.Set("Content-Type", headerApplicationDNS)
|
||||||
|
dohHeader.Set("Accept", headerApplicationDNS)
|
||||||
|
req.Header = dohHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
// newControlDHeaders returns DoH/Doh3 HTTP request headers for ControlD upstream.
|
||||||
|
func newControlDHeaders(ci *ClientInfo) http.Header {
|
||||||
|
header := make(http.Header)
|
||||||
|
header.Set(dohOsHeader, dohOsHeaderValue())
|
||||||
|
if ci.Mac != "" {
|
||||||
|
header.Set(dohMacHeader, ci.Mac)
|
||||||
|
}
|
||||||
|
if ci.IP != "" {
|
||||||
|
header.Set(dohIPHeader, ci.IP)
|
||||||
|
}
|
||||||
|
if ci.Hostname != "" {
|
||||||
|
header.Set(dohHostHeader, ci.Hostname)
|
||||||
|
}
|
||||||
|
if ci.Self {
|
||||||
|
header.Set(dohOsHeader, dohOsHeaderValue())
|
||||||
|
}
|
||||||
|
switch ci.ClientIDPref {
|
||||||
|
case "mac":
|
||||||
|
header.Set(dohClientIDPrefHeader, "1")
|
||||||
|
case "host":
|
||||||
|
header.Set(dohClientIDPrefHeader, "2")
|
||||||
|
}
|
||||||
|
return header
|
||||||
|
}
|
||||||
|
|
||||||
|
// newNextDNSHeaders returns DoH/Doh3 HTTP request headers for nextdns upstream.
|
||||||
|
// https://github.com/nextdns/nextdns/blob/v1.41.0/resolver/doh.go#L100
|
||||||
|
func newNextDNSHeaders(ci *ClientInfo) http.Header {
|
||||||
|
header := make(http.Header)
|
||||||
|
if ci.Mac != "" {
|
||||||
|
// https: //github.com/nextdns/nextdns/blob/v1.41.0/run.go#L543
|
||||||
|
header.Set("X-Device-Model", "mac:"+ci.Mac[:8])
|
||||||
|
}
|
||||||
|
if ci.IP != "" {
|
||||||
|
header.Set("X-Device-Ip", ci.IP)
|
||||||
|
}
|
||||||
|
if ci.Hostname != "" {
|
||||||
|
header.Set("X-Device-Name", ci.Hostname)
|
||||||
|
}
|
||||||
|
return header
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,12 @@ require (
|
|||||||
github.com/jaytaylor/go-hostsfile v0.0.0-20220426042432-61485ac1fa6c
|
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/mdlayher/ndp v1.0.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/prometheus/client_golang v1.15.1
|
||||||
|
github.com/prometheus/prom2json v1.3.3
|
||||||
github.com/quic-go/quic-go v0.38.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
|
||||||
@@ -25,20 +28,23 @@ require (
|
|||||||
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
|
||||||
golang.org/x/net v0.10.0
|
golang.org/x/net v0.17.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.13.0
|
||||||
golang.zx2c4.com/wireguard/windows v0.5.3
|
golang.zx2c4.com/wireguard/windows v0.5.3
|
||||||
tailscale.com v1.44.0
|
tailscale.com v1.44.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect
|
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
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-20230315185526-52ccab3ef572 // 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/golang/protobuf v1.5.3 // 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
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
@@ -51,6 +57,7 @@ require (
|
|||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.18 // indirect
|
github.com/mattn/go-isatty v0.0.18 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||||
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 // indirect
|
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 // indirect
|
||||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||||
github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 // indirect
|
github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 // indirect
|
||||||
@@ -59,6 +66,9 @@ require (
|
|||||||
github.com/onsi/ginkgo/v2 v2.9.5 // 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/prometheus/client_model v0.4.0 // indirect
|
||||||
|
github.com/prometheus/common v0.44.0 // indirect
|
||||||
|
github.com/prometheus/procfs v0.9.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-20 v0.3.2 // indirect
|
github.com/quic-go/qtls-go1-20 v0.3.2 // indirect
|
||||||
github.com/rivo/uniseg v0.4.4 // indirect
|
github.com/rivo/uniseg v0.4.4 // indirect
|
||||||
@@ -70,12 +80,12 @@ require (
|
|||||||
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
|
go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect
|
||||||
golang.org/x/crypto v0.9.0 // indirect
|
golang.org/x/crypto v0.14.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.13.0 // indirect
|
||||||
golang.org/x/tools v0.9.1 // indirect
|
golang.org/x/tools v0.9.1 // indirect
|
||||||
|
google.golang.org/protobuf v1.30.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -42,7 +42,11 @@ github.com/Windscribe/zerolog v0.0.0-20230503170159-e6aa153233be h1:qBKVRi7Mom5h
|
|||||||
github.com/Windscribe/zerolog v0.0.0-20230503170159-e6aa153233be/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
|
github.com/Windscribe/zerolog v0.0.0-20230503170159-e6aa153233be/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
|
||||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA=
|
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA=
|
||||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||||
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
@@ -55,8 +59,6 @@ github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8
|
|||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
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/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 h1:40DHYsri+d1bnroFDU2FQAeq68f3kAlOzlQ93kCf26Q=
|
||||||
github.com/cuonglm/osinfo v0.0.0-20230921071424-e0e1b1e0bbbf/go.mod h1:G45410zMgmnSjLVKCq4f6GpbYAzoP2plX9rPwgx6C24=
|
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=
|
||||||
@@ -116,7 +118,9 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
|
|||||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
|
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
@@ -128,6 +132,7 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
@@ -201,8 +206,12 @@ github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
|
|||||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||||
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 h1:lez6TS6aAau+8wXUP3G9I3TGlmPFEq2CTxBaRqY6AGE=
|
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 h1:lez6TS6aAau+8wXUP3G9I3TGlmPFEq2CTxBaRqY6AGE=
|
||||||
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y=
|
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y=
|
||||||
|
github.com/mdlayher/ndp v1.0.1 h1:+yAD79/BWyFlvAoeG5ncPS0ItlHP/eVbH7bQ6/+LVA4=
|
||||||
|
github.com/mdlayher/ndp v1.0.1/go.mod h1:rf3wKaWhAYJEXFKpgF8kQ2AxypxVbfNcZbqoAo6fVzk=
|
||||||
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
|
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
|
||||||
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
||||||
github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
|
github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
|
||||||
@@ -229,7 +238,17 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
|||||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI=
|
||||||
|
github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
|
||||||
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/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
|
||||||
|
github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
|
||||||
|
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
|
||||||
|
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
|
||||||
|
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
|
||||||
|
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
|
||||||
|
github.com/prometheus/prom2json v1.3.3 h1:IYfSMiZ7sSOfliBoo89PcufjWO4eAR0gznGcETyaUgo=
|
||||||
|
github.com/prometheus/prom2json v1.3.3/go.mod h1:Pv4yIPktEkK7btWsrUTWDDDrnpUrAELaOCj+oFwlgmc=
|
||||||
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-20 v0.3.2 h1:rRgN3WfnKbyik4dBV8A6girlJVxGand/d+jVKbQq5GI=
|
github.com/quic-go/qtls-go1-20 v0.3.2 h1:rRgN3WfnKbyik4dBV8A6girlJVxGand/d+jVKbQq5GI=
|
||||||
@@ -302,8 +321,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
|||||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
@@ -331,8 +350,6 @@ 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=
|
||||||
@@ -378,8 +395,8 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v
|
|||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
@@ -452,8 +469,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.1-0.20230609144347-5059a07aa46a h1:qMsju+PNttu/NMbq8bQ9waDdxgJMu9QNoUDuhnBaYt0=
|
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||||
golang.org/x/sys v0.8.1-0.20230609144347-5059a07aa46a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
@@ -463,8 +480,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
@@ -612,7 +629,10 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
|
|||||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||||
|
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package clientinfo
|
package clientinfo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -55,10 +58,12 @@ type ipLister interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
IP netip.Addr
|
IP netip.Addr
|
||||||
Mac string
|
Mac string
|
||||||
Hostname string
|
Hostname string
|
||||||
Source map[string]struct{}
|
Source map[string]struct{}
|
||||||
|
QueryCount int64
|
||||||
|
IncludeQueryCount bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Table struct {
|
type Table struct {
|
||||||
@@ -67,26 +72,36 @@ type Table struct {
|
|||||||
hostnameResolvers []HostnameResolver
|
hostnameResolvers []HostnameResolver
|
||||||
refreshers []refresher
|
refreshers []refresher
|
||||||
initOnce sync.Once
|
initOnce sync.Once
|
||||||
|
refreshInterval int
|
||||||
|
|
||||||
dhcp *dhcp
|
dhcp *dhcp
|
||||||
merlin *merlinDiscover
|
merlin *merlinDiscover
|
||||||
arp *arpDiscover
|
ubios *ubiosDiscover
|
||||||
ptr *ptrDiscover
|
arp *arpDiscover
|
||||||
mdns *mdns
|
ndp *ndpDiscover
|
||||||
hf *hostsFile
|
ptr *ptrDiscover
|
||||||
vni *virtualNetworkIface
|
mdns *mdns
|
||||||
cfg *ctrld.Config
|
hf *hostsFile
|
||||||
quitCh chan struct{}
|
vni *virtualNetworkIface
|
||||||
selfIP string
|
svcCfg ctrld.ServiceConfig
|
||||||
cdUID string
|
quitCh chan struct{}
|
||||||
|
selfIP string
|
||||||
|
cdUID string
|
||||||
|
ptrNameservers []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTable(cfg *ctrld.Config, selfIP, cdUID string) *Table {
|
func NewTable(cfg *ctrld.Config, selfIP, cdUID string, ns []string) *Table {
|
||||||
|
refreshInterval := cfg.Service.DiscoverRefreshInterval
|
||||||
|
if refreshInterval <= 0 {
|
||||||
|
refreshInterval = 2 * 60 // 2 minutes
|
||||||
|
}
|
||||||
return &Table{
|
return &Table{
|
||||||
cfg: cfg,
|
svcCfg: cfg.Service,
|
||||||
quitCh: make(chan struct{}),
|
quitCh: make(chan struct{}),
|
||||||
selfIP: selfIP,
|
selfIP: selfIP,
|
||||||
cdUID: cdUID,
|
cdUID: cdUID,
|
||||||
|
ptrNameservers: ns,
|
||||||
|
refreshInterval: refreshInterval,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,8 +112,9 @@ func (t *Table) AddLeaseFile(name string, format ctrld.LeaseFileFormat) {
|
|||||||
clientInfoFiles[name] = format
|
clientInfoFiles[name] = format
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Table) RefreshLoop(stopCh chan struct{}) {
|
// RefreshLoop runs all the refresher to update new client info data.
|
||||||
timer := time.NewTicker(time.Minute * 5)
|
func (t *Table) RefreshLoop(ctx context.Context) {
|
||||||
|
timer := time.NewTicker(time.Second * time.Duration(t.refreshInterval))
|
||||||
defer timer.Stop()
|
defer timer.Stop()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -106,7 +122,7 @@ func (t *Table) RefreshLoop(stopCh chan struct{}) {
|
|||||||
for _, r := range t.refreshers {
|
for _, r := range t.refreshers {
|
||||||
_ = r.refresh()
|
_ = r.refresh()
|
||||||
}
|
}
|
||||||
case <-stopCh:
|
case <-ctx.Done():
|
||||||
close(t.quitCh)
|
close(t.quitCh)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -132,14 +148,26 @@ func (t *Table) init() {
|
|||||||
// Otherwise, process all possible sources in order, that means
|
// Otherwise, process all possible sources in order, that means
|
||||||
// the first result of IP/MAC/Hostname lookup will be used.
|
// the first result of IP/MAC/Hostname lookup will be used.
|
||||||
//
|
//
|
||||||
// Merlin custom clients.
|
// Routers custom clients:
|
||||||
|
// - Merlin
|
||||||
|
// - Ubios
|
||||||
if t.discoverDHCP() || t.discoverARP() {
|
if t.discoverDHCP() || t.discoverARP() {
|
||||||
t.merlin = &merlinDiscover{}
|
t.merlin = &merlinDiscover{}
|
||||||
if err := t.merlin.refresh(); err != nil {
|
t.ubios = &ubiosDiscover{}
|
||||||
ctrld.ProxyLogger.Load().Error().Err(err).Msg("could not init Merlin discover")
|
discovers := map[string]interface {
|
||||||
} else {
|
refresher
|
||||||
t.hostnameResolvers = append(t.hostnameResolvers, t.merlin)
|
HostnameResolver
|
||||||
t.refreshers = append(t.refreshers, t.merlin)
|
}{
|
||||||
|
"Merlin": t.merlin,
|
||||||
|
"Ubios": t.ubios,
|
||||||
|
}
|
||||||
|
for platform, discover := range discovers {
|
||||||
|
if err := discover.refresh(); err != nil {
|
||||||
|
ctrld.ProxyLogger.Load().Error().Err(err).Msgf("could not init %s discover", platform)
|
||||||
|
} else {
|
||||||
|
t.hostnameResolvers = append(t.hostnameResolvers, discover)
|
||||||
|
t.refreshers = append(t.refreshers, discover)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Hosts file mapping.
|
// Hosts file mapping.
|
||||||
@@ -167,21 +195,59 @@ func (t *Table) init() {
|
|||||||
}
|
}
|
||||||
go t.dhcp.watchChanges()
|
go t.dhcp.watchChanges()
|
||||||
}
|
}
|
||||||
// ARP table.
|
// ARP/NDP table.
|
||||||
if t.discoverARP() {
|
if t.discoverARP() {
|
||||||
t.arp = &arpDiscover{}
|
t.arp = &arpDiscover{}
|
||||||
|
t.ndp = &ndpDiscover{}
|
||||||
ctrld.ProxyLogger.Load().Debug().Msg("start arp discovery")
|
ctrld.ProxyLogger.Load().Debug().Msg("start arp discovery")
|
||||||
if err := t.arp.refresh(); err != nil {
|
discovers := map[string]interface {
|
||||||
ctrld.ProxyLogger.Load().Error().Err(err).Msg("could not init ARP discover")
|
refresher
|
||||||
} else {
|
IpResolver
|
||||||
t.ipResolvers = append(t.ipResolvers, t.arp)
|
MacResolver
|
||||||
t.macResolvers = append(t.macResolvers, t.arp)
|
}{
|
||||||
t.refreshers = append(t.refreshers, t.arp)
|
"ARP": t.arp,
|
||||||
|
"NDP": t.ndp,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for protocol, discover := range discovers {
|
||||||
|
if err := discover.refresh(); err != nil {
|
||||||
|
ctrld.ProxyLogger.Load().Error().Err(err).Msgf("could not init %s discover", protocol)
|
||||||
|
} else {
|
||||||
|
t.ipResolvers = append(t.ipResolvers, discover)
|
||||||
|
t.macResolvers = append(t.macResolvers, discover)
|
||||||
|
t.refreshers = append(t.refreshers, discover)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
go func() {
|
||||||
|
<-t.quitCh
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
go t.ndp.listen(ctx)
|
||||||
}
|
}
|
||||||
// PTR lookup.
|
// PTR lookup.
|
||||||
if t.discoverPTR() {
|
if t.discoverPTR() {
|
||||||
t.ptr = &ptrDiscover{resolver: ctrld.NewPrivateResolver()}
|
t.ptr = &ptrDiscover{resolver: ctrld.NewPrivateResolver()}
|
||||||
|
if len(t.ptrNameservers) > 0 {
|
||||||
|
nss := make([]string, 0, len(t.ptrNameservers))
|
||||||
|
for _, ns := range t.ptrNameservers {
|
||||||
|
host, port := ns, "53"
|
||||||
|
if h, p, err := net.SplitHostPort(ns); err == nil {
|
||||||
|
host, port = h, p
|
||||||
|
}
|
||||||
|
// Only use valid ip:port pair.
|
||||||
|
if _, portErr := strconv.Atoi(port); portErr == nil && port != "0" && net.ParseIP(host) != nil {
|
||||||
|
nss = append(nss, net.JoinHostPort(host, port))
|
||||||
|
} else {
|
||||||
|
ctrld.ProxyLogger.Load().Warn().Msgf("ignoring invalid nameserver for ptr discover: %q", ns)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(nss) > 0 {
|
||||||
|
t.ptr.resolver = ctrld.NewResolverWithNameserver(nss)
|
||||||
|
ctrld.ProxyLogger.Load().Debug().Msgf("using nameservers %v for ptr discovery", nss)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
ctrld.ProxyLogger.Load().Debug().Msg("start ptr discovery")
|
ctrld.ProxyLogger.Load().Debug().Msg("start ptr discovery")
|
||||||
if err := t.ptr.refresh(); err != nil {
|
if err := t.ptr.refresh(); err != nil {
|
||||||
ctrld.ProxyLogger.Load().Error().Err(err).Msg("could not init PTR discover")
|
ctrld.ProxyLogger.Load().Error().Err(err).Msg("could not init PTR discover")
|
||||||
@@ -240,6 +306,21 @@ func (t *Table) LookupHostname(ip, mac string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LookupRFC1918IPv4 returns the RFC1918 IPv4 address for the given MAC address, if any.
|
||||||
|
func (t *Table) LookupRFC1918IPv4(mac string) string {
|
||||||
|
t.initOnce.Do(t.init)
|
||||||
|
for _, r := range t.ipResolvers {
|
||||||
|
ip, err := netip.ParseAddr(r.LookupIP(mac))
|
||||||
|
if err != nil || ip.Is6() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ip.IsPrivate() {
|
||||||
|
return ip.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
type macEntry struct {
|
type macEntry struct {
|
||||||
mac string
|
mac string
|
||||||
src string
|
src string
|
||||||
@@ -288,7 +369,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, t.vni}
|
il := []ipLister{t.dhcp, t.arp, t.ndp, 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]
|
||||||
@@ -338,39 +419,60 @@ func (t *Table) StoreVPNClient(ci *ctrld.ClientInfo) {
|
|||||||
t.vni.ip2name.Store(ci.IP, ci.Hostname)
|
t.vni.ip2name.Store(ci.IP, ci.Hostname)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ipFinder is the interface for retrieving IP address from hostname.
|
||||||
|
type ipFinder interface {
|
||||||
|
lookupIPByHostname(name string, v6 bool) string
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupIPByHostname returns the ip address of given hostname.
|
||||||
|
// If v6 is true, return IPv6 instead of default IPv4.
|
||||||
|
func (t *Table) LookupIPByHostname(hostname string, v6 bool) *netip.Addr {
|
||||||
|
if t == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, finder := range []ipFinder{t.hf, t.ptr, t.mdns, t.dhcp} {
|
||||||
|
if addr := finder.lookupIPByHostname(hostname, v6); addr != "" {
|
||||||
|
if ip, err := netip.ParseAddr(addr); err == nil {
|
||||||
|
return &ip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Table) discoverDHCP() bool {
|
func (t *Table) discoverDHCP() bool {
|
||||||
if t.cfg.Service.DiscoverDHCP == nil {
|
if t.svcCfg.DiscoverDHCP == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return *t.cfg.Service.DiscoverDHCP
|
return *t.svcCfg.DiscoverDHCP
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Table) discoverARP() bool {
|
func (t *Table) discoverARP() bool {
|
||||||
if t.cfg.Service.DiscoverARP == nil {
|
if t.svcCfg.DiscoverARP == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return *t.cfg.Service.DiscoverARP
|
return *t.svcCfg.DiscoverARP
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Table) discoverMDNS() bool {
|
func (t *Table) discoverMDNS() bool {
|
||||||
if t.cfg.Service.DiscoverMDNS == nil {
|
if t.svcCfg.DiscoverMDNS == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return *t.cfg.Service.DiscoverMDNS
|
return *t.svcCfg.DiscoverMDNS
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Table) discoverPTR() bool {
|
func (t *Table) discoverPTR() bool {
|
||||||
if t.cfg.Service.DiscoverPtr == nil {
|
if t.svcCfg.DiscoverPtr == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return *t.cfg.Service.DiscoverPtr
|
return *t.svcCfg.DiscoverPtr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Table) discoverHosts() bool {
|
func (t *Table) discoverHosts() bool {
|
||||||
if t.cfg.Service.DiscoverHosts == nil {
|
if t.svcCfg.DiscoverHosts == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return *t.cfg.Service.DiscoverHosts
|
return *t.svcCfg.DiscoverHosts
|
||||||
}
|
}
|
||||||
|
|
||||||
// normalizeIP normalizes the ip parsed from dnsmasq/dhcpd lease file.
|
// normalizeIP normalizes the ip parsed from dnsmasq/dhcpd lease file.
|
||||||
|
|||||||
@@ -25,3 +25,22 @@ func Test_normalizeIP(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTable_LookupRFC1918IPv4(t *testing.T) {
|
||||||
|
table := &Table{
|
||||||
|
dhcp: &dhcp{},
|
||||||
|
arp: &arpDiscover{},
|
||||||
|
}
|
||||||
|
|
||||||
|
table.ipResolvers = append(table.ipResolvers, table.dhcp)
|
||||||
|
table.ipResolvers = append(table.ipResolvers, table.arp)
|
||||||
|
|
||||||
|
macAddress := "cc:19:f9:8a:49:e6"
|
||||||
|
rfc1918IPv4 := "10.0.10.245"
|
||||||
|
table.dhcp.ip.Store(macAddress, "127.0.0.1")
|
||||||
|
table.arp.ip.Store(macAddress, rfc1918IPv4)
|
||||||
|
|
||||||
|
if got := table.LookupRFC1918IPv4(macAddress); got != rfc1918IPv4 {
|
||||||
|
t.Fatalf("unexpected result, want: %s, got: %s", rfc1918IPv4, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ package clientinfo
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/csv"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@@ -134,6 +136,39 @@ func (d *dhcp) List() []string {
|
|||||||
return ips
|
return ips
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *dhcp) lookupIPByHostname(name string, v6 bool) string {
|
||||||
|
if d == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
rfc1918Addrs []netip.Addr
|
||||||
|
others []netip.Addr
|
||||||
|
)
|
||||||
|
d.ip2name.Range(func(key, value any) bool {
|
||||||
|
if value != name {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if addr, err := netip.ParseAddr(key.(string)); err == nil && addr.Is6() == v6 {
|
||||||
|
if addr.IsPrivate() {
|
||||||
|
rfc1918Addrs = append(rfc1918Addrs, addr)
|
||||||
|
} else {
|
||||||
|
others = append(others, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
result := [][]netip.Addr{rfc1918Addrs, others}
|
||||||
|
for _, addrs := range result {
|
||||||
|
if len(addrs) > 0 {
|
||||||
|
sort.Slice(addrs, func(i, j int) bool {
|
||||||
|
return addrs[i].Less(addrs[j])
|
||||||
|
})
|
||||||
|
return addrs[0].String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// AddLeaseFile adds given lease file for reading/watching clients info.
|
// AddLeaseFile adds given lease file for reading/watching clients info.
|
||||||
func (d *dhcp) addLeaseFile(name string, format ctrld.LeaseFileFormat) error {
|
func (d *dhcp) addLeaseFile(name string, format ctrld.LeaseFileFormat) error {
|
||||||
if d.watcher == nil {
|
if d.watcher == nil {
|
||||||
@@ -153,6 +188,8 @@ func (d *dhcp) readLeaseFile(name string, format ctrld.LeaseFileFormat) error {
|
|||||||
return d.dnsmasqReadClientInfoFile(name)
|
return d.dnsmasqReadClientInfoFile(name)
|
||||||
case ctrld.IscDhcpd:
|
case ctrld.IscDhcpd:
|
||||||
return d.iscDHCPReadClientInfoFile(name)
|
return d.iscDHCPReadClientInfoFile(name)
|
||||||
|
case ctrld.KeaDHCP4:
|
||||||
|
return d.keaDhcp4ReadClientInfoFile(name)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("unsupported format: %s, file: %s", format, name)
|
return fmt.Errorf("unsupported format: %s, file: %s", format, name)
|
||||||
}
|
}
|
||||||
@@ -168,7 +205,8 @@ func (d *dhcp) dnsmasqReadClientInfoFile(name string) error {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// dnsmasqReadClientInfoReader likes ctrld.Dnsmasq, but reading from an io.Reader instead of file.
|
// dnsmasqReadClientInfoReader performs the same task as dnsmasqReadClientInfoFile,
|
||||||
|
// but by reading from an io.Reader instead of file.
|
||||||
func (d *dhcp) dnsmasqReadClientInfoReader(reader io.Reader) error {
|
func (d *dhcp) dnsmasqReadClientInfoReader(reader io.Reader) error {
|
||||||
return lineread.Reader(reader, func(line []byte) error {
|
return lineread.Reader(reader, func(line []byte) error {
|
||||||
fields := bytes.Fields(line)
|
fields := bytes.Fields(line)
|
||||||
@@ -210,7 +248,8 @@ func (d *dhcp) iscDHCPReadClientInfoFile(name string) error {
|
|||||||
return d.iscDHCPReadClientInfoReader(f)
|
return d.iscDHCPReadClientInfoReader(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
// iscDHCPReadClientInfoReader likes ctrld.IscDhcpd, but reading from an io.Reader instead of file.
|
// iscDHCPReadClientInfoReader performs the same task as iscDHCPReadClientInfoFile,
|
||||||
|
// but by reading from an io.Reader instead of file.
|
||||||
func (d *dhcp) iscDHCPReadClientInfoReader(reader io.Reader) error {
|
func (d *dhcp) iscDHCPReadClientInfoReader(reader io.Reader) error {
|
||||||
s := bufio.NewScanner(reader)
|
s := bufio.NewScanner(reader)
|
||||||
var ip, mac, hostname string
|
var ip, mac, hostname string
|
||||||
@@ -253,6 +292,58 @@ func (d *dhcp) iscDHCPReadClientInfoReader(reader io.Reader) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// keaDhcp4ReadClientInfoFile populates dhcp table with client info reading from kea dhcp4 lease file.
|
||||||
|
func (d *dhcp) keaDhcp4ReadClientInfoFile(name string) error {
|
||||||
|
f, err := os.Open(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
return d.keaDhcp4ReadClientInfoReader(bufio.NewReader(f))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// keaDhcp4ReadClientInfoReader performs the same task as keaDhcp4ReadClientInfoFile,
|
||||||
|
// but by reading from an io.Reader instead of file.
|
||||||
|
func (d *dhcp) keaDhcp4ReadClientInfoReader(r io.Reader) error {
|
||||||
|
cr := csv.NewReader(r)
|
||||||
|
for {
|
||||||
|
record, err := cr.Read()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(record) < 9 {
|
||||||
|
continue // hostname is at 9th field, so skipping record with not enough fields.
|
||||||
|
}
|
||||||
|
if record[0] == "address" {
|
||||||
|
continue // skip header.
|
||||||
|
}
|
||||||
|
mac := record[1]
|
||||||
|
if _, err := net.ParseMAC(mac); err != nil { // skip invalid MAC
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ip := normalizeIP(record[0])
|
||||||
|
if net.ParseIP(ip) == nil {
|
||||||
|
ctrld.ProxyLogger.Load().Warn().Msgf("invalid ip address entry: %q", ip)
|
||||||
|
ip = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
d.mac.Store(ip, mac)
|
||||||
|
d.ip.Store(mac, ip)
|
||||||
|
hostname := record[8]
|
||||||
|
if hostname == "*" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := normalizeHostname(hostname)
|
||||||
|
d.mac2name.Store(mac, name)
|
||||||
|
d.ip2name.Store(ip, name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// addSelf populates current host info to dhcp, so queries from
|
// addSelf populates current host info to dhcp, so queries from
|
||||||
// the host itself can be attached with proper client info.
|
// the host itself can be attached with proper client info.
|
||||||
func (d *dhcp) addSelf() {
|
func (d *dhcp) addSelf() {
|
||||||
|
|||||||
@@ -15,4 +15,5 @@ var clientInfoFiles = map[string]ctrld.LeaseFileFormat{
|
|||||||
"/run/dhcpd.leases": ctrld.IscDhcpd, // EdgeOS
|
"/run/dhcpd.leases": ctrld.IscDhcpd, // EdgeOS
|
||||||
"/var/dhcpd/var/db/dhcpd.leases": ctrld.IscDhcpd, // Pfsense
|
"/var/dhcpd/var/db/dhcpd.leases": ctrld.IscDhcpd, // Pfsense
|
||||||
"/home/pi/.router/run/dhcp/dnsmasq.leases": ctrld.Dnsmasq, // Firewalla
|
"/home/pi/.router/run/dhcp/dnsmasq.leases": ctrld.Dnsmasq, // Firewalla
|
||||||
|
"/var/lib/kea/dhcp4.leases": ctrld.KeaDHCP4, // Pfsense
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,41 @@ lease 192.168.1.2 {
|
|||||||
"00:00:00:00:00:04",
|
"00:00:00:00:00:04",
|
||||||
"example",
|
"example",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kea-dhcp4 good",
|
||||||
|
`address,hwaddr,client_id,valid_lifetime,expire,subnet_id,fqdn_fwd,fqdn_rev,hostname,state,user_context,pool_id
|
||||||
|
192.168.0.123,00:00:00:00:00:05,00:00:00:00:00:05,7200,1703290639,1,0,0,foo,0,,0
|
||||||
|
`,
|
||||||
|
d.keaDhcp4ReadClientInfoReader,
|
||||||
|
"00:00:00:00:00:05",
|
||||||
|
"foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kea-dhcp4 no-header",
|
||||||
|
`192.168.0.123,00:00:00:00:00:05,00:00:00:00:00:05,7200,1703290639,1,0,0,foo,0,,0`,
|
||||||
|
d.keaDhcp4ReadClientInfoReader,
|
||||||
|
"00:00:00:00:00:05",
|
||||||
|
"foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kea-dhcp4 hostname *",
|
||||||
|
`address,hwaddr,client_id,valid_lifetime,expire,subnet_id,fqdn_fwd,fqdn_rev,hostname,state,user_context,pool_id
|
||||||
|
192.168.0.123,00:00:00:00:00:05,00:00:00:00:00:05,7200,1703290639,1,0,0,*,0,,0
|
||||||
|
`,
|
||||||
|
d.keaDhcp4ReadClientInfoReader,
|
||||||
|
"00:00:00:00:00:05",
|
||||||
|
"*",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kea-dhcp4 bad",
|
||||||
|
`address,hwaddr,client_id,valid_lifetime,expire,subnet_id,fqdn_fwd,fqdn_rev,hostname,state,user_context,pool_id
|
||||||
|
192.168.0.123,00:00:00:00:00:05,00:00:00:00:00:05,7200,1703290639,1,0,0,foo,0,,0
|
||||||
|
192.168.0.124,invalid_MAC,00:00:00:00:00:05,7200,1703290639,1,0,0,foo,0,,0
|
||||||
|
`,
|
||||||
|
d.keaDhcp4ReadClientInfoReader,
|
||||||
|
"00:00:00:00:00:05",
|
||||||
|
"foo",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
@@ -76,6 +111,12 @@ lease 192.168.1.2 {
|
|||||||
t.Errorf("readClientInfoReader() error = %v", err)
|
t.Errorf("readClientInfoReader() error = %v", err)
|
||||||
}
|
}
|
||||||
val, existed := d.mac2name.Load(tc.mac)
|
val, existed := d.mac2name.Load(tc.mac)
|
||||||
|
if tc.hostname == "*" {
|
||||||
|
if existed {
|
||||||
|
t.Errorf("* hostname must be skipped")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
if !existed {
|
if !existed {
|
||||||
t.Error("client info missing")
|
t.Error("client info missing")
|
||||||
}
|
}
|
||||||
@@ -86,3 +127,15 @@ lease 192.168.1.2 {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_dhcp_lookupIPByHostname(t *testing.T) {
|
||||||
|
d := &dhcp{}
|
||||||
|
want := "192.168.1.123"
|
||||||
|
d.ip2name.Store(want, "foo")
|
||||||
|
d.ip2name.Store("127.0.0.1", "foo")
|
||||||
|
d.ip2name.Store("169.254.123.123", "foo")
|
||||||
|
|
||||||
|
if got := d.lookupIPByHostname("foo", false); got != want {
|
||||||
|
t.Fatalf("unexpected result, want: %s, got: %s", want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
package clientinfo
|
package clientinfo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/fsnotify/fsnotify"
|
"github.com/fsnotify/fsnotify"
|
||||||
@@ -11,9 +16,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ipv4LocalhostName = "localhost"
|
ipv4LocalhostName = "localhost"
|
||||||
ipv6LocalhostName = "ip6-localhost"
|
ipv6LocalhostName = "ip6-localhost"
|
||||||
ipv6LoopbackName = "ip6-loopback"
|
ipv6LoopbackName = "ip6-loopback"
|
||||||
|
hostEntriesConfPath = "/var/unbound/host_entries.conf"
|
||||||
)
|
)
|
||||||
|
|
||||||
// hostsFile provides client discovery functionality using system hosts file.
|
// hostsFile provides client discovery functionality using system hosts file.
|
||||||
@@ -33,14 +39,9 @@ func (hf *hostsFile) init() error {
|
|||||||
if err := hf.watcher.Add(hostsfile.HostsPath); err != nil {
|
if err := hf.watcher.Add(hostsfile.HostsPath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
m, err := hostsfile.ParseHosts(hostsfile.ReadHostsFile())
|
// Conservatively adding hostEntriesConfPath, since it is not available everywhere.
|
||||||
if err != nil {
|
_ = hf.watcher.Add(hostEntriesConfPath)
|
||||||
return err
|
return hf.refresh()
|
||||||
}
|
|
||||||
hf.mu.Lock()
|
|
||||||
hf.m = m
|
|
||||||
hf.mu.Unlock()
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// refresh reloads hosts file entries.
|
// refresh reloads hosts file entries.
|
||||||
@@ -51,6 +52,14 @@ func (hf *hostsFile) refresh() error {
|
|||||||
}
|
}
|
||||||
hf.mu.Lock()
|
hf.mu.Lock()
|
||||||
hf.m = m
|
hf.m = m
|
||||||
|
// override hosts file with host_entries.conf content if present.
|
||||||
|
hem, err := parseHostEntriesConf(hostEntriesConfPath)
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
ctrld.ProxyLogger.Load().Debug().Err(err).Msg("could not read host_entries.conf file")
|
||||||
|
}
|
||||||
|
for k, v := range hem {
|
||||||
|
hf.m[k] = v
|
||||||
|
}
|
||||||
hf.mu.Unlock()
|
hf.mu.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -109,6 +118,24 @@ func (hf *hostsFile) String() string {
|
|||||||
return "hosts"
|
return "hosts"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (hf *hostsFile) lookupIPByHostname(name string, v6 bool) string {
|
||||||
|
if hf == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
hf.mu.Lock()
|
||||||
|
defer hf.mu.Unlock()
|
||||||
|
for addr, names := range hf.m {
|
||||||
|
if ip, err := netip.ParseAddr(addr); err == nil && !ip.IsLoopback() {
|
||||||
|
for _, n := range names {
|
||||||
|
if n == name && ip.Is6() == v6 {
|
||||||
|
return ip.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// isLocalhostName reports whether the given hostname represents localhost.
|
// isLocalhostName reports whether the given hostname represents localhost.
|
||||||
func isLocalhostName(hostname string) bool {
|
func isLocalhostName(hostname string) bool {
|
||||||
switch hostname {
|
switch hostname {
|
||||||
@@ -118,3 +145,46 @@ func isLocalhostName(hostname string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseHostEntriesConf parses host_entries.conf file and returns parsed result.
|
||||||
|
func parseHostEntriesConf(path string) (map[string][]string, error) {
|
||||||
|
b, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return parseHostEntriesConfFromReader(bytes.NewReader(b)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseHostEntriesConfFromReader is like parseHostEntriesConf, but read from an io.Reader instead of file.
|
||||||
|
func parseHostEntriesConfFromReader(r io.Reader) map[string][]string {
|
||||||
|
hostsMap := map[string][]string{}
|
||||||
|
scanner := bufio.NewScanner(r)
|
||||||
|
|
||||||
|
localZone := ""
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if after, found := strings.CutPrefix(line, "local-zone:"); found {
|
||||||
|
after = strings.TrimSpace(after)
|
||||||
|
fields := strings.Fields(after)
|
||||||
|
if len(fields) > 1 {
|
||||||
|
localZone = strings.Trim(fields[0], `"`)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Only read "local-data-ptr: ..." line, it has all necessary information.
|
||||||
|
after, found := strings.CutPrefix(line, "local-data-ptr:")
|
||||||
|
if !found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
after = strings.TrimSpace(after)
|
||||||
|
after = strings.Trim(after, `"`)
|
||||||
|
fields := strings.Fields(after)
|
||||||
|
if len(fields) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ip := fields[0]
|
||||||
|
name := strings.TrimSuffix(fields[1], "."+localZone)
|
||||||
|
hostsMap[ip] = append(hostsMap[ip], name)
|
||||||
|
}
|
||||||
|
return hostsMap
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package clientinfo
|
package clientinfo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -31,3 +32,46 @@ func Test_hostsFile_LookupHostnameByIP(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_parseHostEntriesConfFromReader(t *testing.T) {
|
||||||
|
const content = `local-zone: "localdomain" transparent
|
||||||
|
local-data-ptr: "127.0.0.1 localhost"
|
||||||
|
local-data: "localhost A 127.0.0.1"
|
||||||
|
local-data: "localhost.localdomain A 127.0.0.1"
|
||||||
|
local-data-ptr: "::1 localhost"
|
||||||
|
local-data: "localhost AAAA ::1"
|
||||||
|
local-data: "localhost.localdomain AAAA ::1"
|
||||||
|
local-data-ptr: "10.0.10.227 OPNsense.localdomain"
|
||||||
|
local-data: "OPNsense.localdomain A 10.0.10.227"
|
||||||
|
local-data: "OPNsense A 10.0.10.227"
|
||||||
|
local-data-ptr: "fe80::5a78:4e29:caa3:f9f7 OPNsense.localdomain"
|
||||||
|
local-data: "OPNsense.localdomain AAAA fe80::5a78:4e29:caa3:f9f7"
|
||||||
|
local-data: "OPNsense AAAA fe80::5a78:4e29:caa3:f9f7"
|
||||||
|
local-data-ptr: "1.1.1.1 banana-party.local.com"
|
||||||
|
local-data: "banana-party.local.com IN A 1.1.1.1"
|
||||||
|
local-data-ptr: "1.1.1.1 cheese-land.lan"
|
||||||
|
local-data: "cheese-land.lan IN A 1.1.1.1"
|
||||||
|
`
|
||||||
|
r := strings.NewReader(content)
|
||||||
|
hostsMap := parseHostEntriesConfFromReader(r)
|
||||||
|
if len(hostsMap) != 5 {
|
||||||
|
t.Fatalf("unexpected number of entries, want 5, got: %d", len(hostsMap))
|
||||||
|
}
|
||||||
|
for ip, names := range hostsMap {
|
||||||
|
switch ip {
|
||||||
|
case "1.1.1.1":
|
||||||
|
for _, name := range names {
|
||||||
|
if name != "banana-party.local.com" && name != "cheese-land.lan" {
|
||||||
|
t.Fatalf("unexpected names for 1.1.1.1: %v", names)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "10.0.10.227":
|
||||||
|
if len(names) != 1 {
|
||||||
|
t.Fatalf("unexpected names for 10.0.10.227: %v", names)
|
||||||
|
}
|
||||||
|
if names[0] != "OPNsense" {
|
||||||
|
t.Fatalf("unexpected name: %s", names[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
package clientinfo
|
package clientinfo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
@@ -59,6 +65,27 @@ func (m *mdns) List() []string {
|
|||||||
return ips
|
return ips
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mdns) lookupIPByHostname(name string, v6 bool) string {
|
||||||
|
if m == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var ip string
|
||||||
|
m.name.Range(func(key, value any) bool {
|
||||||
|
if value == name {
|
||||||
|
if addr, err := netip.ParseAddr(key.(string)); err == nil && addr.Is6() == v6 {
|
||||||
|
ip = addr.String()
|
||||||
|
//lint:ignore S1008 This is used for readable.
|
||||||
|
if addr.IsLoopback() { // Continue searching if this is loopback address.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
func (m *mdns) init(quitCh chan struct{}) error {
|
func (m *mdns) init(quitCh chan struct{}) error {
|
||||||
ifaces, err := multicastInterfaces()
|
ifaces, err := multicastInterfaces()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -85,6 +112,7 @@ func (m *mdns) init(quitCh chan struct{}) error {
|
|||||||
|
|
||||||
go m.probeLoop(v4ConnList, mdnsV4Addr, quitCh)
|
go m.probeLoop(v4ConnList, mdnsV4Addr, quitCh)
|
||||||
go m.probeLoop(v6ConnList, mdnsV6Addr, quitCh)
|
go m.probeLoop(v6ConnList, mdnsV6Addr, quitCh)
|
||||||
|
go m.getDataFromAvahiDaemonCache()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -123,6 +151,10 @@ func (m *mdns) readLoop(conn *net.UDPConn) {
|
|||||||
if err, ok := err.(*net.OpError); ok && (err.Timeout() || err.Temporary()) {
|
if err, ok := err.(*net.OpError); ok && (err.Timeout() || err.Temporary()) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// Do not complain about use of closed network connection.
|
||||||
|
if errors.Is(err, net.ErrClosed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
ctrld.ProxyLogger.Load().Debug().Err(err).Msg("mdns readLoop error")
|
ctrld.ProxyLogger.Load().Debug().Err(err).Msg("mdns readLoop error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -186,6 +218,44 @@ func (m *mdns) probe(conns []*net.UDPConn, remoteAddr net.Addr) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getDataFromAvahiDaemonCache reads entries from avahi-daemon cache to update mdns data.
|
||||||
|
func (m *mdns) getDataFromAvahiDaemonCache() {
|
||||||
|
if _, err := exec.LookPath("avahi-browse"); err != nil {
|
||||||
|
ctrld.ProxyLogger.Load().Debug().Err(err).Msg("could not find avahi-browse binary, skipping.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Run avahi-browse to discover services from cache:
|
||||||
|
// - "-a" -> all services.
|
||||||
|
// - "-r" -> resolve found services.
|
||||||
|
// - "-p" -> parseable format.
|
||||||
|
// - "-c" -> read from cache.
|
||||||
|
out, err := exec.Command("avahi-browse", "-a", "-r", "-p", "-c").Output()
|
||||||
|
if err != nil {
|
||||||
|
ctrld.ProxyLogger.Load().Debug().Err(err).Msg("could not browse services from avahi cache")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.storeDataFromAvahiBrowseOutput(bytes.NewReader(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
// storeDataFromAvahiBrowseOutput parses avahi-browse output from reader, then updating found data to mdns table.
|
||||||
|
func (m *mdns) storeDataFromAvahiBrowseOutput(r io.Reader) {
|
||||||
|
scanner := bufio.NewScanner(r)
|
||||||
|
for scanner.Scan() {
|
||||||
|
fields := strings.FieldsFunc(scanner.Text(), func(r rune) bool {
|
||||||
|
return r == ';'
|
||||||
|
})
|
||||||
|
if len(fields) < 8 || fields[0] != "=" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ip := fields[7]
|
||||||
|
name := normalizeHostname(fields[6])
|
||||||
|
// Only using cache value if we don't have existed one.
|
||||||
|
if _, loaded := m.name.LoadOrStore(ip, name); !loaded {
|
||||||
|
ctrld.ProxyLogger.Load().Debug().Msgf("found hostname: %q, ip: %q via avahi cache", name, ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func multicastInterfaces() ([]net.Interface, error) {
|
func multicastInterfaces() ([]net.Interface, error) {
|
||||||
ifaces, err := net.Interfaces()
|
ifaces, err := net.Interfaces()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package clientinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_mdns_storeDataFromAvahiBrowseOutput(t *testing.T) {
|
||||||
|
const content = `+;wlp0s20f3;IPv6;Foo\032\0402\041;_companion-link._tcp;local
|
||||||
|
+;wlp0s20f3;IPv4;Foo\032\0402\041;_companion-link._tcp;local
|
||||||
|
=;wlp0s20f3;IPv6;Foo\032\0402\041;_companion-link._tcp;local;Foo-2.local;192.168.1.123;64842;"rpBA=00:00:00:00:00:01" "rpHI=e6ae2cbbca0e" "rpAD=36566f4d850f" "rpVr=510.71.1" "rpHA=0ddc20fdddc8" "rpFl=0x30000" "rpHN=1d4a03afdefa" "rpMac=0"
|
||||||
|
=;wlp0s20f3;IPv4;Foo\032\0402\041;_companion-link._tcp;local;Foo-2.local;192.168.1.123;64842;"rpBA=00:00:00:00:00:01" "rpHI=e6ae2cbbca0e" "rpAD=36566f4d850f" "rpVr=510.71.1" "rpHA=0ddc20fdddc8" "rpFl=0x30000" "rpHN=1d4a03afdefa" "rpMac=0"
|
||||||
|
`
|
||||||
|
m := &mdns{}
|
||||||
|
m.storeDataFromAvahiBrowseOutput(strings.NewReader(content))
|
||||||
|
ip := "192.168.1.123"
|
||||||
|
val, loaded := m.name.LoadOrStore(ip, "")
|
||||||
|
if !loaded {
|
||||||
|
t.Fatal("missing Foo-2 data from mdns table")
|
||||||
|
}
|
||||||
|
|
||||||
|
wantHostname := "Foo-2"
|
||||||
|
hostname := val.(string)
|
||||||
|
if hostname != wantHostname {
|
||||||
|
t.Fatalf("unexpected hostname, want: %q, got: %q", wantHostname, hostname)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
package clientinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mdlayher/ndp"
|
||||||
|
|
||||||
|
"github.com/Control-D-Inc/ctrld"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ndpDiscover provides client discovery functionality using NDP protocol.
|
||||||
|
type ndpDiscover struct {
|
||||||
|
mac sync.Map // ip => mac
|
||||||
|
ip sync.Map // mac => ip
|
||||||
|
}
|
||||||
|
|
||||||
|
// refresh re-scans the NDP table.
|
||||||
|
func (nd *ndpDiscover) refresh() error {
|
||||||
|
nd.scan()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupIP returns the ipv6 associated with the input MAC address.
|
||||||
|
func (nd *ndpDiscover) LookupIP(mac string) string {
|
||||||
|
val, ok := nd.ip.Load(mac)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return val.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupMac returns the MAC address of the given IP address.
|
||||||
|
func (nd *ndpDiscover) LookupMac(ip string) string {
|
||||||
|
val, ok := nd.mac.Load(ip)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return val.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns human-readable format of ndpDiscover.
|
||||||
|
func (nd *ndpDiscover) String() string {
|
||||||
|
return "ndp"
|
||||||
|
}
|
||||||
|
|
||||||
|
// List returns all known IP addresses.
|
||||||
|
func (nd *ndpDiscover) List() []string {
|
||||||
|
if nd == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var ips []string
|
||||||
|
nd.ip.Range(func(key, value any) bool {
|
||||||
|
ips = append(ips, value.(string))
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
nd.mac.Range(func(key, value any) bool {
|
||||||
|
ips = append(ips, key.(string))
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return ips
|
||||||
|
}
|
||||||
|
|
||||||
|
// listen listens on ipv6 link local for Neighbor Solicitation message
|
||||||
|
// to update new neighbors information to ndp table.
|
||||||
|
func (nd *ndpDiscover) listen(ctx context.Context) {
|
||||||
|
ifi, err := firstInterfaceWithV6LinkLocal()
|
||||||
|
if err != nil {
|
||||||
|
ctrld.ProxyLogger.Load().Debug().Err(err).Msg("failed to find valid ipv6")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c, ip, err := ndp.Listen(ifi, ndp.LinkLocal)
|
||||||
|
if err != nil {
|
||||||
|
ctrld.ProxyLogger.Load().Debug().Err(err).Msg("ndp listen failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
ctrld.ProxyLogger.Load().Debug().Msgf("listening ndp on: %s", ip.String())
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
_ = c.SetReadDeadline(time.Now().Add(30 * time.Second))
|
||||||
|
msg, _, from, readErr := c.ReadFrom()
|
||||||
|
if readErr != nil {
|
||||||
|
var opErr *net.OpError
|
||||||
|
if errors.As(readErr, &opErr) && (opErr.Timeout() || opErr.Temporary()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ctrld.ProxyLogger.Load().Debug().Err(readErr).Msg("ndp read loop error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only looks for neighbor solicitation message, since new clients
|
||||||
|
// which join network will broadcast this message to us.
|
||||||
|
am, ok := msg.(*ndp.NeighborSolicitation)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fromIP := from.String()
|
||||||
|
for _, opt := range am.Options {
|
||||||
|
if lla, ok := opt.(*ndp.LinkLayerAddress); ok {
|
||||||
|
mac := lla.Addr.String()
|
||||||
|
nd.mac.Store(fromIP, mac)
|
||||||
|
nd.ip.Store(mac, fromIP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanWindows populates NDP table using information from "netsh" command.
|
||||||
|
func (nd *ndpDiscover) scanWindows(r io.Reader) {
|
||||||
|
scanner := bufio.NewScanner(r)
|
||||||
|
for scanner.Scan() {
|
||||||
|
fields := strings.Fields(scanner.Text())
|
||||||
|
if len(fields) < 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if mac := parseMAC(fields[1]); mac != "" {
|
||||||
|
nd.mac.Store(fields[0], mac)
|
||||||
|
nd.ip.Store(mac, fields[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanUnix populates NDP table using information from "ndp" command.
|
||||||
|
func (nd *ndpDiscover) scanUnix(r io.Reader) {
|
||||||
|
scanner := bufio.NewScanner(r)
|
||||||
|
scanner.Scan() // skip header
|
||||||
|
for scanner.Scan() {
|
||||||
|
fields := strings.Fields(scanner.Text())
|
||||||
|
if len(fields) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if mac := parseMAC(fields[1]); mac != "" {
|
||||||
|
ip := fields[0]
|
||||||
|
if idx := strings.IndexByte(ip, '%'); idx != -1 {
|
||||||
|
ip = ip[:idx]
|
||||||
|
}
|
||||||
|
nd.mac.Store(ip, mac)
|
||||||
|
nd.ip.Store(mac, ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalizeMac ensure the given MAC address have the proper format
|
||||||
|
// before being parsed.
|
||||||
|
//
|
||||||
|
// Example, changing "00:0:00:0:00:01" to "00:00:00:00:00:01", which
|
||||||
|
// can be seen on Darwin.
|
||||||
|
func normalizeMac(mac string) string {
|
||||||
|
if len(mac) == 17 {
|
||||||
|
return mac
|
||||||
|
}
|
||||||
|
// Windows use "-" instead of ":" as separator.
|
||||||
|
mac = strings.ReplaceAll(mac, "-", ":")
|
||||||
|
parts := strings.Split(mac, ":")
|
||||||
|
if len(parts) != 6 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
for i, c := range parts {
|
||||||
|
if len(c) == 1 {
|
||||||
|
parts[i] = "0" + c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(parts, ":")
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseMAC parses the input MAC, doing normalization,
|
||||||
|
// and return the result after calling net.ParseMac function.
|
||||||
|
func parseMAC(mac string) string {
|
||||||
|
hw, _ := net.ParseMAC(normalizeMac(mac))
|
||||||
|
return hw.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// firstInterfaceWithV6LinkLocal returns the first interface which is capable of using NDP.
|
||||||
|
func firstInterfaceWithV6LinkLocal() (*net.Interface, error) {
|
||||||
|
ifis, err := net.Interfaces()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ifi := range ifis {
|
||||||
|
// Skip if iface is down/loopback/non-multicast.
|
||||||
|
if ifi.Flags&net.FlagUp == 0 || ifi.Flags&net.FlagLoopback != 0 || ifi.Flags&net.FlagMulticast == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
addrs, err := ifi.Addrs()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, addr := range addrs {
|
||||||
|
ipNet, ok := addr.(*net.IPNet)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ip, ok := netip.AddrFromSlice(ipNet.IP)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid ip address: %s", ipNet.String())
|
||||||
|
}
|
||||||
|
if ip.Is6() && !ip.Is4In6() {
|
||||||
|
return &ifi, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New("no interface can be used")
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package clientinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vishvananda/netlink"
|
||||||
|
|
||||||
|
"github.com/Control-D-Inc/ctrld"
|
||||||
|
)
|
||||||
|
|
||||||
|
// scan populates NDP table using information from system mappings.
|
||||||
|
func (nd *ndpDiscover) scan() {
|
||||||
|
neighs, err := netlink.NeighList(0, netlink.FAMILY_V6)
|
||||||
|
if err != nil {
|
||||||
|
ctrld.ProxyLogger.Load().Warn().Err(err).Msg("could not get neigh list")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, n := range neighs {
|
||||||
|
ip := n.IP.String()
|
||||||
|
mac := n.HardwareAddr.String()
|
||||||
|
nd.mac.Store(ip, mac)
|
||||||
|
nd.ip.Store(mac, ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
//go:build !linux
|
||||||
|
|
||||||
|
package clientinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/Control-D-Inc/ctrld"
|
||||||
|
)
|
||||||
|
|
||||||
|
// scan populates NDP table using information from system mappings.
|
||||||
|
func (nd *ndpDiscover) scan() {
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
data, err := exec.Command("netsh", "interface", "ipv6", "show", "neighbors").Output()
|
||||||
|
if err != nil {
|
||||||
|
ctrld.ProxyLogger.Load().Warn().Err(err).Msg("could not query ndp table")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nd.scanWindows(bytes.NewReader(data))
|
||||||
|
default:
|
||||||
|
data, err := exec.Command("ndp", "-an").Output()
|
||||||
|
if err != nil {
|
||||||
|
ctrld.ProxyLogger.Load().Warn().Err(err).Msg("could not query ndp table")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nd.scanUnix(bytes.NewReader(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package clientinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_ndpDiscover_scanUnix(t *testing.T) {
|
||||||
|
r := strings.NewReader(`Neighbor Linklayer Address Netif Expire St Flgs Prbs
|
||||||
|
2405:4802:1f90:fda0:1459:ec89:523d:3583 00:0:00:0:00:01 en0 permanent R
|
||||||
|
2405:4802:1f90:fda0:186b:c54a:1370:c196 (incomplete) en0 expired N
|
||||||
|
2405:4802:1f90:fda0:88de:14ef:6a8c:579a 00:0:00:0:00:02 en0 permanent R
|
||||||
|
fe80::1%lo0 (incomplete) lo0 permanent R
|
||||||
|
`)
|
||||||
|
nd := &ndpDiscover{}
|
||||||
|
nd.scanUnix(r)
|
||||||
|
|
||||||
|
for _, m := range []*sync.Map{&nd.mac, &nd.ip} {
|
||||||
|
count := 0
|
||||||
|
m.Range(func(key, value any) bool {
|
||||||
|
count++
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if count != 2 {
|
||||||
|
t.Errorf("unexpected count, want 2, got: %d", count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_ndpDiscover_scanWindows(t *testing.T) {
|
||||||
|
r := strings.NewReader(`Interface 14: Wi-Fi
|
||||||
|
|
||||||
|
|
||||||
|
Internet Address Physical Address Type
|
||||||
|
-------------------------------------------- ----------------- -----------
|
||||||
|
2405:4802:1f90:fda0:ffff:ffff:ffff:ff88 00-00-00-00-00-00 Unreachable
|
||||||
|
fe80::1 60-57-47-21-dd-00 Reachable (Router)
|
||||||
|
fe80::6257:47ff:fe21:dd00 60-57-47-21-dd-00 Reachable (Router)
|
||||||
|
ff02::1 33-33-00-00-00-01 Permanent
|
||||||
|
ff02::2 33-33-00-00-00-02 Permanent
|
||||||
|
ff02::c 33-33-00-00-00-0c Permanent
|
||||||
|
`)
|
||||||
|
nd := &ndpDiscover{}
|
||||||
|
nd.scanWindows(r)
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
nd.mac.Range(func(key, value any) bool {
|
||||||
|
count++
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if count != 6 {
|
||||||
|
t.Errorf("unexpected count, want 6, got: %d", count)
|
||||||
|
}
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
nd.ip.Range(func(key, value any) bool {
|
||||||
|
count++
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if count != 5 {
|
||||||
|
t.Errorf("unexpected count, want 5, got: %d", count)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package clientinfo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net/netip"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
@@ -72,15 +73,16 @@ func (p *ptrDiscover) lookupHostname(ip string) string {
|
|||||||
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().Warn().Str("discovery", "ptr").Err(err).Msg("invalid ip address")
|
ctrld.ProxyLogger.Load().Info().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().Warn().Str("discovery", "ptr").Err(err).Msg("could not perform PTR lookup")
|
if p.serverDown.CompareAndSwap(false, true) {
|
||||||
p.serverDown.Store(true)
|
ctrld.ProxyLogger.Load().Info().Str("discovery", "ptr").Err(err).Msg("could not perform PTR lookup")
|
||||||
go p.checkServer()
|
go p.checkServer()
|
||||||
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
for _, rr := range ans.Answer {
|
for _, rr := range ans.Answer {
|
||||||
@@ -93,6 +95,27 @@ func (p *ptrDiscover) lookupHostname(ip string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *ptrDiscover) lookupIPByHostname(name string, v6 bool) string {
|
||||||
|
if p == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var ip string
|
||||||
|
p.hostname.Range(func(key, value any) bool {
|
||||||
|
if value == name {
|
||||||
|
if addr, err := netip.ParseAddr(key.(string)); err == nil && addr.Is6() == v6 {
|
||||||
|
ip = addr.String()
|
||||||
|
//lint:ignore S1008 This is used for readable.
|
||||||
|
if addr.IsLoopback() { // Continue searching if this is loopback address.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
// checkServer monitors if the resolver can reach its nameserver. When the nameserver
|
// 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.
|
// is reachable, set p.serverDown to false, so p.lookupHostname can continue working.
|
||||||
func (p *ptrDiscover) checkServer() {
|
func (p *ptrDiscover) checkServer() {
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package clientinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/router"
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/router/ubios"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ubiosDiscover provides client discovery functionality on Ubios routers.
|
||||||
|
type ubiosDiscover struct {
|
||||||
|
hostname sync.Map // mac => hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
// refresh reloads unifi devices from database.
|
||||||
|
func (u *ubiosDiscover) refresh() error {
|
||||||
|
if router.Name() != ubios.Name {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return u.refreshDevices()
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupHostnameByIP returns hostname for given IP.
|
||||||
|
func (u *ubiosDiscover) LookupHostnameByIP(ip string) string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupHostnameByMac returns unifi device custom hostname for the given MAC address.
|
||||||
|
func (u *ubiosDiscover) LookupHostnameByMac(mac string) string {
|
||||||
|
val, ok := u.hostname.Load(mac)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return val.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// refreshDevices updates unifi devices name from local mongodb.
|
||||||
|
func (u *ubiosDiscover) refreshDevices() error {
|
||||||
|
cmd := exec.Command("/usr/bin/mongo", "localhost:27117/ace", "--quiet", "--eval", `
|
||||||
|
DBQuery.shellBatchSize = 256;
|
||||||
|
db.user.find({name: {$exists: true, $ne: ""}}, {_id:0, mac:1, name:1});`)
|
||||||
|
b, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return u.storeDevices(bytes.NewReader(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
// storeDevices saves unifi devices name for caching.
|
||||||
|
func (u *ubiosDiscover) storeDevices(r io.Reader) error {
|
||||||
|
decoder := json.NewDecoder(r)
|
||||||
|
device := struct {
|
||||||
|
MAC string
|
||||||
|
Name string
|
||||||
|
}{}
|
||||||
|
for {
|
||||||
|
err := decoder.Decode(&device)
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
mac := strings.ToLower(device.MAC)
|
||||||
|
u.hostname.Store(mac, normalizeHostname(device.Name))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns human-readable format of ubiosDiscover.
|
||||||
|
func (u *ubiosDiscover) String() string {
|
||||||
|
return "ubios"
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package clientinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_ubiosDiscover_storeDevices(t *testing.T) {
|
||||||
|
ud := &ubiosDiscover{}
|
||||||
|
r := strings.NewReader(`{ "mac": "00:00:00:00:00:01", "name": "device 1" }
|
||||||
|
{ "mac": "00:00:00:00:00:02", "name": "device 2" }
|
||||||
|
`)
|
||||||
|
if err := ud.storeDevices(r); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
mac string
|
||||||
|
hostname string
|
||||||
|
}{
|
||||||
|
{"device 1", "00:00:00:00:00:01", "device 1"},
|
||||||
|
{"device 2", "00:00:00:00:00:02", "device 2"},
|
||||||
|
{"non-existed", "00:00:00:00:00:03", ""},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if got := ud.LookupHostnameByMac(tc.mac); got != tc.hostname {
|
||||||
|
t.Errorf("hostname mismatched, want: %q, got: %q", tc.hostname, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test for invalid input.
|
||||||
|
r = strings.NewReader(`{ "mac": "00:00:00:00:00:01", "name": "device 1"`)
|
||||||
|
if err := ud.storeDevices(r); err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
} else {
|
||||||
|
t.Log(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -119,7 +120,7 @@ func postUtilityAPI(version string, cdDev bool, body io.Reader) (*ResolverConfig
|
|||||||
return d.DialContext(ctx, network, addrs)
|
return d.DialContext(ctx, network, addrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
if router.Name() == ddwrt.Name {
|
if router.Name() == ddwrt.Name || runtime.GOOS == "android" {
|
||||||
transport.TLSClientConfig = &tls.Config{RootCAs: certs.CACertPool()}
|
transport.TLSClientConfig = &tls.Config{RootCAs: certs.CACertPool()}
|
||||||
}
|
}
|
||||||
client := http.Client{
|
client := http.Client{
|
||||||
|
|||||||
+2
-2
@@ -16,8 +16,8 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
controldIPv6Test = "ipv6.controld.io"
|
controldIPv6Test = "ipv6.controld.io"
|
||||||
v4BootstrapDNS = "76.76.2.0:53"
|
v4BootstrapDNS = "76.76.2.22:53"
|
||||||
v6BootstrapDNS = "[2606:1a40::]:53"
|
v6BootstrapDNS = "[2606:1a40::22]:53"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Dialer = &net.Dialer{
|
var Dialer = &net.Dialer{
|
||||||
|
|||||||
@@ -15,9 +15,12 @@ no-resolv
|
|||||||
{{- range .Upstreams}}
|
{{- range .Upstreams}}
|
||||||
server={{ .IP }}#{{ .Port }}
|
server={{ .IP }}#{{ .Port }}
|
||||||
{{- end}}
|
{{- end}}
|
||||||
{{- if .SendClientInfo}}
|
|
||||||
add-mac
|
add-mac
|
||||||
add-subnet=32,128
|
add-subnet=32,128
|
||||||
|
{{- if .CacheDisabled}}
|
||||||
|
cache-size=0
|
||||||
|
{{- else}}
|
||||||
|
max-cache-ttl=0
|
||||||
{{- end}}
|
{{- end}}
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -39,14 +42,14 @@ if [ -n "$pid" ] && [ -f "/proc/${pid}/cmdline" ]; then
|
|||||||
{{- range .Upstreams}}
|
{{- range .Upstreams}}
|
||||||
pc_append "server={{ .IP }}#{{ .Port }}" "$config_file"
|
pc_append "server={{ .IP }}#{{ .Port }}" "$config_file"
|
||||||
{{- end}}
|
{{- end}}
|
||||||
{{- if .SendClientInfo}}
|
|
||||||
pc_delete "add-mac" "$config_file"
|
pc_delete "add-mac" "$config_file"
|
||||||
pc_delete "add-subnet" "$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
|
pc_append "add-subnet=32,128" "$config_file" # add client ip
|
||||||
{{- 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
|
||||||
|
pc_delete "cache-size=" "$config_file"
|
||||||
|
pc_append "cache-size=0" "$config_file" # disable cache
|
||||||
|
|
||||||
# For John fork
|
# For John fork
|
||||||
pc_delete "resolv-file" "$config_file" # no WAN DNS settings
|
pc_delete "resolv-file" "$config_file" # no WAN DNS settings
|
||||||
@@ -64,7 +67,18 @@ type Upstream struct {
|
|||||||
Port int
|
Port int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConfTmpl generates dnsmasq configuration from ctrld config.
|
||||||
func ConfTmpl(tmplText string, cfg *ctrld.Config) (string, error) {
|
func ConfTmpl(tmplText string, cfg *ctrld.Config) (string, error) {
|
||||||
|
return ConfTmplWithCacheDisabled(tmplText, cfg, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfTmplWithCacheDisabled is like ConfTmpl, but the caller can control whether
|
||||||
|
// dnsmasq cache is disabled using cacheDisabled parameter.
|
||||||
|
//
|
||||||
|
// Generally, the caller should use ConfTmpl, but on some routers which dnsmasq config may be changed
|
||||||
|
// after ctrld started (like EdgeOS/Ubios, Firewalla ...), dnsmasq cache should not be disabled because
|
||||||
|
// the cache-size=0 generated by ctrld will conflict with router's generated config.
|
||||||
|
func ConfTmplWithCacheDisabled(tmplText string, cfg *ctrld.Config, cacheDisabled bool) (string, error) {
|
||||||
listener := cfg.FirstListener()
|
listener := cfg.FirstListener()
|
||||||
if listener == nil {
|
if listener == nil {
|
||||||
return "", errors.New("missing listener")
|
return "", errors.New("missing listener")
|
||||||
@@ -74,24 +88,27 @@ func ConfTmpl(tmplText string, cfg *ctrld.Config) (string, error) {
|
|||||||
ip = "127.0.0.1"
|
ip = "127.0.0.1"
|
||||||
}
|
}
|
||||||
upstreams := []Upstream{{IP: ip, Port: listener.Port}}
|
upstreams := []Upstream{{IP: ip, Port: listener.Port}}
|
||||||
return confTmpl(tmplText, upstreams, cfg.HasUpstreamSendClientInfo())
|
return confTmpl(tmplText, upstreams, cacheDisabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FirewallaConfTmpl generates dnsmasq config for Firewalla routers.
|
||||||
func FirewallaConfTmpl(tmplText string, cfg *ctrld.Config) (string, error) {
|
func FirewallaConfTmpl(tmplText string, cfg *ctrld.Config) (string, error) {
|
||||||
|
// If ctrld listen on all interfaces, generating config for all of them.
|
||||||
if lc := cfg.FirstListener(); lc != nil && (lc.IP == "0.0.0.0" || lc.IP == "") {
|
if lc := cfg.FirstListener(); lc != nil && (lc.IP == "0.0.0.0" || lc.IP == "") {
|
||||||
return confTmpl(tmplText, firewallaUpstreams(lc.Port), cfg.HasUpstreamSendClientInfo())
|
return confTmpl(tmplText, firewallaUpstreams(lc.Port), false)
|
||||||
}
|
}
|
||||||
return ConfTmpl(tmplText, cfg)
|
// Otherwise, generating config for the specific listener from ctrld's config.
|
||||||
|
return ConfTmplWithCacheDisabled(tmplText, cfg, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func confTmpl(tmplText string, upstreams []Upstream, sendClientInfo bool) (string, error) {
|
func confTmpl(tmplText string, upstreams []Upstream, cacheDisabled bool) (string, error) {
|
||||||
tmpl := template.Must(template.New("").Parse(tmplText))
|
tmpl := template.Must(template.New("").Parse(tmplText))
|
||||||
var to = &struct {
|
var to = &struct {
|
||||||
SendClientInfo bool
|
Upstreams []Upstream
|
||||||
Upstreams []Upstream
|
CacheDisabled bool
|
||||||
}{
|
}{
|
||||||
SendClientInfo: sendClientInfo,
|
Upstreams: upstreams,
|
||||||
Upstreams: upstreams,
|
CacheDisabled: cacheDisabled,
|
||||||
}
|
}
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
if err := tmpl.Execute(&sb, to); err != nil {
|
if err := tmpl.Execute(&sb, to); err != nil {
|
||||||
@@ -117,9 +134,14 @@ func firewallaUpstreams(port int) []Upstream {
|
|||||||
return upstreams
|
return upstreams
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// firewallaDnsmasqConfFiles returns dnsmasq config files of all firewalla interfaces.
|
||||||
|
func firewallaDnsmasqConfFiles() ([]string, error) {
|
||||||
|
return filepath.Glob("/home/pi/firerouter/etc/dnsmasq.dns.*.conf")
|
||||||
|
}
|
||||||
|
|
||||||
// FirewallaSelfInterfaces returns list of interfaces that will be configured with default dnsmasq setup on Firewalla.
|
// FirewallaSelfInterfaces returns list of interfaces that will be configured with default dnsmasq setup on Firewalla.
|
||||||
func FirewallaSelfInterfaces() []*net.Interface {
|
func FirewallaSelfInterfaces() []*net.Interface {
|
||||||
matches, err := filepath.Glob("/home/pi/firerouter/etc/dnsmasq.dns.*.conf")
|
matches, err := firewallaDnsmasqConfFiles()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/dnsmasq"
|
"github.com/kardianos/service"
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld"
|
"github.com/Control-D-Inc/ctrld"
|
||||||
"github.com/kardianos/service"
|
"github.com/Control-D-Inc/ctrld/internal/router/dnsmasq"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -20,11 +20,15 @@ const (
|
|||||||
usgDNSMasqConfigPath = "/etc/dnsmasq.conf"
|
usgDNSMasqConfigPath = "/etc/dnsmasq.conf"
|
||||||
usgDNSMasqBackupConfigPath = "/etc/dnsmasq.conf.bak"
|
usgDNSMasqBackupConfigPath = "/etc/dnsmasq.conf.bak"
|
||||||
toggleContentFilteringLink = "https://community.ui.com/questions/UDM-Pro-disable-enable-DNS-filtering/e2cc4060-e56a-4139-b200-62d7f773ff8f"
|
toggleContentFilteringLink = "https://community.ui.com/questions/UDM-Pro-disable-enable-DNS-filtering/e2cc4060-e56a-4139-b200-62d7f773ff8f"
|
||||||
|
toggleDnsShieldLink = "https://community.ui.com/questions/UniFi-OS-3-2-7-DNS-Shield-Missing/d3a85905-4ce0-4fe4-8bf0-6cb04f21371d"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrContentFilteringEnabled = fmt.Errorf(`the "Content Filtering" feature" is enabled, which is conflicted with ctrld.\n
|
var ErrContentFilteringEnabled = fmt.Errorf(`the "Content Filtering" feature" is enabled, which is conflicted with ctrld.\n
|
||||||
To disable it, folowing instruction here: %s`, toggleContentFilteringLink)
|
To disable it, folowing instruction here: %s`, toggleContentFilteringLink)
|
||||||
|
|
||||||
|
var ErrDnsShieldEnabled = fmt.Errorf(`the "DNS Shield" feature" is enabled, which is conflicted with ctrld.\n
|
||||||
|
To disable it, folowing screenshot here: %s`, toggleDnsShieldLink)
|
||||||
|
|
||||||
type EdgeOS struct {
|
type EdgeOS struct {
|
||||||
cfg *ctrld.Config
|
cfg *ctrld.Config
|
||||||
isUSG bool
|
isUSG bool
|
||||||
@@ -50,6 +54,11 @@ func (e *EdgeOS) Install(_ *service.Config) error {
|
|||||||
if ContentFilteringEnabled() {
|
if ContentFilteringEnabled() {
|
||||||
return ErrContentFilteringEnabled
|
return ErrContentFilteringEnabled
|
||||||
}
|
}
|
||||||
|
// If "DNS Shield" is enabled, UniFi OS will spawn dnscrypt-proxy process, and route all DNS queries to it. So
|
||||||
|
// reporting an error and guiding users to disable the feature using UniFi OS web UI.
|
||||||
|
if DnsShieldEnabled() {
|
||||||
|
return ErrDnsShieldEnabled
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +104,7 @@ func (e *EdgeOS) setupUSG() error {
|
|||||||
return fmt.Errorf("setupUSG: backup current config: %w", err)
|
return fmt.Errorf("setupUSG: backup current config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Removing all configured upstreams.
|
// Removing all configured upstreams and cache config.
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
scanner := bufio.NewScanner(bytes.NewReader(buf))
|
scanner := bufio.NewScanner(bytes.NewReader(buf))
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
@@ -109,7 +118,7 @@ func (e *EdgeOS) setupUSG() error {
|
|||||||
sb.WriteString(line)
|
sb.WriteString(line)
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, e.cfg)
|
data, err := dnsmasq.ConfTmplWithCacheDisabled(dnsmasq.ConfigContentTmpl, e.cfg, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -127,7 +136,7 @@ func (e *EdgeOS) setupUSG() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *EdgeOS) setupUDM() error {
|
func (e *EdgeOS) setupUDM() error {
|
||||||
data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, e.cfg)
|
data, err := dnsmasq.ConfTmplWithCacheDisabled(dnsmasq.ConfigContentTmpl, e.cfg, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -169,6 +178,16 @@ func ContentFilteringEnabled() bool {
|
|||||||
return err == nil && !st.IsDir()
|
return err == nil && !st.IsDir()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DnsShieldEnabled reports whether DNS Shield is enabled.
|
||||||
|
// See: https://community.ui.com/releases/UniFi-OS-Dream-Machines-3-2-7/251dfc1e-f4dd-4264-a080-3be9d8b9e02b
|
||||||
|
func DnsShieldEnabled() bool {
|
||||||
|
buf, err := os.ReadFile("/var/run/dnsmasq.conf.d/dns.conf")
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return bytes.Contains(buf, []byte("server=127.0.0.1#5053"))
|
||||||
|
}
|
||||||
|
|
||||||
func LeaseFileDir() string {
|
func LeaseFileDir() string {
|
||||||
if checkUSG() {
|
if checkUSG() {
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"github.com/kardianos/service"
|
"github.com/kardianos/service"
|
||||||
@@ -44,8 +45,24 @@ func (m *Merlin) Uninstall(_ *service.Config) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Merlin) PreRun() error {
|
func (m *Merlin) PreRun() error {
|
||||||
|
// Wait NTP ready.
|
||||||
_ = m.Cleanup()
|
_ = m.Cleanup()
|
||||||
return ntp.WaitNvram()
|
if err := ntp.WaitNvram(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Wait until directories mounted.
|
||||||
|
for _, dir := range []string{"/tmp", "/proc"} {
|
||||||
|
waitDirExists(dir)
|
||||||
|
}
|
||||||
|
// Wait dnsmasq started.
|
||||||
|
for {
|
||||||
|
out, _ := exec.Command("pidof", "dnsmasq").CombinedOutput()
|
||||||
|
if len(bytes.TrimSpace(out)) > 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Merlin) Setup() error {
|
func (m *Merlin) Setup() error {
|
||||||
@@ -56,9 +73,6 @@ func (m *Merlin) Setup() error {
|
|||||||
if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" {
|
if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if _, err := nvram.Run("set", nvram.CtrldSetupKey+"=1"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
buf, err := os.ReadFile(dnsmasq.MerlinPostConfPath)
|
buf, err := os.ReadFile(dnsmasq.MerlinPostConfPath)
|
||||||
// Already setup.
|
// Already setup.
|
||||||
if bytes.Contains(buf, []byte(dnsmasq.MerlinPostConfMarker)) {
|
if bytes.Contains(buf, []byte(dnsmasq.MerlinPostConfMarker)) {
|
||||||
@@ -140,3 +154,12 @@ func merlinParsePostConf(buf []byte) []byte {
|
|||||||
}
|
}
|
||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func waitDirExists(dir string) {
|
||||||
|
for {
|
||||||
|
if _, err := os.Stat(dir); !os.IsNotExist(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,11 +8,10 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/dnsmasq"
|
|
||||||
|
|
||||||
"github.com/kardianos/service"
|
"github.com/kardianos/service"
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld"
|
"github.com/Control-D-Inc/ctrld"
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/router/dnsmasq"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -20,10 +19,9 @@ const (
|
|||||||
openwrtDNSMasqConfigPath = "/tmp/dnsmasq.d/ctrld.conf"
|
openwrtDNSMasqConfigPath = "/tmp/dnsmasq.d/ctrld.conf"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errUCIEntryNotFound = errors.New("uci: Entry not found")
|
|
||||||
|
|
||||||
type Openwrt struct {
|
type Openwrt struct {
|
||||||
cfg *ctrld.Config
|
cfg *ctrld.Config
|
||||||
|
dnsmasqCacheSize string
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a router.Router for configuring/setup/run ctrld on Openwrt routers.
|
// New returns a router.Router for configuring/setup/run ctrld on Openwrt routers.
|
||||||
@@ -52,6 +50,19 @@ func (o *Openwrt) Setup() error {
|
|||||||
if o.cfg.FirstListener().IsDirectDnsListener() {
|
if o.cfg.FirstListener().IsDirectDnsListener() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save current dnsmasq config cache size if present.
|
||||||
|
if cs, err := uci("get", "dhcp.@dnsmasq[0].cachesize"); err == nil {
|
||||||
|
o.dnsmasqCacheSize = cs
|
||||||
|
if _, err := uci("delete", "dhcp.@dnsmasq[0].cachesize"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Commit.
|
||||||
|
if _, err := uci("commit", "dhcp"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, o.cfg)
|
data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, o.cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -59,10 +70,6 @@ func (o *Openwrt) Setup() error {
|
|||||||
if err := os.WriteFile(openwrtDNSMasqConfigPath, []byte(data), 0600); err != nil {
|
if err := os.WriteFile(openwrtDNSMasqConfigPath, []byte(data), 0600); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Commit.
|
|
||||||
if _, err := uci("commit"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Restart dnsmasq service.
|
// Restart dnsmasq service.
|
||||||
if err := restartDNSMasq(); err != nil {
|
if err := restartDNSMasq(); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -78,6 +85,18 @@ func (o *Openwrt) Cleanup() error {
|
|||||||
if err := os.Remove(openwrtDNSMasqConfigPath); err != nil {
|
if err := os.Remove(openwrtDNSMasqConfigPath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restore original value if present.
|
||||||
|
if o.dnsmasqCacheSize != "" {
|
||||||
|
if _, err := uci("set", fmt.Sprintf("dhcp.@dnsmasq[0].cachesize=%s", o.dnsmasqCacheSize)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Commit.
|
||||||
|
if _, err := uci("commit", "dhcp"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Restart dnsmasq service.
|
// Restart dnsmasq service.
|
||||||
if err := restartDNSMasq(); err != nil {
|
if err := restartDNSMasq(); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -92,6 +111,8 @@ func restartDNSMasq() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var errUCIEntryNotFound = errors.New("uci: Entry not found")
|
||||||
|
|
||||||
func uci(args ...string) (string, error) {
|
func uci(args ...string) (string, error) {
|
||||||
cmd := exec.Command("uci", args...)
|
cmd := exec.Command("uci", args...)
|
||||||
var stdout, stderr bytes.Buffer
|
var stdout, stderr bytes.Buffer
|
||||||
|
|||||||
@@ -137,20 +137,9 @@ rcvar="${name}_enable"
|
|||||||
pidfile="/var/run/${name}.pid"
|
pidfile="/var/run/${name}.pid"
|
||||||
child_pidfile="/var/run/${name}_child.pid"
|
child_pidfile="/var/run/${name}_child.pid"
|
||||||
command="/usr/sbin/daemon"
|
command="/usr/sbin/daemon"
|
||||||
daemon_args="-P ${pidfile} -p ${child_pidfile} -t \"${name}: daemon\"{{if .WorkingDirectory}} -c {{.WorkingDirectory}}{{end}}"
|
daemon_args="-r -P ${pidfile} -p ${child_pidfile} -t \"${name}: daemon\"{{if .WorkingDirectory}} -c {{.WorkingDirectory}}{{end}}"
|
||||||
command_args="${daemon_args} {{.Path}}{{range .Arguments}} {{.}}{{end}}"
|
command_args="${daemon_args} {{.Path}}{{range .Arguments}} {{.}}{{end}}"
|
||||||
|
|
||||||
stop_cmd="ctrld_stop"
|
|
||||||
|
|
||||||
ctrld_stop() {
|
|
||||||
pid=$(cat ${pidfile})
|
|
||||||
child_pid=$(cat ${child_pidfile})
|
|
||||||
if [ -e "${child_pidfile}" ]; then
|
|
||||||
kill -s TERM "${child_pid}"
|
|
||||||
wait_for_pids "${child_pid}" "${pidfile}"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
load_rc_config "${name}"
|
load_rc_config "${name}"
|
||||||
run_rc_command "$1"
|
run_rc_command "$1"
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -5,16 +5,17 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/dnsmasq"
|
"github.com/kardianos/service"
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld"
|
"github.com/Control-D-Inc/ctrld"
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/router/dnsmasq"
|
||||||
"github.com/Control-D-Inc/ctrld/internal/router/edgeos"
|
"github.com/Control-D-Inc/ctrld/internal/router/edgeos"
|
||||||
"github.com/kardianos/service"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Name = "ubios"
|
Name = "ubios"
|
||||||
ubiosDNSMasqConfigPath = "/run/dnsmasq.conf.d/zzzctrld.conf"
|
ubiosDNSMasqConfigPath = "/run/dnsmasq.conf.d/zzzctrld.conf"
|
||||||
|
ubiosDNSMasqDnsConfigPath = "/run/dnsmasq.conf.d/dns.conf"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Ubios struct {
|
type Ubios struct {
|
||||||
@@ -35,6 +36,10 @@ func (u *Ubios) Install(config *service.Config) error {
|
|||||||
if edgeos.ContentFilteringEnabled() {
|
if edgeos.ContentFilteringEnabled() {
|
||||||
return edgeos.ErrContentFilteringEnabled
|
return edgeos.ErrContentFilteringEnabled
|
||||||
}
|
}
|
||||||
|
// See comment in (*edgeos.EdgeOS).Install method.
|
||||||
|
if edgeos.DnsShieldEnabled() {
|
||||||
|
return edgeos.ErrDnsShieldEnabled
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +55,7 @@ func (u *Ubios) Setup() error {
|
|||||||
if u.cfg.FirstListener().IsDirectDnsListener() {
|
if u.cfg.FirstListener().IsDirectDnsListener() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, u.cfg)
|
data, err := dnsmasq.ConfTmplWithCacheDisabled(dnsmasq.ConfigContentTmpl, u.cfg, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,10 @@ package ctrld
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"tailscale.com/logtail/backoff"
|
|
||||||
|
|
||||||
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,30 +14,36 @@ var (
|
|||||||
ipv6Available atomic.Bool
|
ipv6Available atomic.Bool
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const ipv6ProbingInterval = 10 * time.Second
|
||||||
|
|
||||||
func hasIPv6() bool {
|
func hasIPv6() bool {
|
||||||
hasIPv6Once.Do(func() {
|
hasIPv6Once.Do(func() {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
val := ctrldnet.IPv6Available(ctx)
|
val := ctrldnet.IPv6Available(ctx)
|
||||||
ipv6Available.Store(val)
|
ipv6Available.Store(val)
|
||||||
go probingIPv6(val)
|
go probingIPv6(context.TODO(), val)
|
||||||
})
|
})
|
||||||
return ipv6Available.Load()
|
return ipv6Available.Load()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(cuonglm): doing poll check natively for supported platforms.
|
// TODO(cuonglm): doing poll check natively for supported platforms.
|
||||||
func probingIPv6(old bool) {
|
func probingIPv6(ctx context.Context, old bool) {
|
||||||
b := backoff.NewBackoff("probingIPv6", func(format string, args ...any) {}, 30*time.Second)
|
ticker := time.NewTicker(ipv6ProbingInterval)
|
||||||
bCtx := context.Background()
|
defer ticker.Stop()
|
||||||
for {
|
for {
|
||||||
func() {
|
select {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
case <-ctx.Done():
|
||||||
defer cancel()
|
return
|
||||||
cur := ctrldnet.IPv6Available(ctx)
|
case <-ticker.C:
|
||||||
if ipv6Available.CompareAndSwap(old, cur) {
|
func() {
|
||||||
old = cur
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
}
|
defer cancel()
|
||||||
}()
|
cur := ctrldnet.IPv6Available(ctx)
|
||||||
b.BackOff(bCtx, errors.New("no change"))
|
if ipv6Available.CompareAndSwap(old, cur) {
|
||||||
|
old = cur
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+46
-7
@@ -5,10 +5,12 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
|
"tailscale.com/net/interfaces"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -24,9 +26,11 @@ const (
|
|||||||
ResolverTypeOS = "os"
|
ResolverTypeOS = "os"
|
||||||
// ResolverTypeLegacy specifies legacy resolver.
|
// ResolverTypeLegacy specifies legacy resolver.
|
||||||
ResolverTypeLegacy = "legacy"
|
ResolverTypeLegacy = "legacy"
|
||||||
|
// ResolverTypePrivate is like ResolverTypeOS, but use for local resolver only.
|
||||||
|
ResolverTypePrivate = "private"
|
||||||
)
|
)
|
||||||
|
|
||||||
var bootstrapDNS = "76.76.2.0"
|
const bootstrapDNS = "76.76.2.22"
|
||||||
|
|
||||||
// or is the Resolver used for ResolverTypeOS.
|
// or is the Resolver used for ResolverTypeOS.
|
||||||
var or = &osResolver{nameservers: defaultNameservers()}
|
var or = &osResolver{nameservers: defaultNameservers()}
|
||||||
@@ -61,6 +65,8 @@ func NewResolver(uc *UpstreamConfig) (Resolver, error) {
|
|||||||
return or, nil
|
return or, nil
|
||||||
case ResolverTypeLegacy:
|
case ResolverTypeLegacy:
|
||||||
return &legacyResolver{uc: uc}, nil
|
return &legacyResolver{uc: uc}, nil
|
||||||
|
case ResolverTypePrivate:
|
||||||
|
return NewPrivateResolver(), nil
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("%w: %s", errUnknownResolver, typ)
|
return nil, fmt.Errorf("%w: %s", errUnknownResolver, typ)
|
||||||
}
|
}
|
||||||
@@ -74,8 +80,9 @@ type osResolverResult struct {
|
|||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve performs DNS resolvers using OS default nameservers. Nameserver is chosen from
|
// Resolve resolves DNS queries using pre-configured nameservers.
|
||||||
// available nameservers with a roundrobin algorithm.
|
// Query is sent to all nameservers concurrently, and the first
|
||||||
|
// success response will be returned.
|
||||||
func (o *osResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) {
|
func (o *osResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) {
|
||||||
numServers := len(o.nameservers)
|
numServers := len(o.nameservers)
|
||||||
if numServers == 0 {
|
if numServers == 0 {
|
||||||
@@ -240,12 +247,16 @@ func NewBootstrapResolver(servers ...string) 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.
|
// excluding:
|
||||||
|
//
|
||||||
|
// - Nameservers from /etc/resolv.conf file.
|
||||||
|
// - Nameservers which is local RFC1918 addresses.
|
||||||
//
|
//
|
||||||
// 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()
|
resolveConfNss := nameserversFromResolvconf()
|
||||||
|
localRfc1918Addrs := Rfc1918Addresses()
|
||||||
n := 0
|
n := 0
|
||||||
for _, ns := range nss {
|
for _, ns := range nss {
|
||||||
host, _, _ := net.SplitHostPort(ns)
|
host, _, _ := net.SplitHostPort(ns)
|
||||||
@@ -258,6 +269,10 @@ func NewPrivateResolver() Resolver {
|
|||||||
if sliceContains(resolveConfNss, host) {
|
if sliceContains(resolveConfNss, host) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// Ignoring local RFC 1918 addresses.
|
||||||
|
if sliceContains(localRfc1918Addrs, 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
|
||||||
@@ -265,11 +280,35 @@ func NewPrivateResolver() Resolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
nss = nss[:n]
|
nss = nss[:n]
|
||||||
if len(nss) == 0 {
|
return NewResolverWithNameserver(nss)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewResolverWithNameserver returns an OS resolver which uses the given nameservers
|
||||||
|
// for resolving DNS queries. If nameservers is empty, a dummy resolver will be returned.
|
||||||
|
//
|
||||||
|
// Each nameserver must be form "host:port". It's the caller responsibility to ensure all
|
||||||
|
// nameservers are well formatted by using net.JoinHostPort function.
|
||||||
|
func NewResolverWithNameserver(nameservers []string) Resolver {
|
||||||
|
if len(nameservers) == 0 {
|
||||||
return &dummyResolver{}
|
return &dummyResolver{}
|
||||||
}
|
}
|
||||||
resolver := &osResolver{nameservers: nss}
|
return &osResolver{nameservers: nameservers}
|
||||||
return resolver
|
}
|
||||||
|
|
||||||
|
// Rfc1918Addresses returns the list of local interfaces private IP addresses
|
||||||
|
func Rfc1918Addresses() []string {
|
||||||
|
var res []string
|
||||||
|
interfaces.ForeachInterface(func(i interfaces.Interface, prefixes []netip.Prefix) {
|
||||||
|
addrs, _ := i.Addrs()
|
||||||
|
for _, addr := range addrs {
|
||||||
|
ipNet, ok := addr.(*net.IPNet)
|
||||||
|
if !ok || !ipNet.IP.IsPrivate() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
res = append(res, ipNet.IP.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDialer(dnsAddress string) *net.Dialer {
|
func newDialer(dnsAddress string) *net.Dialer {
|
||||||
|
|||||||
@@ -82,4 +82,8 @@ rules = [
|
|||||||
{"*.ru" = ["upstream.1"]},
|
{"*.ru" = ["upstream.1"]},
|
||||||
{"*.local.host" = ["upstream.2", "upstream.0"]},
|
{"*.local.host" = ["upstream.2", "upstream.0"]},
|
||||||
]
|
]
|
||||||
|
macs = [
|
||||||
|
{"14:45:A0:67:83:0A" = ["upstream.2"]},
|
||||||
|
{"14:54:4a:8e:08:2d" = ["upstream.2"]},
|
||||||
|
]
|
||||||
`
|
`
|
||||||
|
|||||||
Reference in New Issue
Block a user