Compare commits

...

6 Commits

Author SHA1 Message Date
Simon Zolin
da2a8daff2 + parse "config dnsmasq" 2020-04-23 12:13:41 +03:00
Simon Zolin
5fbf28b9cf * dhcpd: "dnsmasq_leasefile" setting 2020-04-22 18:56:54 +03:00
Simon Zolin
dced8a5a83 rename 2020-03-20 16:29:42 +03:00
Simon Zolin
33a98aa937 minor 2020-03-20 11:45:22 +03:00
Simon Zolin
e822443600 minor 2020-03-19 14:22:16 +03:00
Simon Zolin
82f36341e3 + Replace dnsmasq on OpenWRT (minimal configuration) 2020-03-19 14:17:28 +03:00
6 changed files with 625 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,70 @@ 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 --import-openwrt-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'
config dnsmasq
option leasefile '/tmp/dhcp.leases'
* 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
dnsmasq_leasefile "/tmp/dhcp.leases"
* 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 --import-openwrt-config
.../AdGuardHome

View File

@@ -10,6 +10,7 @@ import (
"sync"
"time"
"github.com/AdguardTeam/golibs/file"
"github.com/AdguardTeam/golibs/log"
"github.com/krolaw/dhcp4"
ping "github.com/sparrc/go-ping"
@@ -43,6 +44,9 @@ type ServerConfig struct {
RangeEnd string `json:"range_end" yaml:"range_end"`
LeaseDuration uint32 `json:"lease_duration" yaml:"lease_duration"` // in seconds
// File path to an additional leases file in dnsmasq format
DnsmasqFilePath string `json:"-" yaml:"dnsmasq_leasefile"`
// IP conflict detector: time (ms) to wait for ICMP reply.
// 0: disable
ICMPTimeout uint32 `json:"icmp_timeout_msec" yaml:"icmp_timeout_msec"`
@@ -139,12 +143,59 @@ 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
}
// Write DHCP leases in dnsmasq format
// Format: UNIX_TIME MAC IP HOSTNAME CLIENT_ID
func writeDnsmasqLeases(leases []Lease) string {
s := ""
for _, l := range leases {
t := l.Expiry.Unix()
if t == leaseExpireStatic {
t = 0
}
host := l.Hostname
if len(host) == 0 {
host = "*"
}
cid := "*"
s += fmt.Sprintf("%d %s %s %s %s\n",
t, l.HWAddr.String(), l.IP.String(), host, cid)
}
return s
}
func (s *Server) notify(flags int) {
if len(s.conf.DnsmasqFilePath) != 0 {
switch flags {
case LeaseChangedAdded:
fallthrough
case LeaseChangedAddedStatic:
fallthrough
case LeaseChangedRemovedStatic:
l := s.Leases(LeasesAll)
data := writeDnsmasqLeases(l)
err := file.SafeWrite(s.conf.DnsmasqFilePath, []byte(data))
if err != nil {
log.Error("file write: %s: %s", s.conf.DnsmasqFilePath, err)
}
}
}
if s.onLeaseChanged == nil {
return
}
@@ -608,6 +659,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 +683,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

@@ -242,3 +242,15 @@ func TestNormalizeLeases(t *testing.T) {
assert.True(t, bytes.Equal(leases[1].HWAddr, []byte{2, 2, 3, 4}))
assert.True(t, bytes.Equal(leases[2].HWAddr, []byte{1, 2, 3, 5}))
}
func TestWriteDnsmasqLeases(t *testing.T) {
leases := []Lease{}
l := Lease{}
l.Expiry = time.Unix(1587559766, 0)
l.HWAddr, _ = net.ParseMAC("12:34:12:34:12:34")
l.IP = net.ParseIP("192.168.8.2")
l.Hostname = "hostname"
leases = append(leases, l)
data := "1587559766 12:34:12:34:12:34 192.168.8.2 hostname *\n"
assert.Equal(t, data, writeDnsmasqLeases(leases))
}

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
@@ -460,6 +457,8 @@ type options struct {
checkConfig bool // Check configuration and exit
disableUpdate bool // If set, don't check for updates
hasImportOpenwrtConfig bool // 'import-openwrt-config' argument is specified
// service control action (see service.ControlAction array + "status" command)
serviceControlAction string
@@ -498,6 +497,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 }},
{"import-openwrt-config", "", "Create or update YAML configuration file from OpenWRT system configuration", nil, func() {
o.hasImportOpenwrtConfig = 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 +552,18 @@ func loadOptions() options {
}
}
if o.verbose {
log.SetLevel(log.DEBUG)
}
if o.hasImportOpenwrtConfig {
err := importOpenwrtConfig(o.configFilename)
if err != nil {
log.Fatalf("%s", err)
}
os.Exit(0)
}
return o
}

364
home/openwrt.go Normal file
View File

@@ -0,0 +1,364 @@
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
dhcpDnsmasqLeaseFile 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
} else if word2 == "dnsmasq" {
state = 4
}
}
}
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
}
case 4:
if word1 != "option" {
break
}
switch word2 {
case "leasefile":
oc.dhcpDnsmasqLeaseFile = word3
}
}
if err != nil {
break
}
}
}
// Parse static DHCP leases from system configuration data
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 our yaml config format
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
}
// Process - read and process system configuration data
func (oc *openwrtConfig) Process() 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")
oc.readConf(data, "dnsmasq", "")
err = oc.prepareOutput()
if err != nil {
return err
}
err = oc.readConfDHCPStatic(data)
if err != nil {
return err
}
return nil
}
// Read system configuration files and write our configuration files
func importOpenwrtConfig(configFn string) error {
oc := openwrtConfig{}
err := oc.Process()
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
config.DHCP.DnsmasqFilePath = oc.dhcpDnsmasqLeaseFile
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,
}
err = ds.AddStaticLeaseWithFlags(l, false)
if err != nil {
continue
}
log.Debug("Static DHCP lease: %s -> %s (%s)",
l.HWAddr, l.IP, l.Hostname)
}
ds.Save()
return nil
}

