From 82f36341e357b633ae5ca2128554c4a56241113d Mon Sep 17 00:00:00 2001 From: Simon Zolin Date: Thu, 12 Mar 2020 18:54:54 +0300 Subject: [PATCH] + Replace dnsmasq on OpenWRT (minimal configuration) --- AGHTechDoc.md | 64 ++++++++ dhcpd/dhcpd.go | 14 +- home/home.go | 21 ++- home/openwrt.go | 344 +++++++++++++++++++++++++++++++++++++++++++ home/openwrt_test.go | 60 ++++++++ 5 files changed, 498 insertions(+), 5 deletions(-) create mode 100644 home/openwrt.go create mode 100644 home/openwrt_test.go diff --git a/AGHTechDoc.md b/AGHTechDoc.md index b1303621..533fa839 100644 --- a/AGHTechDoc.md +++ b/AGHTechDoc.md @@ -64,6 +64,7 @@ Contents: * API: Log in * API: Log out * API: Get current user info +* Replace dnsmasq on OpenWRT ## Relations between subsystems @@ -1594,3 +1595,66 @@ Response: } If no client is configured then authentication is disabled and server sends an empty response. + + +## Replace dnsmasq on OpenWRT + +`/etc/init.d/dnsmasq` script creates a dnsmasq.conf file and then starts dnsmasq. +To replace dnsmasq we have to read system configuration files and update (create new, if necessary) our .yaml file accordingly. + +If started as: + + ./AdGuardHome --auto-config + +* Read `/etc/config/network`: + + config interface 'lan' + option netmask '255.255.255.0' + option ipaddr '192.168.8.1' + +* Read `/etc/config/dhcp`: + + config dhcp 'lan' + option start '100' + option limit '150' + option leasetime '12h' + +* Write this yaml configuration: + + dhcp: + enabled: true + interface_name: "br-lan" + gateway_ip: "192.168.8.1" + subnet_mask: "255.255.255.0" + range_start: "192.168.8.100" + range_end: "192.168.8.249" + lease_duration: 86400 + icmp_timeout_msec: 1000 + +* Read `/etc/config/dhcp`: + + config host '123412341234' + option mac '12:34:12:34:12:34' + option ip '192.168.8.2' + option name 'hostname' + +* Add a static lease to leases.db: + + 12:34:12:34:12:34 | 192.168.8.2 | hostname + +* Read `/etc/resolv.conf`: + + nameserver + nameserver + +* Write this yaml configuration: + + dns: + bootstrap_dns: + - IP1 + - IP2 + +And service script starts AGH like this: + + .../AdGuardHome --auto-config + .../AdGuardHome diff --git a/dhcpd/dhcpd.go b/dhcpd/dhcpd.go index b40a8ae8..4c805d9f 100644 --- a/dhcpd/dhcpd.go +++ b/dhcpd/dhcpd.go @@ -139,6 +139,11 @@ func (s *Server) Init(config ServerConfig) error { return nil } +// Save - save leases in DB +func (s *Server) Save() { + s.dbStore() +} + // SetOnLeaseChanged - set callback func (s *Server) SetOnLeaseChanged(onLeaseChanged onLeaseChangedT) { s.onLeaseChanged = onLeaseChanged @@ -608,6 +613,11 @@ func (s *Server) handleDecline(p dhcp4.Packet, options dhcp4.Options) dhcp4.Pack // AddStaticLease adds a static lease (thread-safe) func (s *Server) AddStaticLease(l Lease) error { + return s.AddStaticLeaseWithFlags(l, true) +} + +// AddStaticLeaseWithFlags - add a static lease (thread-safe) +func (s *Server) AddStaticLeaseWithFlags(l Lease, flush bool) error { if len(l.IP) != 4 { return fmt.Errorf("Invalid IP") } @@ -627,7 +637,9 @@ func (s *Server) AddStaticLease(l Lease) error { } s.leases = append(s.leases, &l) s.reserveIP(l.IP, l.HWAddr) - s.dbStore() + if flush { + s.dbStore() + } s.leasesLock.Unlock() s.notify(LeaseChangedAddedStatic) return nil diff --git a/home/home.go b/home/home.go index 17d05d41..53038525 100644 --- a/home/home.go +++ b/home/home.go @@ -138,12 +138,9 @@ func Main(version string, channel string, armVer string) { // run is a blocking method! // nolint func run(args options) { - // config file path can be overridden by command-line arguments: + Context.configFilename = "AdGuardHome.yaml" if args.configFilename != "" { Context.configFilename = args.configFilename - } else { - // Default config file name - Context.configFilename = "AdGuardHome.yaml" } // configure working dir and config path @@ -459,6 +456,7 @@ type options struct { pidFile string // File name to save PID to checkConfig bool // Check configuration and exit disableUpdate bool // If set, don't check for updates + hasAutoConfig bool // 'auto-config' argument is specified // service control action (see service.ControlAction array + "status" command) serviceControlAction string @@ -498,6 +496,9 @@ func loadOptions() options { {"pidfile", "", "Path to a file where PID is stored", func(value string) { o.pidFile = value }, nil}, {"check-config", "", "Check configuration and exit", nil, func() { o.checkConfig = true }}, {"no-check-update", "", "Don't check for updates", nil, func() { o.disableUpdate = true }}, + {"auto-config", "", "Create or update YAML configuration file from OpenWRT system configuration", nil, func() { + o.hasAutoConfig = true + }}, {"verbose", "v", "Enable verbose output", nil, func() { o.verbose = true }}, {"version", "", "Show the version and exit", nil, func() { fmt.Printf("AdGuardHome %s\n", versionString) @@ -550,6 +551,18 @@ func loadOptions() options { } } + if o.verbose { + log.SetLevel(log.DEBUG) + } + + if o.hasAutoConfig { + err := autoConfig(o.configFilename) + if err != nil { + log.Fatalf("%s", err) + } + os.Exit(0) + } + return o } diff --git a/home/openwrt.go b/home/openwrt.go new file mode 100644 index 00000000..a7f5012a --- /dev/null +++ b/home/openwrt.go @@ -0,0 +1,344 @@ +package home + +import ( + "bufio" + "fmt" + "io/ioutil" + "net" + "strconv" + "strings" + + "github.com/AdguardTeam/AdGuardHome/dhcpd" + "github.com/AdguardTeam/AdGuardHome/util" + "github.com/AdguardTeam/golibs/log" +) + +type openwrtConfig struct { + // network: + netmask string + ipaddr string + + // dhcp: + dhcpStart string + dhcpLimit string + dhcpLeasetime string + + // dhcp static leases: + leases []dhcpd.Lease + + // resolv.conf: + nameservers []string + + // yaml.dhcp: + iface string + gwIP string + snMask string + rangeStart string + rangeEnd string + leaseDur uint32 + + // yaml.dns.bootstrap_dns: + bsDNS []string +} + +// Parse command line: "option name 'value'" +func parseCmd(line string) (string, string, string) { + word1 := util.SplitNext(&line, ' ') + word2 := util.SplitNext(&line, ' ') + word3 := util.SplitNext(&line, ' ') + if len(word3) > 2 && word3[0] == '\'' && word3[len(word3)-1] == '\'' { + // 'value' -> value + word3 = word3[1:] + word3 = word3[:len(word3)-1] + } + return word1, word2, word3 +} + +// Parse system configuration data +// nolint(gocyclo) +func (oc *openwrtConfig) readConf(data []byte, section string, iface string) { + state := 0 + sr := strings.NewReader(string(data)) + r := bufio.NewReader(sr) + for { + line, err := r.ReadString('\n') + line = strings.TrimSpace(line) + if len(line) == 0 { + if state == 2 { + return + } + state = 0 + } + + word1, word2, word3 := parseCmd(line) + + switch state { + case 0: + if word1 == "config" { + state = 1 + if word2 == section && word3 == iface { + // found the needed section + if word2 == "interface" { + state = 2 // found the needed interface + } else if word2 == "dhcp" { + state = 3 + } + } + } + + case 1: + // not interested + break + + case 2: + if word1 != "option" { + break + } + switch word2 { + case "netmask": + oc.netmask = word3 + case "ipaddr": + oc.ipaddr = word3 + } + + case 3: + if word1 != "option" { + break + } + switch word2 { + case "start": + oc.dhcpStart = word3 + case "limit": + oc.dhcpLimit = word3 + case "leasetime": + oc.dhcpLeasetime = word3 + } + } + + if err != nil { + break + } + } +} +func (oc *openwrtConfig) readConfDHCPStatic(data []byte) error { + state := 0 + sr := strings.NewReader(string(data)) + r := bufio.NewReader(sr) + lease := dhcpd.Lease{} + for { + line, err := r.ReadString('\n') + line = strings.TrimSpace(line) + if len(line) == 0 { + if len(lease.HWAddr) != 0 && len(lease.IP) != 0 { + oc.leases = append(oc.leases, lease) + } + lease = dhcpd.Lease{} + state = 0 + } + + word1, word2, word3 := parseCmd(line) + + switch state { + case 0: + if word1 == "config" { + state = 1 + if word2 == "host" { + state = 2 + } + } + + case 1: + // not interested + break + + case 2: + if word1 != "option" { + break + } + switch word2 { + case "mac": + lease.HWAddr, err = net.ParseMAC(word3) + if err != nil { + return err + } + + case "ip": + lease.IP = net.ParseIP(word3) + if lease.IP == nil || lease.IP.To4() == nil { + return fmt.Errorf("Invalid IP address") + } + + case "name": + lease.Hostname = word3 + } + } + + if err != nil { + break + } + } + + if len(lease.HWAddr) != 0 && len(lease.IP) != 0 { + oc.leases = append(oc.leases, lease) + } + return nil +} + +// Parse "/etc/resolv.conf" data +func (oc *openwrtConfig) readResolvConf(data []byte) { + lines := string(data) + + for len(lines) != 0 { + line := util.SplitNext(&lines, '\n') + key := util.SplitNext(&line, ' ') + if key == "nameserver" { + val := util.SplitNext(&line, ' ') + oc.nameservers = append(oc.nameservers, val) + } + } +} + +// Convert system config parameters to the format suitable by our yaml config +func (oc *openwrtConfig) prepareOutput() error { + oc.iface = "br-lan" + + ipAddr := net.ParseIP(oc.ipaddr) + if ipAddr == nil || ipAddr.To4() == nil { + return fmt.Errorf("Invalid IP: %s", oc.ipaddr) + } + oc.gwIP = oc.ipaddr + + ip := net.ParseIP(oc.netmask) + if ip == nil || ip.To4() == nil { + return fmt.Errorf("Invalid IP: %s", oc.netmask) + } + oc.snMask = oc.netmask + + nStart, err := strconv.Atoi(oc.dhcpStart) + if err != nil { + return fmt.Errorf("Invalid 'start': %s", oc.dhcpStart) + } + rangeStart := make(net.IP, 4) + copy(rangeStart, ipAddr.To4()) + rangeStart[3] = byte(nStart) + oc.rangeStart = rangeStart.String() + + nLim, err := strconv.Atoi(oc.dhcpLimit) + if err != nil { + return fmt.Errorf("Invalid 'start': %s", oc.dhcpLimit) + } + n := nStart + nLim - 1 + if n <= 0 || n > 255 { + return fmt.Errorf("Invalid 'start' or 'limit': %s/%s", oc.dhcpStart, oc.dhcpLimit) + } + rangeEnd := make(net.IP, 4) + copy(rangeEnd, ipAddr.To4()) + rangeEnd[3] = byte(n) + oc.rangeEnd = rangeEnd.String() + + if len(oc.dhcpLeasetime) == 0 || oc.dhcpLeasetime[len(oc.dhcpLeasetime)-1] != 'h' { + return fmt.Errorf("Invalid leasetime: %s", oc.dhcpLeasetime) + } + n, err = strconv.Atoi(oc.dhcpLeasetime[:len(oc.dhcpLeasetime)-1]) + if err != nil { + return fmt.Errorf("Invalid leasetime: %s", oc.dhcpLeasetime) + } + oc.leaseDur = uint32(n) * 60 * 60 + + for _, s := range oc.nameservers { + if net.ParseIP(s) == nil { + continue + } + oc.bsDNS = append(oc.bsDNS, s) + } + return nil +} + +func (oc *openwrtConfig) Start() error { + data, err := ioutil.ReadFile("/etc/config/network") + if err != nil { + return err + } + oc.readConf(data, "interface", "lan") + + data, err = ioutil.ReadFile("/etc/config/dhcp") + if err != nil { + return err + } + oc.readConf(data, "dhcp", "lan") + + err = oc.prepareOutput() + if err != nil { + return err + } + + oc.readConfDHCPStatic(data) + if err != nil { + return err + } + + return nil +} + +// Read system configuration files and write our configuration files +func autoConfig(configFn string) error { + oc := openwrtConfig{} + err := oc.Start() + if err != nil { + return err + } + + // config file path can be overridden by command-line arguments: + Context.configFilename = "AdGuardHome.yaml" + if len(configFn) != 0 { + Context.configFilename = configFn + } + + initConfig() + if util.FileExists(config.getConfigFilename()) { + err = upgradeConfig() + if err != nil { + return err + } + err = parseConfig() + if err != nil { + return err + } + } + + config.DNS.BootstrapDNS = oc.bsDNS + + config.DHCP.Enabled = true + config.DHCP.InterfaceName = oc.iface + config.DHCP.GatewayIP = oc.gwIP + config.DHCP.SubnetMask = oc.snMask + config.DHCP.RangeStart = oc.rangeStart + config.DHCP.RangeEnd = oc.rangeEnd + config.DHCP.LeaseDuration = oc.leaseDur + + err = config.write() + if err != nil { + return err + } + + dconf := dhcpd.ServerConfig{ + WorkDir: Context.workDir, + } + ds := dhcpd.Create(dconf) + if ds == nil { + return fmt.Errorf("can't initialize DHCP module") + } + for _, ocl := range oc.leases { + l := dhcpd.Lease{ + HWAddr: ocl.HWAddr, + IP: ocl.IP.To4(), + Hostname: ocl.Hostname, + } + ds.AddStaticLeaseWithFlags(l, false) + log.Debug("Static DHCP lease: %s -> %s (%s)", + l.HWAddr, l.IP, l.Hostname) + } + ds.Save() + + return nil +} diff --git a/home/openwrt_test.go b/home/openwrt_test.go new file mode 100644 index 00000000..dacba768 --- /dev/null +++ b/home/openwrt_test.go @@ -0,0 +1,60 @@ +package home + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestReadConf(t *testing.T) { + oc := openwrtConfig{} + data := []byte(` config interface 'lan' +option netmask '255.255.255.0' +option ipaddr '192.168.8.1'`) + oc.readConf(data, "interface", "lan") + assert.Equal(t, "255.255.255.0", oc.netmask) + assert.Equal(t, "192.168.8.1", oc.ipaddr) + + data = []byte(` config dhcp 'unknown' + +config dhcp 'lan' +option start '100' +option limit '150' +option leasetime '12h' + +config dhcp 'unknown'`) + oc.readConf(data, "dhcp", "lan") + assert.Equal(t, "100", oc.dhcpStart) + assert.Equal(t, "150", oc.dhcpLimit) + assert.Equal(t, "12h", oc.dhcpLeasetime) + + data = []byte(` # comment +nameserver abab::1234 + +nameserver 1.2.3.4`) + oc.readResolvConf(data) + assert.Equal(t, "abab::1234", oc.nameservers[0]) + assert.Equal(t, "1.2.3.4", oc.nameservers[1]) + + err := oc.prepareOutput() + assert.Equal(t, nil, err) + assert.Equal(t, "br-lan", oc.iface) + assert.Equal(t, "192.168.8.1", oc.gwIP) + assert.Equal(t, "255.255.255.0", oc.snMask) + assert.Equal(t, "192.168.8.100", oc.rangeStart) + assert.Equal(t, "192.168.8.249", oc.rangeEnd) + assert.Equal(t, uint32(43200), oc.leaseDur) + assert.Equal(t, "abab::1234", oc.bsDNS[0]) + assert.Equal(t, "1.2.3.4", oc.bsDNS[1]) + + data = []byte(`config host '123412341234' +option mac '12:34:12:34:12:34' +option ip '192.168.8.2' +option name 'hostname'`) + assert.True(t, nil == oc.readConfDHCPStatic(data)) + assert.Equal(t, 1, len(oc.leases)) + assert.Equal(t, "12:34:12:34:12:34", oc.leases[0].HWAddr.String()) + assert.Equal(t, "192.168.8.2", oc.leases[0].IP.String()) + assert.Equal(t, "hostname", oc.leases[0].Hostname) + +}