+ Replace dnsmasq on OpenWRT (minimal configuration)

This commit is contained in:
Simon Zolin
2020-03-12 18:54:54 +03:00
parent 6eadca25d1
commit 82f36341e3
5 changed files with 498 additions and 5 deletions

View File

@@ -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 <IP1>
nameserver <IP2>
* Write this yaml configuration:
dns:
bootstrap_dns:
- IP1
- IP2
And service script starts AGH like this:
.../AdGuardHome --auto-config
.../AdGuardHome

View File

@@ -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

View File

@@ -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
}

344
home/openwrt.go Normal file
View File

@@ -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
}

60
home/openwrt_test.go Normal file
View File

@@ -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)
}