2020-11-16 16:01:12 +00:00
|
|
|
// Package dhcpd provides a DHCP server.
|
2018-12-28 14:17:51 +00:00
|
|
|
package dhcpd
|
|
|
|
|
|
|
|
import (
|
2020-08-25 11:07:11 +00:00
|
|
|
"encoding/hex"
|
2021-01-20 12:59:24 +00:00
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
2018-12-28 14:17:51 +00:00
|
|
|
"net"
|
2019-10-11 16:56:18 +00:00
|
|
|
"net/http"
|
2019-08-19 11:27:02 +00:00
|
|
|
"path/filepath"
|
2020-11-16 16:01:12 +00:00
|
|
|
"runtime"
|
2020-08-25 11:07:11 +00:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
2018-12-28 14:17:51 +00:00
|
|
|
"time"
|
|
|
|
|
2020-10-30 10:32:02 +00:00
|
|
|
"github.com/AdguardTeam/AdGuardHome/internal/util"
|
2019-02-25 13:44:22 +00:00
|
|
|
"github.com/AdguardTeam/golibs/log"
|
2018-12-28 14:17:51 +00:00
|
|
|
)
|
|
|
|
|
2020-11-16 16:01:12 +00:00
|
|
|
const (
|
|
|
|
defaultDiscoverTime = time.Second * 3
|
2021-02-18 11:11:54 +00:00
|
|
|
// leaseExpireStatic is used to define the Expiry field for static
|
|
|
|
// leases.
|
|
|
|
//
|
|
|
|
// TODO(e.burkov): Remove it when static leases determining mechanism
|
|
|
|
// will be improved.
|
|
|
|
leaseExpireStatic = 1
|
2020-11-16 16:01:12 +00:00
|
|
|
)
|
2018-12-28 14:17:51 +00:00
|
|
|
|
2020-02-18 16:27:09 +00:00
|
|
|
var webHandlersRegistered = false
|
|
|
|
|
2019-01-24 17:11:01 +00:00
|
|
|
// Lease contains the necessary information about a DHCP lease
|
2018-12-28 14:17:51 +00:00
|
|
|
type Lease struct {
|
2020-07-03 15:20:01 +00:00
|
|
|
HWAddr net.HardwareAddr `json:"mac"`
|
2018-12-29 13:44:07 +00:00
|
|
|
IP net.IP `json:"ip"`
|
|
|
|
Hostname string `json:"hostname"`
|
2019-05-14 10:02:04 +00:00
|
|
|
|
|
|
|
// Lease expiration time
|
|
|
|
// 1: static lease
|
|
|
|
Expiry time.Time `json:"expires"`
|
2018-12-28 14:17:51 +00:00
|
|
|
}
|
|
|
|
|
2021-01-20 12:59:24 +00:00
|
|
|
// MarshalJSON implements the json.Marshaler interface for *Lease.
|
|
|
|
func (l *Lease) MarshalJSON() ([]byte, error) {
|
2021-02-18 11:11:54 +00:00
|
|
|
var expiryStr string
|
|
|
|
if expiry := l.Expiry; expiry.Unix() != leaseExpireStatic {
|
|
|
|
// The front-end is waiting for RFC 3999 format of the time
|
|
|
|
// value. It also shouldn't got an Expiry field for static
|
|
|
|
// leases.
|
|
|
|
//
|
|
|
|
// See https://github.com/AdguardTeam/AdGuardHome/issues/2692.
|
|
|
|
expiryStr = expiry.Format(time.RFC3339)
|
|
|
|
}
|
|
|
|
|
2021-01-20 12:59:24 +00:00
|
|
|
type lease Lease
|
|
|
|
return json.Marshal(&struct {
|
|
|
|
HWAddr string `json:"mac"`
|
2021-02-18 11:11:54 +00:00
|
|
|
Expiry string `json:"expires,omitempty"`
|
2021-01-20 12:59:24 +00:00
|
|
|
*lease
|
|
|
|
}{
|
|
|
|
HWAddr: l.HWAddr.String(),
|
2021-02-18 11:11:54 +00:00
|
|
|
Expiry: expiryStr,
|
2021-01-20 12:59:24 +00:00
|
|
|
lease: (*lease)(l),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// UnmarshalJSON implements the json.Unmarshaler interface for *Lease.
|
|
|
|
func (l *Lease) UnmarshalJSON(data []byte) (err error) {
|
|
|
|
type lease Lease
|
|
|
|
aux := struct {
|
|
|
|
HWAddr string `json:"mac"`
|
|
|
|
*lease
|
|
|
|
}{
|
|
|
|
lease: (*lease)(l),
|
|
|
|
}
|
|
|
|
if err = json.Unmarshal(data, &aux); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
l.HWAddr, err = net.ParseMAC(aux.HWAddr)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("couldn't parse MAC address: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-01-24 17:11:01 +00:00
|
|
|
// ServerConfig - DHCP server configuration
|
2018-12-28 14:17:51 +00:00
|
|
|
// field ordering is important -- yaml fields will mirror ordering from here
|
|
|
|
type ServerConfig struct {
|
2020-07-03 15:20:01 +00:00
|
|
|
Enabled bool `yaml:"enabled"`
|
|
|
|
InterfaceName string `yaml:"interface_name"`
|
2019-03-07 13:48:55 +00:00
|
|
|
|
2020-07-03 15:20:01 +00:00
|
|
|
Conf4 V4ServerConf `yaml:"dhcpv4"`
|
|
|
|
Conf6 V6ServerConf `yaml:"dhcpv6"`
|
2019-10-14 09:12:06 +00:00
|
|
|
|
2020-07-03 15:20:01 +00:00
|
|
|
WorkDir string `yaml:"-"`
|
|
|
|
DBFilePath string `yaml:"-"` // path to DB file
|
2019-10-11 16:56:18 +00:00
|
|
|
|
|
|
|
// Called when the configuration is changed by HTTP request
|
2020-07-03 15:20:01 +00:00
|
|
|
ConfigModified func() `yaml:"-"`
|
2019-10-11 16:56:18 +00:00
|
|
|
|
|
|
|
// Register an HTTP handler
|
2020-07-03 15:20:01 +00:00
|
|
|
HTTPRegister func(string, string, func(http.ResponseWriter, *http.Request)) `yaml:"-"`
|
2018-12-28 14:17:51 +00:00
|
|
|
}
|
|
|
|
|
2020-12-07 12:38:05 +00:00
|
|
|
// OnLeaseChangedT is a callback for lease changes.
|
2020-07-03 15:20:01 +00:00
|
|
|
type OnLeaseChangedT func(flags int)
|
2020-01-30 07:25:02 +00:00
|
|
|
|
|
|
|
// flags for onLeaseChanged()
|
|
|
|
const (
|
|
|
|
LeaseChangedAdded = iota
|
|
|
|
LeaseChangedAddedStatic
|
|
|
|
LeaseChangedRemovedStatic
|
2020-07-03 15:20:01 +00:00
|
|
|
|
|
|
|
LeaseChangedDBStore
|
2020-01-30 07:25:02 +00:00
|
|
|
)
|
|
|
|
|
2019-01-24 17:11:01 +00:00
|
|
|
// Server - the current state of the DHCP server
|
2018-12-28 14:17:51 +00:00
|
|
|
type Server struct {
|
2020-07-03 15:20:01 +00:00
|
|
|
srv4 DHCPServer
|
|
|
|
srv6 DHCPServer
|
2018-12-28 14:17:51 +00:00
|
|
|
|
2019-05-14 14:49:45 +00:00
|
|
|
conf ServerConfig
|
2020-01-30 07:25:02 +00:00
|
|
|
|
|
|
|
// Called when the leases DB is modified
|
2020-07-03 15:20:01 +00:00
|
|
|
onLeaseChanged []OnLeaseChangedT
|
2018-12-28 14:17:51 +00:00
|
|
|
}
|
|
|
|
|
2020-12-07 12:38:05 +00:00
|
|
|
// ServerInterface is an interface for servers.
|
2020-07-03 15:20:01 +00:00
|
|
|
type ServerInterface interface {
|
|
|
|
Leases(flags int) []Lease
|
|
|
|
SetOnLeaseChanged(onLeaseChanged OnLeaseChangedT)
|
2019-03-06 12:13:27 +00:00
|
|
|
}
|
|
|
|
|
2019-11-22 11:21:08 +00:00
|
|
|
// Create - create object
|
2021-02-11 17:49:03 +00:00
|
|
|
func Create(conf ServerConfig) *Server {
|
2020-11-16 16:01:12 +00:00
|
|
|
s := &Server{}
|
|
|
|
|
2021-02-11 17:49:03 +00:00
|
|
|
s.conf.Enabled = conf.Enabled
|
|
|
|
s.conf.InterfaceName = conf.InterfaceName
|
|
|
|
s.conf.HTTPRegister = conf.HTTPRegister
|
|
|
|
s.conf.ConfigModified = conf.ConfigModified
|
|
|
|
s.conf.DBFilePath = filepath.Join(conf.WorkDir, dbFilename)
|
2019-11-06 10:04:01 +00:00
|
|
|
|
2020-04-07 08:48:03 +00:00
|
|
|
if !webHandlersRegistered && s.conf.HTTPRegister != nil {
|
2020-11-16 16:01:12 +00:00
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
// Our DHCP server doesn't work on Windows yet, so
|
|
|
|
// signal that to the front with an HTTP 501.
|
|
|
|
//
|
|
|
|
// TODO(a.garipov): This needs refactoring. We
|
|
|
|
// shouldn't even try and initialize a DHCP server on
|
|
|
|
// Windows, but there are currently too many
|
|
|
|
// interconnected parts--such as HTTP handlers and
|
|
|
|
// frontend--to make that work properly.
|
|
|
|
s.registerNotImplementedHandlers()
|
|
|
|
} else {
|
|
|
|
s.registerHandlers()
|
|
|
|
}
|
|
|
|
|
2020-04-07 08:48:03 +00:00
|
|
|
webHandlersRegistered = true
|
|
|
|
}
|
|
|
|
|
2020-07-03 15:20:01 +00:00
|
|
|
var err4, err6 error
|
2021-02-11 17:49:03 +00:00
|
|
|
v4conf := conf.Conf4
|
2020-07-03 15:20:01 +00:00
|
|
|
v4conf.Enabled = s.conf.Enabled
|
|
|
|
if len(v4conf.RangeStart) == 0 {
|
|
|
|
v4conf.Enabled = false
|
|
|
|
}
|
|
|
|
v4conf.InterfaceName = s.conf.InterfaceName
|
|
|
|
v4conf.notify = s.onNotify
|
|
|
|
s.srv4, err4 = v4Create(v4conf)
|
|
|
|
|
2021-02-11 17:49:03 +00:00
|
|
|
v6conf := conf.Conf6
|
2020-07-03 15:20:01 +00:00
|
|
|
v6conf.Enabled = s.conf.Enabled
|
|
|
|
if len(v6conf.RangeStart) == 0 {
|
|
|
|
v6conf.Enabled = false
|
|
|
|
}
|
|
|
|
v6conf.InterfaceName = s.conf.InterfaceName
|
|
|
|
v6conf.notify = s.onNotify
|
|
|
|
s.srv6, err6 = v6Create(v6conf)
|
|
|
|
|
|
|
|
if err4 != nil {
|
|
|
|
log.Error("%s", err4)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
if err6 != nil {
|
|
|
|
log.Error("%s", err6)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-02-11 17:49:03 +00:00
|
|
|
s.conf.Conf4 = conf.Conf4
|
|
|
|
s.conf.Conf6 = conf.Conf6
|
|
|
|
|
2020-08-04 11:18:35 +00:00
|
|
|
if s.conf.Enabled && !v4conf.Enabled && !v6conf.Enabled {
|
|
|
|
log.Error("Can't enable DHCP server because neither DHCPv4 nor DHCPv6 servers are configured")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-11-06 10:04:01 +00:00
|
|
|
// we can't delay database loading until DHCP server is started,
|
|
|
|
// because we need static leases functionality available beforehand
|
|
|
|
s.dbLoad()
|
2020-11-16 16:01:12 +00:00
|
|
|
return s
|
2019-11-22 11:21:08 +00:00
|
|
|
}
|
|
|
|
|
2020-07-03 15:20:01 +00:00
|
|
|
// server calls this function after DB is updated
|
|
|
|
func (s *Server) onNotify(flags uint32) {
|
|
|
|
if flags == LeaseChangedDBStore {
|
|
|
|
s.dbStore()
|
|
|
|
return
|
2018-12-28 14:17:51 +00:00
|
|
|
}
|
2020-07-03 15:20:01 +00:00
|
|
|
|
|
|
|
s.notify(int(flags))
|
2019-05-14 12:49:53 +00:00
|
|
|
}
|
|
|
|
|
2020-01-30 07:25:02 +00:00
|
|
|
// SetOnLeaseChanged - set callback
|
2020-07-03 15:20:01 +00:00
|
|
|
func (s *Server) SetOnLeaseChanged(onLeaseChanged OnLeaseChangedT) {
|
2020-06-23 09:13:13 +00:00
|
|
|
s.onLeaseChanged = append(s.onLeaseChanged, onLeaseChanged)
|
2020-01-30 07:25:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Server) notify(flags int) {
|
2020-06-23 09:13:13 +00:00
|
|
|
if len(s.onLeaseChanged) == 0 {
|
2020-01-30 07:25:02 +00:00
|
|
|
return
|
|
|
|
}
|
2020-06-23 09:13:13 +00:00
|
|
|
for _, f := range s.onLeaseChanged {
|
|
|
|
f(flags)
|
|
|
|
}
|
2020-01-30 07:25:02 +00:00
|
|
|
}
|
|
|
|
|
2019-11-22 11:21:08 +00:00
|
|
|
// WriteDiskConfig - write configuration
|
|
|
|
func (s *Server) WriteDiskConfig(c *ServerConfig) {
|
2020-07-03 15:20:01 +00:00
|
|
|
c.Enabled = s.conf.Enabled
|
|
|
|
c.InterfaceName = s.conf.InterfaceName
|
|
|
|
s.srv4.WriteDiskConfig4(&c.Conf4)
|
|
|
|
s.srv6.WriteDiskConfig6(&c.Conf6)
|
2019-05-14 12:49:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Start will listen on port 67 and serve DHCP requests.
|
|
|
|
func (s *Server) Start() error {
|
2020-07-03 15:20:01 +00:00
|
|
|
err := s.srv4.Start()
|
2019-05-14 12:49:53 +00:00
|
|
|
if err != nil {
|
2020-07-03 15:20:01 +00:00
|
|
|
log.Error("DHCPv4: start: %s", err)
|
|
|
|
return err
|
2019-05-14 12:49:53 +00:00
|
|
|
}
|
2019-03-05 14:15:38 +00:00
|
|
|
|
2020-07-03 15:20:01 +00:00
|
|
|
err = s.srv6.Start()
|
2018-12-28 14:17:51 +00:00
|
|
|
if err != nil {
|
2020-07-03 15:20:01 +00:00
|
|
|
log.Error("DHCPv6: start: %s", err)
|
|
|
|
return err
|
2018-12-28 14:17:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-01-24 17:11:01 +00:00
|
|
|
// Stop closes the listening UDP socket
|
2020-07-03 15:20:01 +00:00
|
|
|
func (s *Server) Stop() {
|
|
|
|
s.srv4.Stop()
|
|
|
|
s.srv6.Stop()
|
2019-05-14 10:02:04 +00:00
|
|
|
}
|
|
|
|
|
2020-01-30 07:25:02 +00:00
|
|
|
// flags for Leases() function
|
|
|
|
const (
|
|
|
|
LeasesDynamic = 1
|
|
|
|
LeasesStatic = 2
|
|
|
|
LeasesAll = LeasesDynamic | LeasesStatic
|
|
|
|
)
|
|
|
|
|
2021-02-18 11:11:54 +00:00
|
|
|
// Leases returns the list of active IPv4 and IPv6 DHCP leases. It's safe for
|
|
|
|
// concurrent use.
|
|
|
|
func (s *Server) Leases(flags int) (leases []Lease) {
|
|
|
|
return append(s.srv4.GetLeases(flags), s.srv6.GetLeases(flags)...)
|
2019-05-28 11:11:47 +00:00
|
|
|
}
|
|
|
|
|
2019-09-26 13:40:52 +00:00
|
|
|
// FindMACbyIP - find a MAC address by IP address in the currently active DHCP leases
|
|
|
|
func (s *Server) FindMACbyIP(ip net.IP) net.HardwareAddr {
|
2020-07-03 15:20:01 +00:00
|
|
|
if ip.To4() != nil {
|
|
|
|
return s.srv4.FindMACbyIP(ip)
|
2019-12-23 13:59:02 +00:00
|
|
|
}
|
2020-07-03 15:20:01 +00:00
|
|
|
return s.srv6.FindMACbyIP(ip)
|
2019-09-26 13:40:52 +00:00
|
|
|
}
|
|
|
|
|
2020-07-03 15:20:01 +00:00
|
|
|
// AddStaticLease - add static v4 lease
|
|
|
|
func (s *Server) AddStaticLease(lease Lease) error {
|
|
|
|
return s.srv4.AddStaticLease(lease)
|
2019-03-01 17:03:22 +00:00
|
|
|
}
|
2020-08-25 11:07:11 +00:00
|
|
|
|
|
|
|
// Parse option string
|
|
|
|
// Format:
|
|
|
|
// CODE TYPE VALUE
|
|
|
|
func parseOptionString(s string) (uint8, []byte) {
|
|
|
|
s = strings.TrimSpace(s)
|
|
|
|
scode := util.SplitNext(&s, ' ')
|
|
|
|
t := util.SplitNext(&s, ' ')
|
|
|
|
sval := util.SplitNext(&s, ' ')
|
|
|
|
|
|
|
|
code, err := strconv.Atoi(scode)
|
|
|
|
if err != nil || code <= 0 || code > 255 {
|
|
|
|
return 0, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var val []byte
|
|
|
|
|
|
|
|
switch t {
|
|
|
|
case "hex":
|
|
|
|
val, err = hex.DecodeString(sval)
|
|
|
|
if err != nil {
|
|
|
|
return 0, nil
|
|
|
|
}
|
|
|
|
case "ip":
|
|
|
|
ip := net.ParseIP(sval)
|
|
|
|
if ip == nil {
|
|
|
|
return 0, nil
|
|
|
|
}
|
|
|
|
|
2021-02-16 14:58:42 +00:00
|
|
|
// Most DHCP options require IPv4, so do not put the 16-byte
|
|
|
|
// version if we can. Otherwise, the clients will receive weird
|
|
|
|
// data that looks like four IPv4 addresses.
|
|
|
|
//
|
|
|
|
// See https://github.com/AdguardTeam/AdGuardHome/issues/2688.
|
|
|
|
if ip4 := ip.To4(); ip4 != nil {
|
|
|
|
val = ip4
|
|
|
|
} else {
|
|
|
|
val = ip
|
|
|
|
}
|
2020-08-25 11:07:11 +00:00
|
|
|
default:
|
|
|
|
return 0, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return uint8(code), val
|
|
|
|
}
|