mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-05-15 00:50:25 +02:00
all: add router client info detection
This commit add the ability for ctrld to gather client information, including mac/ip/hostname, and send to Control-D server through a config per upstream. - Add send_client_info upstream config. - Read/Watch dnsmasq leases files on supported platforms. - Add corresponding client info to DoH query header All of these only apply for Control-D upstream, though.
This commit is contained in:
committed by
Cuong Manh Le
parent
d52cd11322
commit
0645a738ad
@@ -0,0 +1,88 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"tailscale.com/util/lineread"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
)
|
||||
|
||||
var clientInfoFiles = []string{
|
||||
"/tmp/dnsmasq.leases", // ddwrt
|
||||
"/tmp/dhcp.leases", // openwrt
|
||||
"/var/lib/misc/dnsmasq.leases", // merlin
|
||||
"/mnt/data/udapi-config/dnsmasq.lease", // UDM Pro
|
||||
"/data/udapi-config/dnsmasq.lease", // UDR
|
||||
}
|
||||
|
||||
func (r *router) watchClientInfoTable() {
|
||||
if r.watcher == nil {
|
||||
return
|
||||
}
|
||||
timer := time.NewTicker(time.Minute * 5)
|
||||
for {
|
||||
select {
|
||||
case <-timer.C:
|
||||
for _, name := range r.watcher.WatchList() {
|
||||
_ = readClientInfoFile(name)
|
||||
}
|
||||
case event, ok := <-r.watcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if event.Has(fsnotify.Write) {
|
||||
if err := readClientInfoFile(event.Name); err != nil && !os.IsNotExist(err) {
|
||||
log.Println("could not read client info file:", err)
|
||||
}
|
||||
}
|
||||
case err, ok := <-r.watcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
log.Println("error:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Stop() error {
|
||||
if Name() == "" {
|
||||
return nil
|
||||
}
|
||||
r := routerPlatform.Load()
|
||||
if r.watcher != nil {
|
||||
if err := r.watcher.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetClientInfoByMac(mac string) *ctrld.ClientInfo {
|
||||
if mac == "" {
|
||||
return nil
|
||||
}
|
||||
_ = Name()
|
||||
r := routerPlatform.Load()
|
||||
val, ok := r.mac.Load(mac)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return val.(*ctrld.ClientInfo)
|
||||
}
|
||||
|
||||
func readClientInfoFile(name string) error {
|
||||
r := routerPlatform.Load()
|
||||
return lineread.File(name, func(line []byte) error {
|
||||
fields := bytes.Fields(line)
|
||||
mac := string(fields[1])
|
||||
ip := string(fields[2])
|
||||
hostname := string(fields[3])
|
||||
r.mac.Store(mac, &ctrld.ClientInfo{Mac: mac, IP: ip, Hostname: hostname})
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -20,9 +20,9 @@ https://wiki.dd-wrt.com/wiki/index.php/Journalling_Flash_File_System
|
||||
`)
|
||||
|
||||
var nvramKeys = map[string]string{
|
||||
"dns_dnsmasq": "1", // Make dnsmasq running but disable DNS ability, ctrld will replace it.
|
||||
"dnsmasq_options": dnsMasqConfigContent, // Configuration of dnsmasq set by ctrld.
|
||||
"dns_crypt": "0", // Disable DNSCrypt.
|
||||
"dns_dnsmasq": "1", // Make dnsmasq running but disable DNS ability, ctrld will replace it.
|
||||
"dnsmasq_options": "", // Configuration of dnsmasq set by ctrld, filled by setupDDWrt.
|
||||
"dns_crypt": "0", // Disable DNSCrypt.
|
||||
}
|
||||
|
||||
func setupDDWrt() error {
|
||||
@@ -31,6 +31,11 @@ func setupDDWrt() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := dnsMasqConf()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nvramKeys["dnsmasq_options"] = data
|
||||
// Backup current value, store ctrld's configs.
|
||||
for key, value := range nvramKeys {
|
||||
old, err := nvram("get", key)
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
package router
|
||||
|
||||
const dnsMasqConfigContent = `# GENERATED BY ctrld - DO NOT MODIFY
|
||||
import (
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
const dnsMasqConfigContentTmpl = `# GENERATED BY ctrld - DO NOT MODIFY
|
||||
no-resolv
|
||||
server=127.0.0.1#5354
|
||||
{{- if .SendClientInfo}}
|
||||
add-mac
|
||||
{{- end}}
|
||||
`
|
||||
|
||||
const merlinDNSMasqPostConfPath = "/jffs/scripts/dnsmasq.postconf"
|
||||
const merlinDNSMasqPostConfMarker = `# GENERATED BY ctrld - EOF`
|
||||
|
||||
const merlinDNSMasqPostConf = `# GENERATED BY ctrld - DO NOT MODIFY
|
||||
const merlinDNSMasqPostConfTmpl = `# GENERATED BY ctrld - DO NOT MODIFY
|
||||
|
||||
#!/bin/sh
|
||||
|
||||
@@ -20,7 +28,9 @@ if [ -n "$pid" ] && [ -f "/proc/${pid}/cmdline" ]; then
|
||||
pc_delete "servers-file" "$config_file" # no WAN DNS settings
|
||||
pc_append "no-resolv" "$config_file" # do not read /etc/resolv.conf
|
||||
pc_append "server=127.0.0.1#5354" "$config_file" # use ctrld as upstream
|
||||
|
||||
{{- if .SendClientInfo}}
|
||||
pc_append "add-mac" "$config_file" # add client mac
|
||||
{{- end}}
|
||||
|
||||
# For John fork
|
||||
pc_delete "resolv-file" "$config_file" # no WAN DNS settings
|
||||
@@ -32,3 +42,24 @@ if [ -n "$pid" ] && [ -f "/proc/${pid}/cmdline" ]; then
|
||||
exit 0
|
||||
fi
|
||||
`
|
||||
|
||||
func dnsMasqConf() (string, error) {
|
||||
var sb strings.Builder
|
||||
var tmplText string
|
||||
switch Name() {
|
||||
case DDWrt, OpenWrt, Ubios:
|
||||
tmplText = dnsMasqConfigContentTmpl
|
||||
case Merlin:
|
||||
tmplText = merlinDNSMasqPostConfTmpl
|
||||
}
|
||||
tmpl := template.Must(template.New("").Parse(tmplText))
|
||||
var to = &struct {
|
||||
SendClientInfo bool
|
||||
}{
|
||||
routerPlatform.Load().sendClientInfo,
|
||||
}
|
||||
if err := tmpl.Execute(&sb, to); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
@@ -19,6 +19,10 @@ func setupMerlin() error {
|
||||
return err
|
||||
}
|
||||
|
||||
merlinDNSMasqPostConf, err := dnsMasqConf()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := strings.Join([]string{
|
||||
merlinDNSMasqPostConf,
|
||||
"\n",
|
||||
@@ -38,7 +42,7 @@ func setupMerlin() error {
|
||||
}
|
||||
|
||||
func cleanupMerlin() error {
|
||||
buf, err := os.ReadFile(merlinDNSMasqPostConf)
|
||||
buf, err := os.ReadFile(merlinDNSMasqPostConfPath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
func Test_merlinParsePostConf(t *testing.T) {
|
||||
origContent := "# foo"
|
||||
data := strings.Join([]string{
|
||||
merlinDNSMasqPostConf,
|
||||
merlinDNSMasqPostConfTmpl,
|
||||
"\n",
|
||||
merlinDNSMasqPostConfMarker,
|
||||
"\n",
|
||||
|
||||
@@ -19,6 +19,10 @@ func setupOpenWrt() error {
|
||||
return err
|
||||
}
|
||||
// Disable dnsmasq as DNS server.
|
||||
dnsMasqConfigContent, err := dnsMasqConf()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(openwrtDNSMasqConfigPath, []byte(dnsMasqConfigContent), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/kardianos/service"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
@@ -25,7 +27,10 @@ var ErrNotSupported = errors.New("unsupported platform")
|
||||
var routerPlatform atomic.Pointer[router]
|
||||
|
||||
type router struct {
|
||||
name string
|
||||
name string
|
||||
sendClientInfo bool
|
||||
mac sync.Map
|
||||
watcher *fsnotify.Watcher
|
||||
}
|
||||
|
||||
// SupportedPlatforms return all platforms that can be configured to run with ctrld.
|
||||
@@ -33,18 +38,37 @@ func SupportedPlatforms() []string {
|
||||
return []string{DDWrt, Merlin, OpenWrt, Ubios}
|
||||
}
|
||||
|
||||
var configureFunc = map[string]func() error{
|
||||
DDWrt: setupDDWrt,
|
||||
Merlin: setupMerlin,
|
||||
OpenWrt: setupOpenWrt,
|
||||
Ubios: setupUbiOS,
|
||||
}
|
||||
|
||||
// Configure configures things for running ctrld on the router.
|
||||
func Configure(c *ctrld.Config) error {
|
||||
name := Name()
|
||||
switch name {
|
||||
case DDWrt:
|
||||
return setupDDWrt()
|
||||
case Merlin:
|
||||
return setupMerlin()
|
||||
case OpenWrt:
|
||||
return setupOpenWrt()
|
||||
case Ubios:
|
||||
return setupUbiOS()
|
||||
case DDWrt, Merlin, OpenWrt, Ubios:
|
||||
if c.HasUpstreamSendClientInfo() {
|
||||
r := routerPlatform.Load()
|
||||
r.sendClientInfo = true
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.watcher = watcher
|
||||
go r.watchClientInfoTable()
|
||||
for _, file := range clientInfoFiles {
|
||||
_ = readClientInfoFile(file)
|
||||
_ = r.watcher.Add(file)
|
||||
}
|
||||
}
|
||||
configure := configureFunc[name]
|
||||
if err := configure(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return ErrNotSupported
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@ const (
|
||||
|
||||
func setupUbiOS() error {
|
||||
// Disable dnsmasq as DNS server.
|
||||
dnsMasqConfigContent, err := dnsMasqConf()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(ubiosDNSMasqConfigPath, []byte(dnsMasqConfigContent), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user