This commit is contained in:
Simon Zolin 2020-05-19 15:34:46 +03:00
parent f60d6f973d
commit 6280a1ad02
7 changed files with 440 additions and 738 deletions

View file

@ -25,12 +25,12 @@ func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) {
}
// get ipv4 address of an interface
ifaceIPNet := getIfaceIPv4(iface)
if ifaceIPNet == nil {
ifaceIPNet := getIfaceIPv4(*iface)
if len(ifaceIPNet) == 0 {
return false, fmt.Errorf("Couldn't find IPv4 address of interface %s %+v", ifaceName, iface)
}
srcIP := ifaceIPNet.IP
srcIP := ifaceIPNet[0]
src := net.JoinHostPort(srcIP.String(), "68")
dst := "255.255.255.255:67"

View file

@ -11,7 +11,6 @@ import (
"github.com/AdguardTeam/golibs/file"
"github.com/AdguardTeam/golibs/log"
"github.com/krolaw/dhcp4"
)
const dbFilename = "leases.db"
@ -31,22 +30,12 @@ func normalizeIP(ip net.IP) net.IP {
return ip
}
// Safe version of dhcp4.IPInRange()
func ipInRange(start, stop, ip net.IP) bool {
if len(start) != len(stop) ||
len(start) != len(ip) {
return false
}
return dhcp4.IPInRange(start, stop, ip)
}
// Load lease table from DB
func (s *Server) dbLoad() {
s.leases = nil
s.IPpool = make(map[[4]byte]net.HardwareAddr)
dynLeases := []*Lease{}
staticLeases := []*Lease{}
v6StaticLeases := []*Lease{}
v6DynLeases := []*Lease{}
data, err := ioutil.ReadFile(s.conf.DBFilePath)
if err != nil {
@ -72,14 +61,6 @@ func (s *Server) dbLoad() {
continue
}
if obj[i].Expiry != leaseExpireStatic &&
len(obj[i].IP) == 4 &&
!ipInRange(s.leaseStart, s.leaseStop, obj[i].IP) {
log.Tracef("Skipping a lease with IP %v: not within current IP range", obj[i].IP)
continue
}
lease := Lease{
HWAddr: obj[i].HWAddr,
IP: obj[i].IP,
@ -88,28 +69,31 @@ func (s *Server) dbLoad() {
}
if len(obj[i].IP) == 16 {
v6StaticLeases = append(v6StaticLeases, &lease)
if obj[i].Expiry == leaseExpireStatic {
v6StaticLeases = append(v6StaticLeases, &lease)
} else {
v6DynLeases = append(v6DynLeases, &lease)
}
} else if obj[i].Expiry == leaseExpireStatic {
staticLeases = append(staticLeases, &lease)
} else {
dynLeases = append(dynLeases, &lease)
if obj[i].Expiry == leaseExpireStatic {
staticLeases = append(staticLeases, &lease)
} else {
dynLeases = append(dynLeases, &lease)
}
}
}
s.leases = normalizeLeases(staticLeases, dynLeases)
leases4 := normalizeLeases(staticLeases, dynLeases)
s.srv4.ResetLeases(leases4)
for _, lease := range s.leases {
s.reserveIP(lease.IP, lease.HWAddr)
}
v6StaticLeases = normalizeLeases(v6StaticLeases, []*Lease{})
leases6 := normalizeLeases(v6StaticLeases, v6DynLeases)
if s.srv6 != nil {
s.srv6.leases = v6StaticLeases
s.srv6.ResetLeases(leases6)
}
log.Info("DHCP: loaded leases v4:%d v6:%d total-read:%d from DB",
len(s.leases), len(v6StaticLeases), numLeases)
len(leases4), len(leases6), numLeases)
}
// Skip duplicate leases
@ -143,15 +127,15 @@ func normalizeLeases(staticLeases, dynLeases []*Lease) []*Lease {
func (s *Server) dbStore() {
var leases []leaseJSON
for i := range s.leases {
if s.leases[i].Expiry.Unix() == 0 {
for _, l := range s.srv4.leases {
if l.Expiry.Unix() == 0 {
continue
}
lease := leaseJSON{
HWAddr: s.leases[i].HWAddr,
IP: s.leases[i].IP,
Hostname: s.leases[i].Hostname,
Expiry: s.leases[i].Expiry.Unix(),
HWAddr: l.HWAddr,
IP: l.IP,
Hostname: l.Hostname,
Expiry: l.Expiry.Unix(),
}
leases = append(leases, lease)
}

View file

@ -66,7 +66,7 @@ func (s *Server) handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
leases := convertLeases(s.Leases(LeasesDynamic), true)
staticLeases := convertLeases(s.Leases(LeasesStatic), false)
status := map[string]interface{}{
"config": s.conf,
"config": s.conf.Conf4,
"config_v6": v6ServerConfToJSON(s.conf.Conf6),
"leases": leases,
"static_leases": staticLeases,
@ -87,7 +87,7 @@ type staticLeaseJSON struct {
}
type dhcpServerConfigJSON struct {
ServerConfig `json:",inline"`
V4ServerConf `json:",inline"`
V6 v6ServerConfJSON `json:"v6"`
}
@ -99,18 +99,20 @@ func (s *Server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
return
}
err = s.CheckConfig(newconfig.ServerConfig)
if err != nil {
httpError(r, w, http.StatusBadRequest, "Invalid DHCP configuration: %s", err)
return
}
// err = s.CheckConfig(newconfig.ServerConfig)
// if err != nil {
// httpError(r, w, http.StatusBadRequest, "Invalid DHCP configuration: %s", err)
// return
// }
err = s.Stop()
if err != nil {
log.Error("failed to stop the DHCP server: %s", err)
}
err = s.Init(newconfig.ServerConfig)
v4conf := newconfig.V4ServerConf
v4conf.notify = s.conf.Conf4.notify
s4, err := v4Create(v4conf)
if err != nil {
httpError(r, w, http.StatusBadRequest, "Invalid DHCP configuration: %s", err)
return
@ -123,6 +125,8 @@ func (s *Server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
httpError(r, w, http.StatusBadRequest, "Invalid DHCPv6 configuration: %s", err)
return
}
s.srv4 = s4
s.srv6 = s6
s.conf.ConfigModified()
@ -317,7 +321,7 @@ func (s *Server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request
HWAddr: mac,
Hostname: lj.Hostname,
}
err = s.AddStaticLease(lease)
err = s.srv4.AddStaticLease(lease)
if err != nil {
httpError(r, w, http.StatusBadRequest, "%s", err)
return
@ -367,7 +371,7 @@ func (s *Server) handleDHCPRemoveStaticLease(w http.ResponseWriter, r *http.Requ
HWAddr: mac,
Hostname: lj.Hostname,
}
err = s.RemoveStaticLease(lease)
err = s.srv4.RemoveStaticLease(lease)
if err != nil {
httpError(r, w, http.StatusBadRequest, "%s", err)
return
@ -387,12 +391,12 @@ func (s *Server) handleReset(w http.ResponseWriter, r *http.Request) {
oldconf := s.conf
s.conf = ServerConfig{}
s.conf.LeaseDuration = 86400
s.conf.ICMPTimeout = 1000
s.conf.WorkDir = oldconf.WorkDir
s.conf.HTTPRegister = oldconf.HTTPRegister
s.conf.ConfigModified = oldconf.ConfigModified
s.conf.DBFilePath = oldconf.DBFilePath
s.conf.ConfigModified()
}

View file

@ -1,18 +1,14 @@
package dhcpd
import (
"bytes"
"fmt"
"net"
"net/http"
"path/filepath"
"strings"
"sync"
"time"
"github.com/AdguardTeam/golibs/log"
"github.com/krolaw/dhcp4"
ping "github.com/sparrc/go-ping"
)
const defaultDiscoverTime = time.Second * 3
@ -35,18 +31,6 @@ type Lease struct {
// ServerConfig - DHCP server configuration
// field ordering is important -- yaml fields will mirror ordering from here
type ServerConfig struct {
Enabled bool `json:"enabled" yaml:"enabled"`
InterfaceName string `json:"interface_name" yaml:"interface_name"` // eth0, en0 and so on
GatewayIP string `json:"gateway_ip" yaml:"gateway_ip"`
SubnetMask string `json:"subnet_mask" yaml:"subnet_mask"`
RangeStart string `json:"range_start" yaml:"range_start"`
RangeEnd string `json:"range_end" yaml:"range_end"`
LeaseDuration uint32 `json:"lease_duration" yaml:"lease_duration"` // in seconds
// IP conflict detector: time (ms) to wait for ICMP reply.
// 0: disable
ICMPTimeout uint32 `json:"icmp_timeout_msec" yaml:"icmp_timeout_msec"`
Conf4 V4ServerConf `json:"-" yaml:"dhcpv4"`
Conf6 V6ServerConf `json:"-" yaml:"dhcpv6"`
@ -68,26 +52,23 @@ const (
LeaseChangedAddedStatic
LeaseChangedRemovedStatic
LeaseChangedBlacklisted
LeaseChangedDBStore
)
// Server - the current state of the DHCP server
type Server struct {
conn *filterConn // listening UDP socket
// conn *filterConn // listening UDP socket
ipnet *net.IPNet // if interface name changes, this needs to be reset
// ipnet *net.IPNet // if interface name changes, this needs to be reset
cond *sync.Cond // Synchronize worker thread with main thread
mutex sync.Mutex // Mutex for 'cond'
running bool // Set if the worker thread is running
stopping bool // Set if the worker thread should be stopped
// cond *sync.Cond // Synchronize worker thread with main thread
// mutex sync.Mutex // Mutex for 'cond'
// running bool // Set if the worker thread is running
// stopping bool // Set if the worker thread should be stopped
// leases
leases []*Lease
leasesLock sync.RWMutex
leaseStart net.IP // parsed from config RangeStart
leaseStop net.IP // parsed from config RangeEnd
leaseTime time.Duration // parsed from config LeaseDuration
leaseOptions dhcp4.Options // parsed from config GatewayIP and SubnetMask
// leaseOptions dhcp4.Options // parsed from config GatewayIP and SubnetMask
srv4 *V4Server
srv6 *V6Server
@ -110,23 +91,15 @@ func printInterfaces() {
// CheckConfig checks the configuration
func (s *Server) CheckConfig(config ServerConfig) error {
tmpServer := Server{}
return tmpServer.setConfig(config)
return nil
}
// Create - create object
func Create(config ServerConfig) *Server {
s := Server{}
s.conf = config
s.conf.Conf6.notify = s.notify6
s.conf.Conf4.notify = s.onNotify
s.conf.Conf6.notify = s.onNotify
s.conf.DBFilePath = filepath.Join(config.WorkDir, dbFilename)
if s.conf.Enabled {
err := s.setConfig(config)
if err != nil {
log.Error("DHCP: %s", err)
return nil
}
}
if !webHandlersRegistered && s.conf.HTTPRegister != nil {
webHandlersRegistered = true
@ -152,17 +125,18 @@ func Create(config ServerConfig) *Server {
return &s
}
// v6 server calls this function after DB is updated
func (s *Server) notify6(flags uint32) {
s.dbStore()
// server calls this function after DB is updated
func (s *Server) onNotify(flags uint32) {
if flags == LeaseChangedDBStore {
s.dbStore()
return
}
s.notify(int(flags))
}
// Init checks the configuration and initializes the server
func (s *Server) Init(config ServerConfig) error {
err := s.setConfig(config)
if err != nil {
return err
}
return nil
}
@ -180,84 +154,18 @@ func (s *Server) notify(flags int) {
// WriteDiskConfig - write configuration
func (s *Server) WriteDiskConfig(c *ServerConfig) {
*c = s.conf
s.srv4.WriteDiskConfig(&c.Conf4)
s.srv6.WriteDiskConfig(&c.Conf6)
}
func (s *Server) setConfig(config ServerConfig) error {
iface, err := net.InterfaceByName(config.InterfaceName)
if err != nil {
printInterfaces()
return wrapErrPrint(err, "Couldn't find interface by name %s", config.InterfaceName)
}
// get ipv4 address of an interface
s.ipnet = getIfaceIPv4(iface)
if s.ipnet == nil {
return wrapErrPrint(err, "Couldn't find IPv4 address of interface %s %+v", config.InterfaceName, iface)
}
if config.LeaseDuration == 0 {
s.leaseTime = time.Hour * 2
} else {
s.leaseTime = time.Second * time.Duration(config.LeaseDuration)
}
s.leaseStart, err = parseIPv4(config.RangeStart)
if err != nil {
return wrapErrPrint(err, "Failed to parse range start address %s", config.RangeStart)
}
s.leaseStop, err = parseIPv4(config.RangeEnd)
if err != nil {
return wrapErrPrint(err, "Failed to parse range end address %s", config.RangeEnd)
}
if dhcp4.IPRange(s.leaseStart, s.leaseStop) <= 0 {
return wrapErrPrint(err, "DHCP: Incorrect range_start/range_end values")
}
subnet, err := parseIPv4(config.SubnetMask)
if err != nil || !isValidSubnetMask(subnet) {
return wrapErrPrint(err, "Failed to parse subnet mask %s", config.SubnetMask)
}
// if !bytes.Equal(subnet, s.ipnet.Mask) {
// return wrapErrPrint(err, "specified subnet mask %s does not meatch interface %s subnet mask %s", s.SubnetMask, s.InterfaceName, s.ipnet.Mask)
// }
router, err := parseIPv4(config.GatewayIP)
if err != nil {
return wrapErrPrint(err, "Failed to parse gateway IP %s", config.GatewayIP)
}
s.leaseOptions = dhcp4.Options{
dhcp4.OptionSubnetMask: subnet,
dhcp4.OptionRouter: router,
dhcp4.OptionDomainNameServer: s.ipnet.IP,
}
oldconf := s.conf
s.conf = config
s.conf.WorkDir = oldconf.WorkDir
s.conf.HTTPRegister = oldconf.HTTPRegister
s.conf.ConfigModified = oldconf.ConfigModified
s.conf.DBFilePath = oldconf.DBFilePath
return nil
}
// Start will listen on port 67 and serve DHCP requests.
func (s *Server) Start() error {
iface, err := net.InterfaceByName(s.conf.InterfaceName)
if err != nil {
return wrapErrPrint(err, "Couldn't find interface by name %s", s.conf.InterfaceName)
}
err = s.srv4.Start(*iface)
err := s.srv4.Start()
if err != nil {
return err
}
err = s.srv6.Start(*iface)
err = s.srv6.Start()
if err != nil {
return err
}
@ -269,461 +177,6 @@ func (s *Server) Start() error {
func (s *Server) Stop() error {
s.srv4.Stop()
s.srv6.Stop()
/* if s.conn == nil {
// nothing to do, return silently
return nil
}
s.stopping = true
err := s.closeConn()
if err != nil {
return wrapErrPrint(err, "Couldn't close UDP listening socket")
}
// We've just closed the listening socket.
// Worker thread should exit right after it tries to read from the socket.
s.mutex.Lock()
for s.running {
s.cond.Wait()
}
s.mutex.Unlock() */
return nil
}
// closeConn will close the connection and set it to zero
func (s *Server) closeConn() error {
if s.conn == nil {
return nil
}
err := s.conn.Close()
s.conn = nil
return err
}
// Reserve a lease for the client
func (s *Server) reserveLease(p dhcp4.Packet) (*Lease, error) {
// WARNING: do not remove copy()
// the given hwaddr by p.CHAddr() in the packet survives only during ServeDHCP() call
// since we need to retain it we need to make our own copy
hwaddrCOW := p.CHAddr()
hwaddr := make(net.HardwareAddr, len(hwaddrCOW))
copy(hwaddr, hwaddrCOW)
// not assigned a lease, create new one, find IP from LRU
hostname := p.ParseOptions()[dhcp4.OptionHostName]
lease := &Lease{HWAddr: hwaddr, Hostname: string(hostname)}
log.Tracef("Lease not found for %s: creating new one", hwaddr)
s.leasesLock.Lock()
defer s.leasesLock.Unlock()
ip, err := s.findFreeIP(hwaddr)
if err != nil {
i := s.findExpiredLease()
if i < 0 {
return nil, wrapErrPrint(err, "Couldn't find free IP for the lease %s", hwaddr.String())
}
log.Tracef("Assigning IP address %s to %s (lease for %s expired at %s)",
s.leases[i].IP, hwaddr, s.leases[i].HWAddr, s.leases[i].Expiry)
lease.IP = s.leases[i].IP
s.leases[i] = lease
s.reserveIP(lease.IP, hwaddr)
return lease, nil
}
log.Tracef("Assigning to %s IP address %s", hwaddr, ip.String())
lease.IP = ip
s.leases = append(s.leases, lease)
return lease, nil
}
// Find a lease for the client
func (s *Server) findLease(p dhcp4.Packet) *Lease {
hwaddr := p.CHAddr()
for i := range s.leases {
if bytes.Equal([]byte(hwaddr), []byte(s.leases[i].HWAddr)) {
// log.Tracef("bytes.Equal(%s, %s) returned true", hwaddr, s.leases[i].hwaddr)
return s.leases[i]
}
}
return nil
}
// Find an expired lease and return its index or -1
func (s *Server) findExpiredLease() int {
now := time.Now().Unix()
for i, lease := range s.leases {
if lease.Expiry.Unix() <= now && lease.Expiry.Unix() != leaseExpireStatic {
return i
}
}
return -1
}
func (s *Server) findFreeIP(hwaddr net.HardwareAddr) (net.IP, error) {
// go from start to end, find unreserved IP
var foundIP net.IP
for i := 0; i < dhcp4.IPRange(s.leaseStart, s.leaseStop); i++ {
newIP := dhcp4.IPAdd(s.leaseStart, i)
foundHWaddr := s.findReservedHWaddr(newIP)
log.Tracef("tried IP %v, got hwaddr %v", newIP, foundHWaddr)
if foundHWaddr != nil && len(foundHWaddr) != 0 {
// if !bytes.Equal(foundHWaddr, hwaddr) {
// log.Tracef("SHOULD NOT HAPPEN: hwaddr in IP pool %s is not equal to hwaddr in lease %s", foundHWaddr, hwaddr)
// }
continue
}
foundIP = newIP
break
}
if foundIP == nil {
// TODO: LRU
return nil, fmt.Errorf("couldn't find free entry in IP pool")
}
s.reserveIP(foundIP, hwaddr)
return foundIP, nil
}
func (s *Server) findReservedHWaddr(ip net.IP) net.HardwareAddr {
rawIP := []byte(ip)
IP4 := [4]byte{rawIP[0], rawIP[1], rawIP[2], rawIP[3]}
return s.IPpool[IP4]
}
func (s *Server) reserveIP(ip net.IP, hwaddr net.HardwareAddr) {
rawIP := []byte(ip)
IP4 := [4]byte{rawIP[0], rawIP[1], rawIP[2], rawIP[3]}
s.IPpool[IP4] = hwaddr
}
func (s *Server) unreserveIP(ip net.IP) {
rawIP := []byte(ip)
IP4 := [4]byte{rawIP[0], rawIP[1], rawIP[2], rawIP[3]}
delete(s.IPpool, IP4)
}
// ServeDHCP handles an incoming DHCP request
func (s *Server) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options dhcp4.Options) dhcp4.Packet {
s.printLeases()
switch msgType {
case dhcp4.Discover: // Broadcast Packet From Client - Can I have an IP?
return s.handleDiscover(p, options)
case dhcp4.Request: // Broadcast From Client - I'll take that IP (Also start for renewals)
// start/renew a lease -- update lease time
// some clients (OSX) just go right ahead and do Request first from previously known IP, if they get NAK, they restart full cycle with Discover then Request
return s.handleDHCP4Request(p, options)
case dhcp4.Decline: // Broadcast From Client - Sorry I can't use that IP
return s.handleDecline(p, options)
case dhcp4.Release: // From Client, I don't need that IP anymore
return s.handleRelease(p, options)
case dhcp4.Inform: // From Client, I have this IP and there's nothing you can do about it
return s.handleInform(p, options)
// from server -- ignore those but enumerate just in case
case dhcp4.Offer: // Broadcast From Server - Here's an IP
log.Printf("DHCP: received message from %s: Offer", p.CHAddr())
case dhcp4.ACK: // From Server, Yes you can have that IP
log.Printf("DHCP: received message from %s: ACK", p.CHAddr())
case dhcp4.NAK: // From Server, No you cannot have that IP
log.Printf("DHCP: received message from %s: NAK", p.CHAddr())
default:
log.Printf("DHCP: unknown packet %v from %s", msgType, p.CHAddr())
return nil
}
return nil
}
// Send ICMP to the specified machine
// Return TRUE if it doesn't reply, which probably means that the IP is available
func (s *Server) addrAvailable(target net.IP) bool {
if s.conf.ICMPTimeout == 0 {
return true
}
pinger, err := ping.NewPinger(target.String())
if err != nil {
log.Error("ping.NewPinger(): %v", err)
return true
}
pinger.SetPrivileged(true)
pinger.Timeout = time.Duration(s.conf.ICMPTimeout) * time.Millisecond
pinger.Count = 1
reply := false
pinger.OnRecv = func(pkt *ping.Packet) {
// log.Tracef("Received ICMP Reply from %v", target)
reply = true
}
log.Tracef("Sending ICMP Echo to %v", target)
pinger.Run()
if reply {
log.Info("DHCP: IP conflict: %v is already used by another device", target)
return false
}
log.Tracef("ICMP procedure is complete: %v", target)
return true
}
// Add the specified IP to the black list for a time period
func (s *Server) blacklistLease(lease *Lease) {
hw := make(net.HardwareAddr, 6)
s.leasesLock.Lock()
s.reserveIP(lease.IP, hw)
lease.HWAddr = hw
lease.Hostname = ""
lease.Expiry = time.Now().Add(s.leaseTime)
s.dbStore()
s.leasesLock.Unlock()
s.notify(LeaseChangedBlacklisted)
}
// Return TRUE if DHCP packet is correct
func isValidPacket(p dhcp4.Packet) bool {
hw := p.CHAddr()
zeroes := make([]byte, len(hw))
if bytes.Equal(hw, zeroes) {
log.Tracef("Packet has empty CHAddr")
return false
}
return true
}
func (s *Server) handleDiscover(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet {
// find a lease, but don't update lease time
var lease *Lease
var err error
reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress])
hostname := p.ParseOptions()[dhcp4.OptionHostName]
log.Tracef("Message from client: Discover. ReqIP: %s HW: %s Hostname: %s",
reqIP, p.CHAddr(), hostname)
if !isValidPacket(p) {
return nil
}
lease = s.findLease(p)
for lease == nil {
lease, err = s.reserveLease(p)
if err != nil {
log.Error("Couldn't find free lease: %s", err)
return nil
}
if !s.addrAvailable(lease.IP) {
s.blacklistLease(lease)
lease = nil
continue
}
break
}
opt := s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList])
reply := dhcp4.ReplyPacket(p, dhcp4.Offer, s.ipnet.IP, lease.IP, s.leaseTime, opt)
log.Tracef("Replying with offer: offered IP %v for %v with options %+v", lease.IP, s.leaseTime, reply.ParseOptions())
return reply
}
func (s *Server) handleDHCP4Request(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet {
var lease *Lease
reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress])
log.Tracef("Message from client: Request. IP: %s ReqIP: %s HW: %s",
p.CIAddr(), reqIP, p.CHAddr())
if !isValidPacket(p) {
return nil
}
server := options[dhcp4.OptionServerIdentifier]
if server != nil && !net.IP(server).Equal(s.ipnet.IP) {
log.Tracef("Request message not for this DHCP server (%v vs %v)", server, s.ipnet.IP)
return nil // Message not for this dhcp server
}
if reqIP == nil {
reqIP = p.CIAddr()
} else if reqIP == nil || reqIP.To4() == nil {
log.Tracef("Requested IP isn't a valid IPv4: %s", reqIP)
return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil)
}
lease = s.findLease(p)
if lease == nil {
log.Tracef("Lease for %s isn't found", p.CHAddr())
return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil)
}
if !lease.IP.Equal(reqIP) {
log.Tracef("Lease for %s doesn't match requested/client IP: %s vs %s",
lease.HWAddr, lease.IP, reqIP)
return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil)
}
if lease.Expiry.Unix() != leaseExpireStatic {
lease.Expiry = time.Now().Add(s.leaseTime)
s.leasesLock.Lock()
s.dbStore()
s.leasesLock.Unlock()
s.notify(LeaseChangedAdded) // Note: maybe we shouldn't call this function if only expiration time is updated
}
log.Tracef("Replying with ACK. IP: %s HW: %s Expire: %s",
lease.IP, lease.HWAddr, lease.Expiry)
opt := s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList])
return dhcp4.ReplyPacket(p, dhcp4.ACK, s.ipnet.IP, lease.IP, s.leaseTime, opt)
}
func (s *Server) handleInform(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet {
log.Tracef("Message from client: Inform. IP: %s HW: %s",
p.CIAddr(), p.CHAddr())
return nil
}
func (s *Server) handleRelease(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet {
log.Tracef("Message from client: Release. IP: %s HW: %s",
p.CIAddr(), p.CHAddr())
return nil
}
func (s *Server) handleDecline(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet {
reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress])
log.Tracef("Message from client: Decline. IP: %s HW: %s",
reqIP, p.CHAddr())
return nil
}
// AddStaticLease adds a static lease (thread-safe)
func (s *Server) AddStaticLease(l Lease) error {
if len(l.IP) != 4 {
return fmt.Errorf("invalid IP")
}
if len(l.HWAddr) != 6 {
return fmt.Errorf("invalid MAC")
}
l.Expiry = time.Unix(leaseExpireStatic, 0)
s.leasesLock.Lock()
if s.findReservedHWaddr(l.IP) != nil {
err := s.rmDynamicLeaseWithIP(l.IP)
if err != nil {
s.leasesLock.Unlock()
return err
}
} else {
err := s.rmDynamicLeaseWithMAC(l.HWAddr)
if err != nil {
s.leasesLock.Unlock()
return err
}
}
s.leases = append(s.leases, &l)
s.reserveIP(l.IP, l.HWAddr)
s.dbStore()
s.leasesLock.Unlock()
s.notify(LeaseChangedAddedStatic)
return nil
}
// Remove a dynamic lease by IP address
func (s *Server) rmDynamicLeaseWithIP(ip net.IP) error {
var newLeases []*Lease
for _, lease := range s.leases {
if net.IP.Equal(lease.IP.To4(), ip) {
if lease.Expiry.Unix() == leaseExpireStatic {
return fmt.Errorf("static lease with the same IP already exists")
}
continue
}
newLeases = append(newLeases, lease)
}
s.leases = newLeases
s.unreserveIP(ip)
return nil
}
// Remove a dynamic lease by IP address
func (s *Server) rmDynamicLeaseWithMAC(mac net.HardwareAddr) error {
var newLeases []*Lease
for _, lease := range s.leases {
if bytes.Equal(lease.HWAddr, mac) {
if lease.Expiry.Unix() == leaseExpireStatic {
return fmt.Errorf("static lease with the same IP already exists")
}
s.unreserveIP(lease.IP)
continue
}
newLeases = append(newLeases, lease)
}
s.leases = newLeases
return nil
}
// Remove a lease
func (s *Server) rmLease(l Lease) error {
var newLeases []*Lease
for _, lease := range s.leases {
if net.IP.Equal(lease.IP.To4(), l.IP) {
if !bytes.Equal(lease.HWAddr, l.HWAddr) ||
lease.Hostname != l.Hostname {
return fmt.Errorf("Lease not found")
}
continue
}
newLeases = append(newLeases, lease)
}
s.leases = newLeases
s.unreserveIP(l.IP)
return nil
}
// RemoveStaticLease removes a static lease (thread-safe)
func (s *Server) RemoveStaticLease(l Lease) error {
if len(l.IP) != 4 {
return fmt.Errorf("invalid IP")
}
if len(l.HWAddr) != 6 {
return fmt.Errorf("invalid MAC")
}
s.leasesLock.Lock()
if s.findReservedHWaddr(l.IP) == nil {
s.leasesLock.Unlock()
return fmt.Errorf("lease not found")
}
err := s.rmLease(l)
if err != nil {
s.leasesLock.Unlock()
return err
}
s.dbStore()
s.leasesLock.Unlock()
s.notify(LeaseChangedRemovedStatic)
return nil
}
@ -736,16 +189,7 @@ const (
// Leases returns the list of current DHCP leases (thread-safe)
func (s *Server) Leases(flags int) []Lease {
var result []Lease
now := time.Now().Unix()
s.leasesLock.RLock()
for _, lease := range s.leases {
if ((flags&LeasesDynamic) != 0 && lease.Expiry.Unix() > now) ||
((flags&LeasesStatic) != 0 && lease.Expiry.Unix() == leaseExpireStatic) {
result = append(result, *lease)
}
}
s.leasesLock.RUnlock()
result := s.srv4.GetLeases(flags)
if s.srv6 != nil {
v6leases := s.srv6.GetLeases(flags)
@ -755,49 +199,12 @@ func (s *Server) Leases(flags int) []Lease {
return result
}
// Print information about the current leases
func (s *Server) printLeases() {
log.Tracef("Leases:")
for i, lease := range s.leases {
log.Tracef("Lease #%d: hwaddr %s, ip %s, expiry %s",
i, lease.HWAddr, lease.IP, lease.Expiry)
}
}
// FindIPbyMAC finds an IP address by MAC address in the currently active DHCP leases
func (s *Server) FindIPbyMAC(mac net.HardwareAddr) net.IP {
now := time.Now().Unix()
s.leasesLock.RLock()
defer s.leasesLock.RUnlock()
for _, l := range s.leases {
if l.Expiry.Unix() > now && bytes.Equal(mac, l.HWAddr) {
return l.IP
}
}
return nil
}
// FindMACbyIP - find a MAC address by IP address in the currently active DHCP leases
func (s *Server) FindMACbyIP(ip net.IP) net.HardwareAddr {
now := time.Now().Unix()
s.leasesLock.RLock()
defer s.leasesLock.RUnlock()
ip4 := ip.To4()
if ip4 == nil {
return nil
if ip.To4() != nil {
return s.srv4.FindMACbyIP4(ip)
}
for _, l := range s.leases {
if l.IP.Equal(ip4) {
unix := l.Expiry.Unix()
if unix > now || unix == leaseExpireStatic {
return l.HWAddr
}
}
}
return nil
return s.srv6.FindMACbyIP6(ip)
}
// Reset internal state

View file

@ -17,34 +17,6 @@ func isTimeout(err error) bool {
return operr.Timeout()
}
// return first IPv4 address of an interface, if there is any
func getIfaceIPv4(iface *net.Interface) *net.IPNet {
ifaceAddrs, err := iface.Addrs()
if err != nil {
panic(err)
}
for _, addr := range ifaceAddrs {
ipnet, ok := addr.(*net.IPNet)
if !ok {
// not an IPNet, should not happen
log.Fatalf("SHOULD NOT HAPPEN: got iface.Addrs() element %s that is not net.IPNet", addr)
}
if ipnet.IP.To4() == nil {
log.Tracef("Got IP that is not IPv4: %v", ipnet.IP)
continue
}
log.Tracef("Got IP that is IPv4: %v", ipnet.IP)
return &net.IPNet{
IP: ipnet.IP.To4(),
Mask: ipnet.Mask,
}
}
return nil
}
func wrapErrPrint(err error, message string, args ...interface{}) error {
var errx error
if err == nil {

View file

@ -1,13 +1,16 @@
package dhcpd
import (
"bytes"
"fmt"
"net"
"sync"
"time"
"github.com/AdguardTeam/golibs/log"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv4/server4"
"github.com/krolaw/dhcp4"
"github.com/sparrc/go-ping"
)
// V4Server - DHCPv4 server
@ -23,44 +26,326 @@ type V4Server struct {
// V4ServerConf - server configuration
type V4ServerConf struct {
Enabled bool `yaml:"enabled"`
// RangeStart string `yaml:"range_start"`
LeaseDuration uint32 `yaml:"lease_duration"` // in seconds
Enabled bool `json:"enabled" yaml:"enabled"`
InterfaceName string `json:"interface_name" yaml:"interface_name"` // eth0, en0 and so on
GatewayIP string `json:"gateway_ip" yaml:"gateway_ip"`
SubnetMask string `json:"subnet_mask" yaml:"subnet_mask"`
RangeStart string `json:"range_start" yaml:"range_start"`
RangeEnd string `json:"range_end" yaml:"range_end"`
LeaseDuration uint32 `json:"lease_duration" yaml:"lease_duration"` // in seconds
// ipStart net.IP
leaseTime time.Duration
// dnsIPAddrs []net.IP // IPv6 addresses to return to DHCP clients as DNS server addresses
// sid dhcpv6.Duid
// IP conflict detector: time (ms) to wait for ICMP reply.
// 0: disable
ICMPTimeout uint32 `json:"icmp_timeout_msec" yaml:"icmp_timeout_msec"`
// notify func(uint32)
ipStart net.IP
ipStop net.IP
leaseTime time.Duration
dnsIPAddrs []net.IP // IPv4 addresses to return to DHCP clients as DNS server addresses
notify func(uint32)
}
// WriteDiskConfig - write configuration
func (s *V4Server) WriteDiskConfig(c *V4ServerConf) {
*c = s.conf
}
func ipInRange(start, stop, ip net.IP) bool {
if len(start) != len(stop) ||
len(start) != len(ip) {
return false
}
// return dhcp4.IPInRange(start, stop, ip)
return false
}
// ResetLeases - reset leases
func (s *V4Server) ResetLeases(ll []*Lease) {
s.leases = nil
s.IPpool = make(map[[4]byte]net.HardwareAddr)
for _, l := range ll {
if l.Expiry.Unix() != leaseExpireStatic &&
!ipInRange(s.conf.ipStart, s.conf.ipStop, l.IP) {
log.Tracef("DHCPv4: skipping a lease with IP %v: not within current IP range", l.IP)
continue
}
s.leases = append(s.leases, l)
s.reserveIP(l.IP, l.HWAddr)
}
}
// GetLeases returns the list of current DHCP leases (thread-safe)
func (s *V4Server) GetLeases(flags int) []Lease {
var result []Lease
now := time.Now().Unix()
s.leasesLock.Lock()
for _, lease := range s.leases {
if ((flags&LeasesDynamic) != 0 && lease.Expiry.Unix() > now) ||
((flags&LeasesStatic) != 0 && lease.Expiry.Unix() == leaseExpireStatic) {
result = append(result, *lease)
}
}
s.leasesLock.Unlock()
return result
}
// FindMACbyIP4 - find a MAC address by IP address in the currently active DHCP leases
func (s *V4Server) FindMACbyIP4(ip net.IP) net.HardwareAddr {
now := time.Now().Unix()
s.leasesLock.Lock()
defer s.leasesLock.Unlock()
ip4 := ip.To4()
if ip4 == nil {
return nil
}
for _, l := range s.leases {
if l.IP.Equal(ip4) {
unix := l.Expiry.Unix()
if unix > now || unix == leaseExpireStatic {
return l.HWAddr
}
}
}
return nil
}
func (s *V4Server) reserveIP(ip net.IP, hwaddr net.HardwareAddr) {
rawIP := []byte(ip)
IP4 := [4]byte{rawIP[0], rawIP[1], rawIP[2], rawIP[3]}
s.IPpool[IP4] = hwaddr
}
func (s *V4Server) unreserveIP(ip net.IP) {
rawIP := []byte(ip)
IP4 := [4]byte{rawIP[0], rawIP[1], rawIP[2], rawIP[3]}
delete(s.IPpool, IP4)
}
func (s *V4Server) findReservedHWaddr(ip net.IP) net.HardwareAddr {
rawIP := []byte(ip)
IP4 := [4]byte{rawIP[0], rawIP[1], rawIP[2], rawIP[3]}
return s.IPpool[IP4]
}
// Add the specified IP to the black list for a time period
func (s *V4Server) blacklistLease(lease *Lease) {
hw := make(net.HardwareAddr, 6)
s.leasesLock.Lock()
s.reserveIP(lease.IP, hw)
lease.HWAddr = hw
lease.Hostname = ""
lease.Expiry = time.Now().Add(s.conf.leaseTime)
s.conf.notify(LeaseChangedDBStore)
s.leasesLock.Unlock()
s.conf.notify(LeaseChangedBlacklisted)
}
// Remove a dynamic lease by IP address
func (s *V4Server) rmDynamicLeaseWithIP(ip net.IP) error {
var newLeases []*Lease
for _, lease := range s.leases {
if net.IP.Equal(lease.IP.To4(), ip) {
if lease.Expiry.Unix() == leaseExpireStatic {
return fmt.Errorf("static lease with the same IP already exists")
}
continue
}
newLeases = append(newLeases, lease)
}
s.leases = newLeases
s.unreserveIP(ip)
return nil
}
// Remove a dynamic lease by IP address
func (s *V4Server) rmDynamicLeaseWithMAC(mac net.HardwareAddr) error {
var newLeases []*Lease
for _, lease := range s.leases {
if bytes.Equal(lease.HWAddr, mac) {
if lease.Expiry.Unix() == leaseExpireStatic {
return fmt.Errorf("static lease with the same IP already exists")
}
s.unreserveIP(lease.IP)
continue
}
newLeases = append(newLeases, lease)
}
s.leases = newLeases
return nil
}
// Remove a lease
func (s *V4Server) rmLease(l Lease) error {
var newLeases []*Lease
for _, lease := range s.leases {
if net.IP.Equal(lease.IP.To4(), l.IP) {
if !bytes.Equal(lease.HWAddr, l.HWAddr) ||
lease.Hostname != l.Hostname {
return fmt.Errorf("Lease not found")
}
continue
}
newLeases = append(newLeases, lease)
}
s.leases = newLeases
s.unreserveIP(l.IP)
return nil
}
// AddStaticLease adds a static lease (thread-safe)
func (s *V4Server) AddStaticLease(l Lease) error {
if len(l.IP) != 4 {
return fmt.Errorf("invalid IP")
}
if len(l.HWAddr) != 6 {
return fmt.Errorf("invalid MAC")
}
l.Expiry = time.Unix(leaseExpireStatic, 0)
s.leasesLock.Lock()
if s.findReservedHWaddr(l.IP) != nil {
err := s.rmDynamicLeaseWithIP(l.IP)
if err != nil {
s.leasesLock.Unlock()
return err
}
} else {
err := s.rmDynamicLeaseWithMAC(l.HWAddr)
if err != nil {
s.leasesLock.Unlock()
return err
}
}
s.leases = append(s.leases, &l)
s.reserveIP(l.IP, l.HWAddr)
s.conf.notify(LeaseChangedDBStore)
s.leasesLock.Unlock()
s.conf.notify(LeaseChangedAddedStatic)
return nil
}
// RemoveStaticLease removes a static lease (thread-safe)
func (s *V4Server) RemoveStaticLease(l Lease) error {
if len(l.IP) != 4 {
return fmt.Errorf("invalid IP")
}
if len(l.HWAddr) != 6 {
return fmt.Errorf("invalid MAC")
}
s.leasesLock.Lock()
if s.findReservedHWaddr(l.IP) == nil {
s.leasesLock.Unlock()
return fmt.Errorf("lease not found")
}
err := s.rmLease(l)
if err != nil {
s.leasesLock.Unlock()
return err
}
s.conf.notify(LeaseChangedDBStore)
s.leasesLock.Unlock()
s.conf.notify(LeaseChangedRemovedStatic)
return nil
}
// Send ICMP to the specified machine
// Return TRUE if it doesn't reply, which probably means that the IP is available
func (s *V4Server) addrAvailable(target net.IP) bool {
if s.conf.ICMPTimeout == 0 {
return true
}
pinger, err := ping.NewPinger(target.String())
if err != nil {
log.Error("ping.NewPinger(): %v", err)
return true
}
pinger.SetPrivileged(true)
pinger.Timeout = time.Duration(s.conf.ICMPTimeout) * time.Millisecond
pinger.Count = 1
reply := false
pinger.OnRecv = func(pkt *ping.Packet) {
// log.Tracef("Received ICMP Reply from %v", target)
reply = true
}
log.Tracef("Sending ICMP Echo to %v", target)
pinger.Run()
if reply {
log.Info("DHCP: IP conflict: %v is already used by another device", target)
return false
}
log.Tracef("ICMP procedure is complete: %v", target)
return true
}
func (s *V4Server) packetHandler(conn net.PacketConn, peer net.Addr, m *dhcpv4.DHCPv4) {
}
// Get IPv4 address list
func getIfaceIPv4(iface net.Interface) []net.IP {
addrs, err := iface.Addrs()
if err != nil {
return nil
}
var res []net.IP
for _, a := range addrs {
ipnet, ok := a.(*net.IPNet)
if !ok {
continue
}
if ipnet.IP.To4() != nil {
res = append(res, ipnet.IP)
}
}
return res
}
// Start - start server
func (s *V4Server) Start(iface net.Interface) error {
if s.conn != nil {
_ = s.closeConn()
}
c, err := newFilterConn(iface, ":67") // it has to be bound to 0.0.0.0:67, otherwise it won't see DHCP discover/request packets
func (s *V4Server) Start() error {
iface, err := net.InterfaceByName(s.conf.InterfaceName)
if err != nil {
return wrapErrPrint(err, "Couldn't start listening socket on 0.0.0.0:67")
return wrapErrPrint(err, "DHCPv4: Couldn't find interface by name %s", s.conf.InterfaceName)
}
log.Info("DHCP: listening on 0.0.0.0:67")
s.conn = c
s.cond = sync.NewCond(&s.mutex)
log.Debug("DHCPv4: starting...")
s.conf.dnsIPAddrs = getIfaceIPv4(*iface)
if len(s.conf.dnsIPAddrs) == 0 {
return fmt.Errorf("DHCPv4: no IPv4 address for interface %s", iface.Name)
}
laddr := &net.UDPAddr{
IP: net.ParseIP("0.0.0.0"),
Port: dhcpv4.ServerPort,
}
server, err := server4.NewServer(iface.Name, laddr, s.packetHandler, server4.WithDebugLogger())
if err != nil {
return err
}
log.Info("DHCPv4: listening")
s.running = true
go func() {
// operate on c instead of c.conn because c.conn can change over time
err := dhcp4.Serve(c, s)
if err != nil && !s.stopping {
log.Printf("dhcp4.Serve() returned with error: %s", err)
}
_ = c.Close() // in case Serve() exits for other reason than listening socket closure
s.running = false
s.cond.Signal()
err = server.Serve()
log.Error("DHCPv4: %s", err)
}()
return nil
}
@ -74,6 +359,15 @@ func (s *V4Server) Reset() {
// Stop - stop server
func (s *V4Server) Stop() {
if s.srv == nil {
return
}
err := s.srv.Close()
if err != nil {
log.Error("DHCPv4: srv.Close: %s", err)
}
// now server.Serve() will return
}
// Create DHCPv6 server
@ -90,8 +384,10 @@ func v4Create(conf V4ServerConf) (*V4Server, error) {
// return nil, fmt.Errorf("DHCPv6: invalid range-start IP: %s", conf.RangeStart)
// }
// s.conf.ICMPTimeout = 1000
if conf.LeaseDuration == 0 {
s.conf.leaseTime = time.Hour * 2
s.conf.leaseTime = time.Hour * 24
s.conf.LeaseDuration = uint32(s.conf.leaseTime.Seconds())
} else {
s.conf.leaseTime = time.Second * time.Duration(conf.LeaseDuration)

View file

@ -27,6 +27,7 @@ type V6Server struct {
// V6ServerConf - server configuration
type V6ServerConf struct {
Enabled bool `yaml:"enabled"`
InterfaceName string `yaml:"interface_name"`
RangeStart string `yaml:"range_start"`
LeaseDuration uint32 `yaml:"lease_duration"` // in seconds
@ -43,6 +44,15 @@ func (s *V6Server) WriteDiskConfig(c *V6ServerConf) {
*c = s.conf
}
// ResetLeases - reset leases
func (s *V6Server) ResetLeases(ll []*Lease) {
s.leases = nil
for _, l := range ll {
// TODO
s.leases = append(s.leases, l)
}
}
// GetLeases - get current leases
func (s *V6Server) GetLeases(flags int) []Lease {
var result []Lease
@ -64,6 +74,29 @@ func (s *V6Server) GetLeases(flags int) []Lease {
return result
}
// FindMACbyIP6 - find a MAC address by IP address in the currently active DHCP leases
func (s *V6Server) FindMACbyIP6(ip net.IP) net.HardwareAddr {
now := time.Now().Unix()
s.leasesLock.Lock()
defer s.leasesLock.Unlock()
ip4 := ip.To4()
if ip4 == nil {
return nil
}
for _, l := range s.leases {
if l.IP.Equal(ip4) {
unix := l.Expiry.Unix()
if unix > now || unix == leaseExpireStatic {
return l.HWAddr
}
}
}
return nil
}
// AddStaticLease - add a static lease
func (s *V6Server) AddStaticLease(l Lease) error {
if len(l.IP) != 16 {
@ -81,9 +114,9 @@ func (s *V6Server) AddStaticLease(l Lease) error {
s.leasesLock.Unlock()
return err
}
s.conf.notify(LeaseChangedAddedStatic)
s.conf.notify(LeaseChangedDBStore)
s.leasesLock.Unlock()
// s.notify(LeaseChangedAddedStatic)
s.conf.notify(LeaseChangedAddedStatic)
return nil
}
@ -102,9 +135,9 @@ func (s *V6Server) RemoveStaticLease(l Lease) error {
s.leasesLock.Unlock()
return err
}
s.conf.notify(LeaseChangedRemovedStatic)
s.conf.notify(LeaseChangedDBStore)
s.leasesLock.Unlock()
// s.notify(LeaseChangedRemovedStatic)
s.conf.notify(LeaseChangedRemovedStatic)
return nil
}
@ -265,8 +298,9 @@ func (s *V6Server) commitLease(msg *dhcpv6.Message, lease *Lease) time.Duration
lease.Expiry = time.Now().Add(s.conf.leaseTime)
s.leasesLock.Lock()
s.conf.notify(LeaseChangedAdded)
s.conf.notify(LeaseChangedDBStore)
s.leasesLock.Unlock()
s.conf.notify(LeaseChangedAdded)
}
}
return lifetime
@ -430,13 +464,18 @@ func getIfaceIPv6(iface net.Interface) []net.IP {
}
// Start - start server
func (s *V6Server) Start(iface net.Interface) error {
func (s *V6Server) Start() error {
iface, err := net.InterfaceByName(s.conf.InterfaceName)
if err != nil {
return wrapErrPrint(err, "Couldn't find interface by name %s", s.conf.InterfaceName)
}
if !s.conf.Enabled {
return nil
}
log.Debug("DHCPv6: starting...")
s.conf.dnsIPAddrs = getIfaceIPv6(iface)
s.conf.dnsIPAddrs = getIfaceIPv6(*iface)
if len(s.conf.dnsIPAddrs) == 0 {
return fmt.Errorf("DHCPv6: no IPv6 address for interface %s", iface.Name)
}
@ -501,7 +540,7 @@ func v6Create(conf V6ServerConf) (*V6Server, error) {
}
if conf.LeaseDuration == 0 {
s.conf.leaseTime = time.Hour * 2
s.conf.leaseTime = time.Hour * 24
s.conf.LeaseDuration = uint32(s.conf.leaseTime.Seconds())
} else {
s.conf.leaseTime = time.Second * time.Duration(conf.LeaseDuration)