104
home/openwrt_test.go Normal file
View File

@@ -0,0 +1,104 @@
package home
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestReadConf(t *testing.T) {
oc := openwrtConfig{}
// "interface"
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)
// "dhcp"
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)
// resolv.conf
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])
// prepareOutput()
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])
tmp := oc.ipaddr
oc.dhcpStart = "invalid" // not an IP
assert.True(t, oc.prepareOutput() != nil)
oc.ipaddr = tmp
tmp = oc.dhcpStart
oc.dhcpStart = "invalid" // not an integer
assert.True(t, oc.prepareOutput() != nil)
oc.dhcpStart = "256" //byte overflow
assert.True(t, oc.prepareOutput() != nil)
oc.dhcpStart = tmp
tmp = oc.dhcpLimit
oc.dhcpLimit = "invalid" // not an integer
assert.True(t, oc.prepareOutput() != nil)
oc.dhcpLimit = "200" //byte overflow
assert.True(t, oc.prepareOutput() != nil)
oc.dhcpLimit = tmp
tmp = oc.dhcpLeasetime
oc.dhcpLeasetime = "12m" // not an 'h'
assert.True(t, oc.prepareOutput() != nil)
oc.dhcpLeasetime = "invalid" // not an integer
assert.True(t, oc.prepareOutput() != nil)
oc.dhcpLeasetime = tmp
// dhcp static leases
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)
// "dnsmasq"
// Note: "config dnsmasq ''" will also work
data = []byte(`
config dhcp 'unknown'
option asdf '100'
config dnsmasq
option asdf '100'
option leasefile '/tmp/dhcp.leases'
option leasetime '12h'`)
oc.readConf(data, "dnsmasq", "")
assert.Equal(t, "/tmp/dhcp.leases", oc.dhcpDnsmasqLeaseFile)
}