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:
Cuong Manh Le
2023-04-20 23:16:20 +07:00
committed by Cuong Manh Le
parent d52cd11322
commit 0645a738ad
17 changed files with 370 additions and 27 deletions
+88
View File
@@ -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
})
}
+8 -3
View File
@@ -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)
+34 -3
View File
@@ -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
}
+5 -1
View File
@@ -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
}
+1 -1
View File
@@ -9,7 +9,7 @@ import (
func Test_merlinParsePostConf(t *testing.T) {
origContent := "# foo"
data := strings.Join([]string{
merlinDNSMasqPostConf,
merlinDNSMasqPostConfTmpl,
"\n",
merlinDNSMasqPostConfMarker,
"\n",
+4
View File
@@ -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
}
+33 -9
View File
@@ -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
}
+4
View File
@@ -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
}