mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-08-10 13:20:33 +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:
@@ -0,0 +1,11 @@
|
|||||||
|
package ctrld
|
||||||
|
|
||||||
|
// ClientInfoCtxKey is the context key to store client info.
|
||||||
|
type ClientInfoCtxKey struct{}
|
||||||
|
|
||||||
|
// ClientInfo represents ctrld's clients information.
|
||||||
|
type ClientInfo struct {
|
||||||
|
Mac string
|
||||||
|
IP string
|
||||||
|
Hostname string
|
||||||
|
}
|
||||||
+27
-1
@@ -20,7 +20,13 @@ import (
|
|||||||
"github.com/Control-D-Inc/ctrld/internal/router"
|
"github.com/Control-D-Inc/ctrld/internal/router"
|
||||||
)
|
)
|
||||||
|
|
||||||
const staleTTL = 60 * time.Second
|
const (
|
||||||
|
staleTTL = 60 * time.Second
|
||||||
|
// 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
|
||||||
|
// This is also dns.EDNS0LOCALSTART, but define our own constant here for clarification.
|
||||||
|
EDNS0_OPTION_MAC = 0xFDE9
|
||||||
|
)
|
||||||
|
|
||||||
var osUpstreamConfig = &ctrld.UpstreamConfig{
|
var osUpstreamConfig = &ctrld.UpstreamConfig{
|
||||||
Name: "OS resolver",
|
Name: "OS resolver",
|
||||||
@@ -230,6 +236,12 @@ 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 := router.GetClientInfoByMac(macFromMsg(msg))
|
||||||
|
if ci != nil {
|
||||||
|
ctx = context.WithValue(ctx, ctrld.ClientInfoCtxKey{}, ci)
|
||||||
|
}
|
||||||
|
}
|
||||||
answer, err := resolve1(n, upstreamConfig, msg)
|
answer, err := resolve1(n, upstreamConfig, msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctrld.Log(ctx, mainLog.Debug().Err(err), "could not resolve query on first attempt, retrying...")
|
ctrld.Log(ctx, mainLog.Debug().Err(err), "could not resolve query on first attempt, retrying...")
|
||||||
@@ -386,3 +398,17 @@ func dnsListenAddress(lc *ctrld.ListenerConfig) string {
|
|||||||
}
|
}
|
||||||
return net.JoinHostPort(lc.IP, strconv.Itoa(lc.Port))
|
return net.JoinHostPort(lc.IP, strconv.Itoa(lc.Port))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func macFromMsg(msg *dns.Msg) string {
|
||||||
|
if opt := msg.IsEdns0(); opt != nil {
|
||||||
|
for _, s := range opt.Option {
|
||||||
|
switch e := s.(type) {
|
||||||
|
case *dns.EDNS0_LOCAL:
|
||||||
|
if e.Code == EDNS0_OPTION_MAC {
|
||||||
|
return net.HardwareAddr(e.Data).String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|||||||
@@ -155,3 +155,39 @@ func TestCache(t *testing.T) {
|
|||||||
assert.Equal(t, answer1.Rcode, got1.Rcode)
|
assert.Equal(t, answer1.Rcode, got1.Rcode)
|
||||||
assert.Equal(t, answer2.Rcode, got2.Rcode)
|
assert.Equal(t, answer2.Rcode, got2.Rcode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_macFromMsg(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
mac string
|
||||||
|
wantMac bool
|
||||||
|
}{
|
||||||
|
{"has mac", "4c:20:b8:ab:87:1b", true},
|
||||||
|
{"no mac", "4c:20:b8:ab:87:1b", false},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
hw, err := net.ParseMAC(tc.mac)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetQuestion(selfCheckFQDN+".", dns.TypeA)
|
||||||
|
o := &dns.OPT{Hdr: dns.RR_Header{Name: ".", Rrtype: dns.TypeOPT}}
|
||||||
|
if tc.wantMac {
|
||||||
|
ec1 := &dns.EDNS0_LOCAL{Code: EDNS0_OPTION_MAC, Data: hw}
|
||||||
|
o.Option = append(o.Option, ec1)
|
||||||
|
}
|
||||||
|
m.Extra = append(m.Extra, o)
|
||||||
|
got := macFromMsg(m)
|
||||||
|
if tc.wantMac && got != tc.mac {
|
||||||
|
t.Errorf("mismatch, want: %q, got: %q", tc.mac, got)
|
||||||
|
}
|
||||||
|
if !tc.wantMac && got != "" {
|
||||||
|
t.Errorf("unexpected mac: %q", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -139,6 +139,9 @@ func (p *prog) Stop(s service.Service) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
p.preStop()
|
p.preStop()
|
||||||
|
if err := router.Stop(); err != nil {
|
||||||
|
mainLog.Warn().Err(err).Msg("problem occurred while stopping router")
|
||||||
|
}
|
||||||
mainLog.Info().Msg("Service stopped")
|
mainLog.Info().Msg("Service stopped")
|
||||||
close(p.stopCh)
|
close(p.stopCh)
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -80,6 +80,17 @@ type Config struct {
|
|||||||
Upstream map[string]*UpstreamConfig `mapstructure:"upstream" toml:"upstream" validate:"min=1,dive"`
|
Upstream map[string]*UpstreamConfig `mapstructure:"upstream" toml:"upstream" validate:"min=1,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasUpstreamSendClientInfo reports whether the config has any upstream
|
||||||
|
// is configured to send client info to Control D DNS server.
|
||||||
|
func (c *Config) HasUpstreamSendClientInfo() bool {
|
||||||
|
for _, uc := range c.Upstream {
|
||||||
|
if uc.UpstreamSendClientInfo() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// 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"`
|
||||||
@@ -101,12 +112,15 @@ type NetworkConfig struct {
|
|||||||
|
|
||||||
// UpstreamConfig specifies configuration for upstreams that ctrld will forward requests to.
|
// UpstreamConfig specifies configuration for upstreams that ctrld will forward requests to.
|
||||||
type UpstreamConfig struct {
|
type UpstreamConfig struct {
|
||||||
Name string `mapstructure:"name" toml:"name,omitempty"`
|
Name string `mapstructure:"name" toml:"name,omitempty"`
|
||||||
Type string `mapstructure:"type" toml:"type,omitempty" validate:"oneof=doh doh3 dot doq os legacy"`
|
Type string `mapstructure:"type" toml:"type,omitempty" validate:"oneof=doh doh3 dot doq os legacy"`
|
||||||
Endpoint string `mapstructure:"endpoint" toml:"endpoint,omitempty" validate:"required_unless=Type os"`
|
Endpoint string `mapstructure:"endpoint" toml:"endpoint,omitempty" validate:"required_unless=Type os"`
|
||||||
BootstrapIP string `mapstructure:"bootstrap_ip" toml:"bootstrap_ip,omitempty"`
|
BootstrapIP string `mapstructure:"bootstrap_ip" toml:"bootstrap_ip,omitempty"`
|
||||||
Domain string `mapstructure:"-" toml:"-"`
|
Domain string `mapstructure:"-" toml:"-"`
|
||||||
Timeout int `mapstructure:"timeout" toml:"timeout,omitempty" validate:"gte=0"`
|
Timeout int `mapstructure:"timeout" toml:"timeout,omitempty" validate:"gte=0"`
|
||||||
|
// The caller should not access this field directly.
|
||||||
|
// Use UpstreamSendClientInfo instead.
|
||||||
|
SendClientInfo *bool `mapstructure:"send_client_info" toml:"send_client_info,omitempty"`
|
||||||
transport *http.Transport `mapstructure:"-" toml:"-"`
|
transport *http.Transport `mapstructure:"-" toml:"-"`
|
||||||
http3RoundTripper http.RoundTripper `mapstructure:"-" toml:"-"`
|
http3RoundTripper http.RoundTripper `mapstructure:"-" toml:"-"`
|
||||||
certPool *x509.CertPool `mapstructure:"-" toml:"-"`
|
certPool *x509.CertPool `mapstructure:"-" toml:"-"`
|
||||||
@@ -163,6 +177,34 @@ func (uc *UpstreamConfig) Init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpstreamSendClientInfo reports whether the upstream is
|
||||||
|
// configured to send client info to Control D DNS server.
|
||||||
|
//
|
||||||
|
// Client info includes:
|
||||||
|
// - MAC
|
||||||
|
// - Lan IP
|
||||||
|
// - Hostname
|
||||||
|
func (uc *UpstreamConfig) UpstreamSendClientInfo() bool {
|
||||||
|
if uc.SendClientInfo != nil && !(*uc.SendClientInfo) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if uc.SendClientInfo == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
switch uc.Type {
|
||||||
|
case ResolverTypeDOH, ResolverTypeDOH3:
|
||||||
|
if u, err := url.Parse(uc.Endpoint); err == nil {
|
||||||
|
domain := u.Hostname()
|
||||||
|
for _, parent := range []string{"controld.com", "controld.net"} {
|
||||||
|
if dns.IsSubDomain(parent, domain) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// SetCertPool sets the system cert pool used for TLS connections.
|
// SetCertPool sets the system cert pool used for TLS connections.
|
||||||
func (uc *UpstreamConfig) SetCertPool(cp *x509.CertPool) {
|
func (uc *UpstreamConfig) SetCertPool(cp *x509.CertPool) {
|
||||||
uc.certPool = cp
|
uc.certPool = cp
|
||||||
|
|||||||
@@ -147,6 +147,28 @@ func TestUpstreamConfig_Init(t *testing.T) {
|
|||||||
Timeout: 0,
|
Timeout: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"doh+doh3 with send client info set",
|
||||||
|
&UpstreamConfig{
|
||||||
|
Name: "doh",
|
||||||
|
Type: "doh",
|
||||||
|
Endpoint: "https://example.com?k=v",
|
||||||
|
BootstrapIP: "",
|
||||||
|
Domain: "",
|
||||||
|
Timeout: 0,
|
||||||
|
SendClientInfo: ptrBool(false),
|
||||||
|
},
|
||||||
|
&UpstreamConfig{
|
||||||
|
Name: "doh",
|
||||||
|
Type: "doh",
|
||||||
|
Endpoint: "https://example.com?k=v",
|
||||||
|
BootstrapIP: "",
|
||||||
|
Domain: "example.com",
|
||||||
|
Timeout: 0,
|
||||||
|
SendClientInfo: ptrBool(false),
|
||||||
|
u: u2,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
@@ -158,3 +180,7 @@ func TestUpstreamConfig_Init(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ptrBool(b bool) *bool {
|
||||||
|
return &b
|
||||||
|
}
|
||||||
|
|||||||
+5
-1
@@ -24,10 +24,12 @@ func TestLoadConfig(t *testing.T) {
|
|||||||
assert.Contains(t, cfg.Network, "0")
|
assert.Contains(t, cfg.Network, "0")
|
||||||
assert.Contains(t, cfg.Network, "1")
|
assert.Contains(t, cfg.Network, "1")
|
||||||
|
|
||||||
assert.Len(t, cfg.Upstream, 3)
|
assert.Len(t, cfg.Upstream, 4)
|
||||||
assert.Contains(t, cfg.Upstream, "0")
|
assert.Contains(t, cfg.Upstream, "0")
|
||||||
assert.Contains(t, cfg.Upstream, "1")
|
assert.Contains(t, cfg.Upstream, "1")
|
||||||
assert.Contains(t, cfg.Upstream, "2")
|
assert.Contains(t, cfg.Upstream, "2")
|
||||||
|
assert.Contains(t, cfg.Upstream, "3")
|
||||||
|
assert.NotNil(t, cfg.Upstream["3"].SendClientInfo)
|
||||||
|
|
||||||
assert.Len(t, cfg.Listener, 2)
|
assert.Len(t, cfg.Listener, 2)
|
||||||
assert.Contains(t, cfg.Listener, "0")
|
assert.Contains(t, cfg.Listener, "0")
|
||||||
@@ -42,6 +44,8 @@ func TestLoadConfig(t *testing.T) {
|
|||||||
assert.Len(t, cfg.Listener["0"].Policy.Rules, 2)
|
assert.Len(t, cfg.Listener["0"].Policy.Rules, 2)
|
||||||
assert.Contains(t, cfg.Listener["0"].Policy.Rules[0], "*.ru")
|
assert.Contains(t, cfg.Listener["0"].Policy.Rules[0], "*.ru")
|
||||||
assert.Contains(t, cfg.Listener["0"].Policy.Rules[1], "*.local.host")
|
assert.Contains(t, cfg.Listener["0"].Policy.Rules[1], "*.local.host")
|
||||||
|
|
||||||
|
assert.True(t, cfg.HasUpstreamSendClientInfo())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadDefaultConfig(t *testing.T) {
|
func TestLoadDefaultConfig(t *testing.T) {
|
||||||
|
|||||||
@@ -12,12 +12,22 @@ import (
|
|||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DoHMacHeader = "Dns-Mac"
|
||||||
|
DoHIPHeader = "Dns-IP"
|
||||||
|
DoHHostHeader = "Dns-Host"
|
||||||
|
|
||||||
|
headerContentTypeValue = "application/dns-message"
|
||||||
|
headerAcceptValue = "application/dns-message"
|
||||||
|
)
|
||||||
|
|
||||||
func newDohResolver(uc *UpstreamConfig) *dohResolver {
|
func newDohResolver(uc *UpstreamConfig) *dohResolver {
|
||||||
r := &dohResolver{
|
r := &dohResolver{
|
||||||
endpoint: uc.u,
|
endpoint: uc.u,
|
||||||
isDoH3: uc.Type == ResolverTypeDOH3,
|
isDoH3: uc.Type == ResolverTypeDOH3,
|
||||||
transport: uc.transport,
|
transport: uc.transport,
|
||||||
http3RoundTripper: uc.http3RoundTripper,
|
http3RoundTripper: uc.http3RoundTripper,
|
||||||
|
sendClientInfo: uc.UpstreamSendClientInfo(),
|
||||||
}
|
}
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
@@ -27,6 +37,7 @@ type dohResolver struct {
|
|||||||
isDoH3 bool
|
isDoH3 bool
|
||||||
transport *http.Transport
|
transport *http.Transport
|
||||||
http3RoundTripper http.RoundTripper
|
http3RoundTripper http.RoundTripper
|
||||||
|
sendClientInfo bool
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@@ -45,8 +56,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)
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", "application/dns-message")
|
addHeader(ctx, req, r.sendClientInfo)
|
||||||
req.Header.Set("Accept", "application/dns-message")
|
|
||||||
|
|
||||||
c := http.Client{Transport: r.transport}
|
c := http.Client{Transport: r.transport}
|
||||||
if r.isDoH3 {
|
if r.isDoH3 {
|
||||||
@@ -78,3 +88,21 @@ func (r *dohResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, erro
|
|||||||
answer := new(dns.Msg)
|
answer := new(dns.Msg)
|
||||||
return answer, answer.Unpack(buf)
|
return answer, answer.Unpack(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func addHeader(ctx context.Context, req *http.Request, sendClientInfo bool) {
|
||||||
|
req.Header.Set("Content-Type", headerContentTypeValue)
|
||||||
|
req.Header.Set("Accept", headerAcceptValue)
|
||||||
|
if sendClientInfo {
|
||||||
|
if ci, ok := ctx.Value(ClientInfoCtxKey{}).(*ClientInfo); ok && ci != nil {
|
||||||
|
if ci.Mac != "" {
|
||||||
|
req.Header.Set(DoHMacHeader, ci.Mac)
|
||||||
|
}
|
||||||
|
if ci.IP != "" {
|
||||||
|
req.Header.Set(DoHIPHeader, ci.IP)
|
||||||
|
}
|
||||||
|
if ci.Hostname != "" {
|
||||||
|
req.Header.Set(DoHHostHeader, ci.Hostname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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{
|
var nvramKeys = map[string]string{
|
||||||
"dns_dnsmasq": "1", // Make dnsmasq running but disable DNS ability, ctrld will replace it.
|
"dns_dnsmasq": "1", // Make dnsmasq running but disable DNS ability, ctrld will replace it.
|
||||||
"dnsmasq_options": dnsMasqConfigContent, // Configuration of dnsmasq set by ctrld.
|
"dnsmasq_options": "", // Configuration of dnsmasq set by ctrld, filled by setupDDWrt.
|
||||||
"dns_crypt": "0", // Disable DNSCrypt.
|
"dns_crypt": "0", // Disable DNSCrypt.
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupDDWrt() error {
|
func setupDDWrt() error {
|
||||||
@@ -31,6 +31,11 @@ func setupDDWrt() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data, err := dnsMasqConf()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
nvramKeys["dnsmasq_options"] = data
|
||||||
// Backup current value, store ctrld's configs.
|
// Backup current value, store ctrld's configs.
|
||||||
for key, value := range nvramKeys {
|
for key, value := range nvramKeys {
|
||||||
old, err := nvram("get", key)
|
old, err := nvram("get", key)
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
package router
|
package router
|
||||||
|
|
||||||
const dnsMasqConfigContent = `# GENERATED BY ctrld - DO NOT MODIFY
|
import (
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
const dnsMasqConfigContentTmpl = `# GENERATED BY ctrld - DO NOT MODIFY
|
||||||
no-resolv
|
no-resolv
|
||||||
server=127.0.0.1#5354
|
server=127.0.0.1#5354
|
||||||
|
{{- if .SendClientInfo}}
|
||||||
|
add-mac
|
||||||
|
{{- end}}
|
||||||
`
|
`
|
||||||
|
|
||||||
const merlinDNSMasqPostConfPath = "/jffs/scripts/dnsmasq.postconf"
|
const merlinDNSMasqPostConfPath = "/jffs/scripts/dnsmasq.postconf"
|
||||||
const merlinDNSMasqPostConfMarker = `# GENERATED BY ctrld - EOF`
|
const merlinDNSMasqPostConfMarker = `# GENERATED BY ctrld - EOF`
|
||||||
|
|
||||||
const merlinDNSMasqPostConf = `# GENERATED BY ctrld - DO NOT MODIFY
|
const merlinDNSMasqPostConfTmpl = `# GENERATED BY ctrld - DO NOT MODIFY
|
||||||
|
|
||||||
#!/bin/sh
|
#!/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_delete "servers-file" "$config_file" # no WAN DNS settings
|
||||||
pc_append "no-resolv" "$config_file" # do not read /etc/resolv.conf
|
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
|
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
|
# For John fork
|
||||||
pc_delete "resolv-file" "$config_file" # no WAN DNS settings
|
pc_delete "resolv-file" "$config_file" # no WAN DNS settings
|
||||||
@@ -32,3 +42,24 @@ if [ -n "$pid" ] && [ -f "/proc/${pid}/cmdline" ]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
merlinDNSMasqPostConf, err := dnsMasqConf()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
data := strings.Join([]string{
|
data := strings.Join([]string{
|
||||||
merlinDNSMasqPostConf,
|
merlinDNSMasqPostConf,
|
||||||
"\n",
|
"\n",
|
||||||
@@ -38,7 +42,7 @@ func setupMerlin() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func cleanupMerlin() error {
|
func cleanupMerlin() error {
|
||||||
buf, err := os.ReadFile(merlinDNSMasqPostConf)
|
buf, err := os.ReadFile(merlinDNSMasqPostConfPath)
|
||||||
if err != nil && !os.IsNotExist(err) {
|
if err != nil && !os.IsNotExist(err) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
func Test_merlinParsePostConf(t *testing.T) {
|
func Test_merlinParsePostConf(t *testing.T) {
|
||||||
origContent := "# foo"
|
origContent := "# foo"
|
||||||
data := strings.Join([]string{
|
data := strings.Join([]string{
|
||||||
merlinDNSMasqPostConf,
|
merlinDNSMasqPostConfTmpl,
|
||||||
"\n",
|
"\n",
|
||||||
merlinDNSMasqPostConfMarker,
|
merlinDNSMasqPostConfMarker,
|
||||||
"\n",
|
"\n",
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ func setupOpenWrt() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Disable dnsmasq as DNS server.
|
// Disable dnsmasq as DNS server.
|
||||||
|
dnsMasqConfigContent, err := dnsMasqConf()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := os.WriteFile(openwrtDNSMasqConfigPath, []byte(dnsMasqConfigContent), 0600); err != nil {
|
if err := os.WriteFile(openwrtDNSMasqConfigPath, []byte(dnsMasqConfigContent), 0600); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
"github.com/kardianos/service"
|
"github.com/kardianos/service"
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld"
|
"github.com/Control-D-Inc/ctrld"
|
||||||
@@ -25,7 +27,10 @@ var ErrNotSupported = errors.New("unsupported platform")
|
|||||||
var routerPlatform atomic.Pointer[router]
|
var routerPlatform atomic.Pointer[router]
|
||||||
|
|
||||||
type router struct {
|
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.
|
// 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}
|
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.
|
// Configure configures things for running ctrld on the router.
|
||||||
func Configure(c *ctrld.Config) error {
|
func Configure(c *ctrld.Config) error {
|
||||||
name := Name()
|
name := Name()
|
||||||
switch name {
|
switch name {
|
||||||
case DDWrt:
|
case DDWrt, Merlin, OpenWrt, Ubios:
|
||||||
return setupDDWrt()
|
if c.HasUpstreamSendClientInfo() {
|
||||||
case Merlin:
|
r := routerPlatform.Load()
|
||||||
return setupMerlin()
|
r.sendClientInfo = true
|
||||||
case OpenWrt:
|
watcher, err := fsnotify.NewWatcher()
|
||||||
return setupOpenWrt()
|
if err != nil {
|
||||||
case Ubios:
|
return err
|
||||||
return setupUbiOS()
|
}
|
||||||
|
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:
|
default:
|
||||||
return ErrNotSupported
|
return ErrNotSupported
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ const (
|
|||||||
|
|
||||||
func setupUbiOS() error {
|
func setupUbiOS() error {
|
||||||
// Disable dnsmasq as DNS server.
|
// Disable dnsmasq as DNS server.
|
||||||
|
dnsMasqConfigContent, err := dnsMasqConf()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := os.WriteFile(ubiosDNSMasqConfigPath, []byte(dnsMasqConfigContent), 0600); err != nil {
|
if err := os.WriteFile(ubiosDNSMasqConfigPath, []byte(dnsMasqConfigContent), 0600); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,13 @@ type = "legacy"
|
|||||||
endpoint = "8.8.8.8"
|
endpoint = "8.8.8.8"
|
||||||
timeout = 5
|
timeout = 5
|
||||||
|
|
||||||
|
[upstream.3]
|
||||||
|
name = "DOH with client info"
|
||||||
|
type = "doh"
|
||||||
|
endpoint = "https://dns.controld.com/client_info_upstream/main-device"
|
||||||
|
timeout = 5
|
||||||
|
send_client_info = false
|
||||||
|
|
||||||
[listener.0]
|
[listener.0]
|
||||||
ip = "127.0.0.1"
|
ip = "127.0.0.1"
|
||||||
port = 53
|
port = 53
|
||||||
|
|||||||
Reference in New Issue
Block a user