+ dhcpv6 server; rewrite dhcpv4 server; changed API

This commit is contained in:
Simon Zolin 2020-07-03 18:20:01 +03:00
parent c3123473cf
commit a3317c08c4
27 changed files with 2384 additions and 1280 deletions

View File

@ -12,6 +12,7 @@ Contents:
* Updating * Updating
* Get version command * Get version command
* Update command * Update command
* API: Get global status
* TLS * TLS
* API: Get TLS configuration * API: Get TLS configuration
* API: Set TLS configuration * API: Set TLS configuration
@ -24,6 +25,7 @@ Contents:
* API: Find clients by IP * API: Find clients by IP
* DHCP server * DHCP server
* DHCP server in DNS * DHCP server in DNS
* "Show DHCP interfaces" command
* "Show DHCP status" command * "Show DHCP status" command
* "Check DHCP" command * "Check DHCP" command
* "Enable DHCP" command * "Enable DHCP" command
@ -376,6 +378,28 @@ Error response:
UI shows error message "Auto-update has failed" UI shows error message "Auto-update has failed"
## API: Get global status
Request:
GET /control/status
Response:
200 OK
{
"dns_addresses":["..."],
"dns_port":53,
"http_port":3000,
"language":"en",
"protection_enabled":true,
"running":true,
"dhcp_available":true,
"version":"undefined"
}
## DHCP server ## DHCP server
Enable DHCP server algorithm: Enable DHCP server algorithm:
@ -405,6 +429,28 @@ DHCP leases are used in several ways by DNS module.
> PTR 100.1.168.192.in-addr.arpa. = bills-notebook. > PTR 100.1.168.192.in-addr.arpa. = bills-notebook.
### "Show DHCP interfaces" command
Request:
GET /control/dhcp/interfaces
Response:
200 OK
{
"iface_name":{
"name":"iface_name",
"mtu":1500,
"hardware_address":"...",
"ip_addresses":["ipv4 addr","ipv6 addr", ...],
"flags":"up|broadcast|multicast"
}
...
}
### "Show DHCP status" command ### "Show DHCP status" command
Request: Request:
@ -416,16 +462,19 @@ Response:
200 OK 200 OK
{ {
"config":{ "enabled":false,
"enabled":false, "interface_name":"...",
"interface_name":"...", "v4":{
"gateway_ip":"...", "gateway_ip":"...",
"subnet_mask":"...", "subnet_mask":"...",
"range_start":"...", "range_start":"...", // if empty: DHCPv4 won't be enabled
"range_end":"...", "range_end":"...",
"lease_duration":60, "lease_duration":60,
"icmp_timeout_msec":0
}, },
"v6":{
"range_start":"...", // if empty: DHCPv6 won't be enabled
"lease_duration":60,
}
"leases":[ "leases":[
{"ip":"...","mac":"...","hostname":"...","expires":"..."} {"ip":"...","mac":"...","hostname":"...","expires":"..."}
... ...
@ -484,14 +533,19 @@ Request:
POST /control/dhcp/set_config POST /control/dhcp/set_config
{ {
"enabled":true, "enabled":true,
"interface_name":"vboxnet0", "interface_name":"vboxnet0",
"v4":{
"gateway_ip":"192.169.56.1", "gateway_ip":"192.169.56.1",
"subnet_mask":"255.255.255.0", "subnet_mask":"255.255.255.0",
"range_start":"192.169.56.3", "range_start":"192.169.56.100",
"range_end":"192.169.56.3", "range_end":"192.169.56.200", // Note: first 3 octects must match "range_start"
"lease_duration":60, "lease_duration":60,
"icmp_timeout_msec":0 },
"v6":{
"range_start":"...",
"lease_duration":60,
}
} }
Response: Response:
@ -500,6 +554,10 @@ Response:
OK OK
For v4, if range_start = "1.2.3.4", the range_end must be "1.2.3.X" where X > 4.
For v6, if range_start = "2001::1", the last IP is "2001:ff".
### Static IP check/set ### Static IP check/set

View File

@ -1,23 +1,22 @@
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
package dhcpd package dhcpd
import ( import (
"bytes" "bytes"
"crypto/rand"
"encoding/binary"
"fmt" "fmt"
"math"
"net" "net"
"os" "os"
"time" "time"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/krolaw/dhcp4" "github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/iana"
"golang.org/x/net/ipv4" "golang.org/x/net/ipv4"
) )
// CheckIfOtherDHCPServersPresent sends a DHCP request to the specified network interface, // CheckIfOtherDHCPServersPresent sends a DHCP request to the specified network interface,
// and waits for a response for a period defined by defaultDiscoverTime // and waits for a response for a period defined by defaultDiscoverTime
// nolint
func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) { func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) {
iface, err := net.InterfaceByName(ifaceName) iface, err := net.InterfaceByName(ifaceName)
if err != nil { if err != nil {
@ -25,60 +24,29 @@ func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) {
} }
// get ipv4 address of an interface // get ipv4 address of an interface
ifaceIPNet := getIfaceIPv4(iface) ifaceIPNet := getIfaceIPv4(*iface)
if ifaceIPNet == nil { if len(ifaceIPNet) == 0 {
return false, fmt.Errorf("Couldn't find IPv4 address of interface %s %+v", ifaceName, iface) 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") src := net.JoinHostPort(srcIP.String(), "68")
dst := "255.255.255.255:67" dst := "255.255.255.255:67"
// form a DHCP request packet, try to emulate existing client as much as possible hostname, _ := os.Hostname()
xID := make([]byte, 4)
n, err := rand.Read(xID) req, err := dhcpv4.NewDiscovery(iface.HardwareAddr)
if n != 4 && err == nil {
err = fmt.Errorf("Generated less than 4 bytes")
}
if err != nil { if err != nil {
return false, wrapErrPrint(err, "Couldn't generate random bytes") return false, fmt.Errorf("dhcpv4.NewDiscovery: %s", err)
} }
hostname, err := os.Hostname() req.Options.Update(dhcpv4.OptClientIdentifier(iface.HardwareAddr))
if err != nil { req.Options.Update(dhcpv4.OptHostName(hostname))
return false, wrapErrPrint(err, "Couldn't get hostname")
}
requestList := []byte{
byte(dhcp4.OptionSubnetMask),
byte(dhcp4.OptionClasslessRouteFormat),
byte(dhcp4.OptionRouter),
byte(dhcp4.OptionDomainNameServer),
byte(dhcp4.OptionDomainName),
byte(dhcp4.OptionDomainSearch),
252, // private/proxy autodiscovery
95, // LDAP
byte(dhcp4.OptionNetBIOSOverTCPIPNameServer),
byte(dhcp4.OptionNetBIOSOverTCPIPNodeType),
}
maxUDPsizeRaw := make([]byte, 2)
binary.BigEndian.PutUint16(maxUDPsizeRaw, 1500)
leaseTimeRaw := make([]byte, 4)
leaseTime := uint32(math.RoundToEven((time.Hour * 24 * 90).Seconds()))
binary.BigEndian.PutUint32(leaseTimeRaw, leaseTime)
options := []dhcp4.Option{
{Code: dhcp4.OptionParameterRequestList, Value: requestList},
{Code: dhcp4.OptionMaximumDHCPMessageSize, Value: maxUDPsizeRaw},
{Code: dhcp4.OptionClientIdentifier, Value: append([]byte{0x01}, iface.HardwareAddr...)},
{Code: dhcp4.OptionIPAddressLeaseTime, Value: leaseTimeRaw},
{Code: dhcp4.OptionHostName, Value: []byte(hostname)},
}
packet := dhcp4.RequestPacket(dhcp4.Discover, iface.HardwareAddr, nil, xID, false, options)
// resolve 0.0.0.0:68 // resolve 0.0.0.0:68
udpAddr, err := net.ResolveUDPAddr("udp4", src) udpAddr, err := net.ResolveUDPAddr("udp4", src)
if err != nil { if err != nil {
return false, wrapErrPrint(err, "Couldn't resolve UDP address %s", src) return false, wrapErrPrint(err, "Couldn't resolve UDP address %s", src)
} }
// spew.Dump(udpAddr, err)
if !udpAddr.IP.To4().Equal(srcIP) { if !udpAddr.IP.To4().Equal(srcIP) {
return false, wrapErrPrint(err, "Resolved UDP address is not %s", src) return false, wrapErrPrint(err, "Resolved UDP address is not %s", src)
@ -102,7 +70,7 @@ func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) {
// send to 255.255.255.255:67 // send to 255.255.255.255:67
cm := ipv4.ControlMessage{} cm := ipv4.ControlMessage{}
_, err = c.WriteTo(packet, &cm, dstAddr) _, err = c.WriteTo(req.ToBytes(), &cm, dstAddr)
if err != nil { if err != nil {
return false, wrapErrPrint(err, "Couldn't send a packet to %s", dst) return false, wrapErrPrint(err, "Couldn't send a packet to %s", dst)
} }
@ -113,7 +81,7 @@ func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) {
// TODO: replicate dhclient's behaviour of retrying several times with progressively bigger timeouts // TODO: replicate dhclient's behaviour of retrying several times with progressively bigger timeouts
b := make([]byte, 1500) b := make([]byte, 1500)
_ = c.SetReadDeadline(time.Now().Add(defaultDiscoverTime)) _ = c.SetReadDeadline(time.Now().Add(defaultDiscoverTime))
n, _, _, err = c.ReadFrom(b) n, _, _, err := c.ReadFrom(b)
if isTimeout(err) { if isTimeout(err) {
// timed out -- no DHCP servers // timed out -- no DHCP servers
return false, nil return false, nil
@ -121,27 +89,24 @@ func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) {
if err != nil { if err != nil {
return false, wrapErrPrint(err, "Couldn't receive packet") return false, wrapErrPrint(err, "Couldn't receive packet")
} }
// spew.Dump(n, fromAddr, err, b)
log.Tracef("Received packet (%v bytes)", n) log.Tracef("Received packet (%v bytes)", n)
if n < 240 { response, err := dhcpv4.FromBytes(b[:n])
// packet too small for dhcp if err != nil {
log.Debug("DHCPv4: dhcpv4.FromBytes: %s", err)
continue continue
} }
response := dhcp4.Packet(b[:n]) log.Debug("DHCPv4: received message from server: %s", response.Summary())
if response.OpCode() != dhcp4.BootReply ||
response.HType() != 1 /*Ethernet*/ ||
response.HLen() > 16 ||
!bytes.Equal(response.CHAddr(), iface.HardwareAddr) ||
!bytes.Equal(response.XId(), xID) {
continue
}
parsedOptions := response.ParseOptions() if !(response.OpCode == dhcpv4.OpcodeBootReply &&
if t := parsedOptions[dhcp4.OptionDHCPMessageType]; len(t) != 1 { response.HWType == iana.HWTypeEthernet &&
continue //packet without DHCP message type bytes.Equal(response.ClientHWAddr, iface.HardwareAddr) &&
bytes.Equal(response.TransactionID[:], req.TransactionID[:]) &&
response.Options.Has(dhcpv4.OptionDHCPMessageType)) {
log.Debug("DHCPv4: received message from server doesn't match our request")
continue
} }
log.Tracef("The packet is from an active DHCP server") log.Tracef("The packet is from an active DHCP server")

View File

@ -0,0 +1,7 @@
package dhcpd
import "fmt"
func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) {
return false, fmt.Errorf("not supported")
}

View File

@ -11,7 +11,6 @@ import (
"github.com/AdguardTeam/golibs/file" "github.com/AdguardTeam/golibs/file"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/krolaw/dhcp4"
) )
const dbFilename = "leases.db" const dbFilename = "leases.db"
@ -31,21 +30,12 @@ func normalizeIP(ip net.IP) net.IP {
return 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 // Load lease table from DB
func (s *Server) dbLoad() { func (s *Server) dbLoad() {
s.leases = nil
s.IPpool = make(map[[4]byte]net.HardwareAddr)
dynLeases := []*Lease{} dynLeases := []*Lease{}
staticLeases := []*Lease{} staticLeases := []*Lease{}
v6StaticLeases := []*Lease{}
v6DynLeases := []*Lease{}
data, err := ioutil.ReadFile(s.conf.DBFilePath) data, err := ioutil.ReadFile(s.conf.DBFilePath)
if err != nil { if err != nil {
@ -66,10 +56,8 @@ func (s *Server) dbLoad() {
for i := range obj { for i := range obj {
obj[i].IP = normalizeIP(obj[i].IP) obj[i].IP = normalizeIP(obj[i].IP)
if obj[i].Expiry != leaseExpireStatic && if !(len(obj[i].IP) == 4 || len(obj[i].IP) == 16) {
!ipInRange(s.leaseStart, s.leaseStop, obj[i].IP) { log.Info("DHCP: invalid IP: %s", obj[i].IP)
log.Tracef("Skipping a lease with IP %v: not within current IP range", obj[i].IP)
continue continue
} }
@ -80,20 +68,32 @@ func (s *Server) dbLoad() {
Expiry: time.Unix(obj[i].Expiry, 0), Expiry: time.Unix(obj[i].Expiry, 0),
} }
if obj[i].Expiry == leaseExpireStatic { if len(obj[i].IP) == 16 {
staticLeases = append(staticLeases, &lease) if obj[i].Expiry == leaseExpireStatic {
v6StaticLeases = append(v6StaticLeases, &lease)
} else {
v6DynLeases = append(v6DynLeases, &lease)
}
} else { } 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 { leases6 := normalizeLeases(v6StaticLeases, v6DynLeases)
s.reserveIP(lease.IP, lease.HWAddr) if s.srv6 != nil {
s.srv6.ResetLeases(leases6)
} }
log.Info("DHCP: loaded %d (%d) leases from DB", len(s.leases), numLeases) log.Info("DHCP: loaded leases v4:%d v6:%d total-read:%d from DB",
len(leases4), len(leases6), numLeases)
} }
// Skip duplicate leases // Skip duplicate leases
@ -127,19 +127,36 @@ func normalizeLeases(staticLeases, dynLeases []*Lease) []*Lease {
func (s *Server) dbStore() { func (s *Server) dbStore() {
var leases []leaseJSON var leases []leaseJSON
for i := range s.leases { leases4 := s.srv4.GetLeasesRef()
if s.leases[i].Expiry.Unix() == 0 { for _, l := range leases4 {
if l.Expiry.Unix() == 0 {
continue continue
} }
lease := leaseJSON{ lease := leaseJSON{
HWAddr: s.leases[i].HWAddr, HWAddr: l.HWAddr,
IP: s.leases[i].IP, IP: l.IP,
Hostname: s.leases[i].Hostname, Hostname: l.Hostname,
Expiry: s.leases[i].Expiry.Unix(), Expiry: l.Expiry.Unix(),
} }
leases = append(leases, lease) leases = append(leases, lease)
} }
if s.srv6 != nil {
leases6 := s.srv6.GetLeasesRef()
for _, l := range leases6 {
if l.Expiry.Unix() == 0 {
continue
}
lease := leaseJSON{
HWAddr: l.HWAddr,
IP: l.IP,
Hostname: l.Hostname,
Expiry: l.Expiry.Unix(),
}
leases = append(leases, lease)
}
}
data, err := json.Marshal(leases) data, err := json.Marshal(leases)
if err != nil { if err != nil {
log.Error("json.Marshal: %v", err) log.Error("json.Marshal: %v", err)

View File

@ -40,13 +40,70 @@ func convertLeases(inputLeases []Lease, includeExpires bool) []map[string]string
return leases return leases
} }
type v4ServerConfJSON struct {
GatewayIP string `json:"gateway_ip"`
SubnetMask string `json:"subnet_mask"`
RangeStart string `json:"range_start"`
RangeEnd string `json:"range_end"`
LeaseDuration uint32 `json:"lease_duration"`
}
func v4ServerConfToJSON(c V4ServerConf) v4ServerConfJSON {
return v4ServerConfJSON{
GatewayIP: c.GatewayIP,
SubnetMask: c.SubnetMask,
RangeStart: c.RangeStart,
RangeEnd: c.RangeEnd,
LeaseDuration: c.LeaseDuration,
}
}
func v4JSONToServerConf(j v4ServerConfJSON) V4ServerConf {
return V4ServerConf{
GatewayIP: j.GatewayIP,
SubnetMask: j.SubnetMask,
RangeStart: j.RangeStart,
RangeEnd: j.RangeEnd,
LeaseDuration: j.LeaseDuration,
}
}
type v6ServerConfJSON struct {
RangeStart string `json:"range_start"`
LeaseDuration uint32 `json:"lease_duration"`
}
func v6ServerConfToJSON(c V6ServerConf) v6ServerConfJSON {
return v6ServerConfJSON{
RangeStart: c.RangeStart,
LeaseDuration: c.LeaseDuration,
}
}
func v6JSONToServerConf(j v6ServerConfJSON) V6ServerConf {
return V6ServerConf{
RangeStart: j.RangeStart,
LeaseDuration: j.LeaseDuration,
}
}
func (s *Server) handleDHCPStatus(w http.ResponseWriter, r *http.Request) { func (s *Server) handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
leases := convertLeases(s.Leases(LeasesDynamic), true) leases := convertLeases(s.Leases(LeasesDynamic), true)
staticLeases := convertLeases(s.Leases(LeasesStatic), false) staticLeases := convertLeases(s.Leases(LeasesStatic), false)
v4conf := V4ServerConf{}
s.srv4.WriteDiskConfig4(&v4conf)
v6conf := V6ServerConf{}
s.srv6.WriteDiskConfig6(&v6conf)
status := map[string]interface{}{ status := map[string]interface{}{
"config": s.conf, "enabled": s.conf.Enabled,
"leases": leases, "interface_name": s.conf.InterfaceName,
"static_leases": staticLeases, "v4": v4ServerConfToJSON(v4conf),
"v6": v6ServerConfToJSON(v6conf),
"leases": leases,
"static_leases": staticLeases,
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
@ -64,8 +121,10 @@ type staticLeaseJSON struct {
} }
type dhcpServerConfigJSON struct { type dhcpServerConfigJSON struct {
ServerConfig `json:",inline"` Enabled bool `json:"enabled"`
StaticLeases []staticLeaseJSON `json:"static_leases"` InterfaceName string `json:"interface_name"`
V4 v4ServerConfJSON `json:"v4"`
V6 v6ServerConfJSON `json:"v6"`
} }
func (s *Server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) { func (s *Server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
@ -76,22 +135,49 @@ func (s *Server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
return return
} }
err = s.CheckConfig(newconfig.ServerConfig) v4conf := v4JSONToServerConf(newconfig.V4)
v4conf.Enabled = newconfig.Enabled
if len(v4conf.RangeStart) == 0 {
v4conf.Enabled = false
}
v4conf.InterfaceName = newconfig.InterfaceName
c4 := V4ServerConf{}
s.srv4.WriteDiskConfig4(&c4)
v4conf.notify = c4.notify
v4conf.ICMPTimeout = c4.ICMPTimeout
s4, err := v4Create(v4conf)
if err != nil { if err != nil {
httpError(r, w, http.StatusBadRequest, "Invalid DHCP configuration: %s", err) httpError(r, w, http.StatusBadRequest, "Invalid DHCPv4 configuration: %s", err)
return return
} }
err = s.Stop() v6conf := v6JSONToServerConf(newconfig.V6)
if err != nil { v6conf.Enabled = newconfig.Enabled
log.Error("failed to stop the DHCP server: %s", err) if len(v6conf.RangeStart) == 0 {
v6conf.Enabled = false
} }
v6conf.InterfaceName = newconfig.InterfaceName
err = s.Init(newconfig.ServerConfig) v6conf.notify = s.onNotify
if err != nil { s6, err := v6Create(v6conf)
httpError(r, w, http.StatusBadRequest, "Invalid DHCP configuration: %s", err) if s6 == nil {
httpError(r, w, http.StatusBadRequest, "Invalid DHCPv6 configuration: %s", err)
return return
} }
if newconfig.Enabled && !v4conf.Enabled && !v6conf.Enabled {
httpError(r, w, http.StatusBadRequest, "DHCPv4 or DHCPv6 configuration must be complete")
return
}
s.Stop()
s.conf.Enabled = newconfig.Enabled
s.conf.InterfaceName = newconfig.InterfaceName
s.srv4 = s4
s.srv6 = s6
s.conf.ConfigModified() s.conf.ConfigModified()
if newconfig.Enabled { if newconfig.Enabled {
@ -246,20 +332,45 @@ func (s *Server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request
return return
} }
ip, _ := parseIPv4(lj.IP) ip := net.ParseIP(lj.IP)
if ip != nil && ip.To4() == nil {
mac, err := net.ParseMAC(lj.HWAddr)
if err != nil {
httpError(r, w, http.StatusBadRequest, "invalid MAC")
return
}
lease := Lease{
IP: ip,
HWAddr: mac,
}
err = s.srv6.AddStaticLease(lease)
if err != nil {
httpError(r, w, http.StatusBadRequest, "%s", err)
return
}
return
}
ip, _ = parseIPv4(lj.IP)
if ip == nil { if ip == nil {
httpError(r, w, http.StatusBadRequest, "invalid IP") httpError(r, w, http.StatusBadRequest, "invalid IP")
return return
} }
mac, _ := net.ParseMAC(lj.HWAddr) mac, err := net.ParseMAC(lj.HWAddr)
if err != nil {
httpError(r, w, http.StatusBadRequest, "invalid MAC")
return
}
lease := Lease{ lease := Lease{
IP: ip, IP: ip,
HWAddr: mac, HWAddr: mac,
Hostname: lj.Hostname, Hostname: lj.Hostname,
} }
err = s.AddStaticLease(lease) err = s.srv4.AddStaticLease(lease)
if err != nil { if err != nil {
httpError(r, w, http.StatusBadRequest, "%s", err) httpError(r, w, http.StatusBadRequest, "%s", err)
return return
@ -275,7 +386,28 @@ func (s *Server) handleDHCPRemoveStaticLease(w http.ResponseWriter, r *http.Requ
return return
} }
ip, _ := parseIPv4(lj.IP) ip := net.ParseIP(lj.IP)
if ip != nil && ip.To4() == nil {
mac, err := net.ParseMAC(lj.HWAddr)
if err != nil {
httpError(r, w, http.StatusBadRequest, "invalid MAC")
return
}
lease := Lease{
IP: ip,
HWAddr: mac,
}
err = s.srv6.RemoveStaticLease(lease)
if err != nil {
httpError(r, w, http.StatusBadRequest, "%s", err)
return
}
return
}
ip, _ = parseIPv4(lj.IP)
if ip == nil { if ip == nil {
httpError(r, w, http.StatusBadRequest, "invalid IP") httpError(r, w, http.StatusBadRequest, "invalid IP")
return return
@ -288,7 +420,7 @@ func (s *Server) handleDHCPRemoveStaticLease(w http.ResponseWriter, r *http.Requ
HWAddr: mac, HWAddr: mac,
Hostname: lj.Hostname, Hostname: lj.Hostname,
} }
err = s.RemoveStaticLease(lease) err = s.srv4.RemoveStaticLease(lease)
if err != nil { if err != nil {
httpError(r, w, http.StatusBadRequest, "%s", err) httpError(r, w, http.StatusBadRequest, "%s", err)
return return
@ -296,24 +428,29 @@ func (s *Server) handleDHCPRemoveStaticLease(w http.ResponseWriter, r *http.Requ
} }
func (s *Server) handleReset(w http.ResponseWriter, r *http.Request) { func (s *Server) handleReset(w http.ResponseWriter, r *http.Request) {
err := s.Stop() s.Stop()
if err != nil {
log.Error("DHCP: Stop: %s", err)
}
err = os.Remove(s.conf.DBFilePath) err := os.Remove(s.conf.DBFilePath)
if err != nil && !os.IsNotExist(err) { if err != nil && !os.IsNotExist(err) {
log.Error("DHCP: os.Remove: %s: %s", s.conf.DBFilePath, err) log.Error("DHCP: os.Remove: %s: %s", s.conf.DBFilePath, err)
} }
oldconf := s.conf oldconf := s.conf
s.conf = ServerConfig{} s.conf = ServerConfig{}
s.conf.LeaseDuration = 86400
s.conf.ICMPTimeout = 1000
s.conf.WorkDir = oldconf.WorkDir s.conf.WorkDir = oldconf.WorkDir
s.conf.HTTPRegister = oldconf.HTTPRegister s.conf.HTTPRegister = oldconf.HTTPRegister
s.conf.ConfigModified = oldconf.ConfigModified s.conf.ConfigModified = oldconf.ConfigModified
s.conf.DBFilePath = oldconf.DBFilePath s.conf.DBFilePath = oldconf.DBFilePath
v4conf := V4ServerConf{}
v4conf.ICMPTimeout = 1000
v4conf.notify = s.onNotify
s.srv4, _ = v4Create(v4conf)
v6conf := V6ServerConf{}
v6conf.notify = s.onNotify
s.srv6, _ = v6Create(v6conf)
s.conf.ConfigModified() s.conf.ConfigModified()
} }

View File

@ -1,18 +1,12 @@
package dhcpd package dhcpd
import ( import (
"bytes"
"fmt"
"net" "net"
"net/http" "net/http"
"path/filepath" "path/filepath"
"strings"
"sync"
"time" "time"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/krolaw/dhcp4"
ping "github.com/sparrc/go-ping"
) )
const defaultDiscoverTime = time.Second * 3 const defaultDiscoverTime = time.Second * 3
@ -21,9 +15,8 @@ const leaseExpireStatic = 1
var webHandlersRegistered = false var webHandlersRegistered = false
// Lease contains the necessary information about a DHCP lease // Lease contains the necessary information about a DHCP lease
// field ordering is important -- yaml fields will mirror ordering from here
type Lease struct { type Lease struct {
HWAddr net.HardwareAddr `json:"mac" yaml:"hwaddr"` HWAddr net.HardwareAddr `json:"mac"`
IP net.IP `json:"ip"` IP net.IP `json:"ip"`
Hostname string `json:"hostname"` Hostname string `json:"hostname"`
@ -35,29 +28,23 @@ type Lease struct {
// ServerConfig - DHCP server configuration // ServerConfig - DHCP server configuration
// field ordering is important -- yaml fields will mirror ordering from here // field ordering is important -- yaml fields will mirror ordering from here
type ServerConfig struct { type ServerConfig struct {
Enabled bool `json:"enabled" yaml:"enabled"` Enabled bool `yaml:"enabled"`
InterfaceName string `json:"interface_name" yaml:"interface_name"` // eth0, en0 and so on InterfaceName string `yaml:"interface_name"`
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. Conf4 V4ServerConf `yaml:"dhcpv4"`
// 0: disable Conf6 V6ServerConf `yaml:"dhcpv6"`
ICMPTimeout uint32 `json:"icmp_timeout_msec" yaml:"icmp_timeout_msec"`
WorkDir string `json:"-" yaml:"-"` WorkDir string `yaml:"-"`
DBFilePath string `json:"-" yaml:"-"` // path to DB file DBFilePath string `yaml:"-"` // path to DB file
// Called when the configuration is changed by HTTP request // Called when the configuration is changed by HTTP request
ConfigModified func() `json:"-" yaml:"-"` ConfigModified func() `yaml:"-"`
// Register an HTTP handler // Register an HTTP handler
HTTPRegister func(string, string, func(http.ResponseWriter, *http.Request)) `json:"-" yaml:"-"` HTTPRegister func(string, string, func(http.ResponseWriter, *http.Request)) `yaml:"-"`
} }
type onLeaseChangedT func(flags int) type OnLeaseChangedT func(flags int)
// flags for onLeaseChanged() // flags for onLeaseChanged()
const ( const (
@ -65,87 +52,91 @@ const (
LeaseChangedAddedStatic LeaseChangedAddedStatic
LeaseChangedRemovedStatic LeaseChangedRemovedStatic
LeaseChangedBlacklisted LeaseChangedBlacklisted
LeaseChangedDBStore
) )
// Server - the current state of the DHCP server // Server - the current state of the DHCP server
type Server struct { type Server struct {
conn *filterConn // listening UDP socket srv4 DHCPServer
srv6 DHCPServer
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
// 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
// IP address pool -- if entry is in the pool, then it's attached to a lease
IPpool map[[4]byte]net.HardwareAddr
conf ServerConfig conf ServerConfig
// Called when the leases DB is modified // Called when the leases DB is modified
onLeaseChanged []onLeaseChangedT onLeaseChanged []OnLeaseChangedT
} }
// Print information about the available network interfaces type ServerInterface interface {
func printInterfaces() { Leases(flags int) []Lease
ifaces, _ := net.Interfaces() SetOnLeaseChanged(onLeaseChanged OnLeaseChangedT)
var buf strings.Builder
for i := range ifaces {
buf.WriteString(fmt.Sprintf("\"%s\", ", ifaces[i].Name))
}
log.Info("Available network interfaces: %s", buf.String())
} }
// CheckConfig checks the configuration // CheckConfig checks the configuration
func (s *Server) CheckConfig(config ServerConfig) error { func (s *Server) CheckConfig(config ServerConfig) error {
tmpServer := Server{} return nil
return tmpServer.setConfig(config)
} }
// Create - create object // Create - create object
func Create(config ServerConfig) *Server { func Create(config ServerConfig) *Server {
s := Server{} s := Server{}
s.conf = config s.conf.Enabled = config.Enabled
s.conf.InterfaceName = config.InterfaceName
s.conf.HTTPRegister = config.HTTPRegister
s.conf.ConfigModified = config.ConfigModified
s.conf.DBFilePath = filepath.Join(config.WorkDir, dbFilename) 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 { if !webHandlersRegistered && s.conf.HTTPRegister != nil {
webHandlersRegistered = true webHandlersRegistered = true
s.registerHandlers() s.registerHandlers()
} }
var err4, err6 error
v4conf := config.Conf4
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)
v6conf := config.Conf6
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
}
// we can't delay database loading until DHCP server is started, // we can't delay database loading until DHCP server is started,
// because we need static leases functionality available beforehand // because we need static leases functionality available beforehand
s.dbLoad() s.dbLoad()
return &s return &s
} }
// Init checks the configuration and initializes the server // server calls this function after DB is updated
func (s *Server) Init(config ServerConfig) error { func (s *Server) onNotify(flags uint32) {
err := s.setConfig(config) if flags == LeaseChangedDBStore {
if err != nil { s.dbStore()
return err return
} }
return nil
s.notify(int(flags))
} }
// SetOnLeaseChanged - set callback // SetOnLeaseChanged - set callback
func (s *Server) SetOnLeaseChanged(onLeaseChanged onLeaseChangedT) { func (s *Server) SetOnLeaseChanged(onLeaseChanged OnLeaseChangedT) {
s.onLeaseChanged = append(s.onLeaseChanged, onLeaseChanged) s.onLeaseChanged = append(s.onLeaseChanged, onLeaseChanged)
} }
@ -160,563 +151,33 @@ func (s *Server) notify(flags int) {
// WriteDiskConfig - write configuration // WriteDiskConfig - write configuration
func (s *Server) WriteDiskConfig(c *ServerConfig) { func (s *Server) WriteDiskConfig(c *ServerConfig) {
*c = s.conf c.Enabled = s.conf.Enabled
} c.InterfaceName = s.conf.InterfaceName
s.srv4.WriteDiskConfig4(&c.Conf4)
func (s *Server) setConfig(config ServerConfig) error { s.srv6.WriteDiskConfig6(&c.Conf6)
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. // Start will listen on port 67 and serve DHCP requests.
func (s *Server) Start() error { func (s *Server) Start() error {
// TODO: don't close if interface and addresses are the same err := s.srv4.Start()
if s.conn != nil {
_ = s.closeConn()
}
iface, err := net.InterfaceByName(s.conf.InterfaceName)
if err != nil { if err != nil {
return wrapErrPrint(err, "Couldn't find interface by name %s", s.conf.InterfaceName) log.Error("DHCPv4: start: %s", err)
return err
} }
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 err = s.srv6.Start()
if err != nil { if err != nil {
return wrapErrPrint(err, "Couldn't start listening socket on 0.0.0.0:67") log.Error("DHCPv6: start: %s", err)
return err
} }
log.Info("DHCP: listening on 0.0.0.0:67")
s.conn = c
s.cond = sync.NewCond(&s.mutex)
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()
}()
return nil return nil
} }
// Stop closes the listening UDP socket // Stop closes the listening UDP socket
func (s *Server) Stop() error { func (s *Server) Stop() {
if s.conn == nil { s.srv4.Stop()
// nothing to do, return silently s.srv6.Stop()
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
} }
// flags for Leases() function // flags for Leases() function
@ -728,69 +189,23 @@ const (
// Leases returns the list of current DHCP leases (thread-safe) // Leases returns the list of current DHCP leases (thread-safe)
func (s *Server) Leases(flags int) []Lease { func (s *Server) Leases(flags int) []Lease {
var result []Lease result := s.srv4.GetLeases(flags)
now := time.Now().Unix()
s.leasesLock.RLock() v6leases := s.srv6.GetLeases(flags)
for _, lease := range s.leases { result = append(result, v6leases...)
if ((flags&LeasesDynamic) != 0 && lease.Expiry.Unix() > now) ||
((flags&LeasesStatic) != 0 && lease.Expiry.Unix() == leaseExpireStatic) {
result = append(result, *lease)
}
}
s.leasesLock.RUnlock()
return result 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 // FindMACbyIP - find a MAC address by IP address in the currently active DHCP leases
func (s *Server) FindMACbyIP(ip net.IP) net.HardwareAddr { func (s *Server) FindMACbyIP(ip net.IP) net.HardwareAddr {
now := time.Now().Unix() if ip.To4() != nil {
return s.srv4.FindMACbyIP(ip)
s.leasesLock.RLock()
defer s.leasesLock.RUnlock()
ip4 := ip.To4()
if ip4 == nil {
return nil
} }
return s.srv6.FindMACbyIP(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
} }
// Reset internal state // AddStaticLease - add static v4 lease
func (s *Server) reset() { func (s *Server) AddStaticLease(lease Lease) error {
s.leasesLock.Lock() return s.srv4.AddStaticLease(lease)
s.leases = nil
s.IPpool = make(map[[4]byte]net.HardwareAddr)
s.leasesLock.Unlock()
} }

View File

@ -1,3 +1,5 @@
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
package dhcpd package dhcpd
import ( import (
@ -7,7 +9,6 @@ import (
"testing" "testing"
"time" "time"
"github.com/krolaw/dhcp4"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -17,234 +18,66 @@ func check(t *testing.T, result bool, msg string) {
} }
} }
// Tests performed: func testNotify(flags uint32) {
// . Handle Discover message (lease reserve)
// . Handle Request message (lease commit)
// . Static leases
func TestDHCP(t *testing.T) {
var s = Server{}
s.conf.DBFilePath = dbFilename
defer func() { _ = os.Remove(dbFilename) }()
var p, p2 dhcp4.Packet
var hw net.HardwareAddr
var lease *Lease
var opt dhcp4.Options
s.reset()
s.leaseStart = []byte{1, 1, 1, 1}
s.leaseStop = []byte{1, 1, 1, 2}
s.leaseTime = 5 * time.Second
s.leaseOptions = dhcp4.Options{}
s.ipnet = &net.IPNet{
IP: []byte{1, 2, 3, 4},
Mask: []byte{0xff, 0xff, 0xff, 0xff},
}
p = make(dhcp4.Packet, 241)
// Discover and reserve an IP
hw = []byte{3, 2, 3, 4, 5, 6}
p.SetCHAddr(hw)
p.SetCIAddr([]byte{0, 0, 0, 0})
opt = make(dhcp4.Options, 10)
p2 = s.handleDiscover(p, opt)
opt = p2.ParseOptions()
check(t, bytes.Equal(opt[dhcp4.OptionDHCPMessageType], []byte{byte(dhcp4.Offer)}), "dhcp4.Offer")
check(t, bytes.Equal(p2.YIAddr(), []byte{1, 1, 1, 1}), "p2.YIAddr")
check(t, bytes.Equal(p2.CHAddr(), hw), "p2.CHAddr")
check(t, bytes.Equal(opt[dhcp4.OptionIPAddressLeaseTime], dhcp4.OptionsLeaseTime(5*time.Second)), "OptionIPAddressLeaseTime")
check(t, bytes.Equal(opt[dhcp4.OptionServerIdentifier], s.ipnet.IP), "OptionServerIdentifier")
lease = s.findLease(p)
check(t, bytes.Equal(lease.HWAddr, hw), "lease.HWAddr")
check(t, bytes.Equal(lease.IP, []byte{1, 1, 1, 1}), "lease.IP")
// Reserve an IP - the next IP from the range
hw = []byte{2, 2, 3, 4, 5, 6}
p.SetCHAddr(hw)
lease, _ = s.reserveLease(p)
check(t, bytes.Equal(lease.HWAddr, hw), "lease.HWAddr")
check(t, bytes.Equal(lease.IP, []byte{1, 1, 1, 2}), "lease.IP")
// Reserve an IP - we have no more available IPs,
// so the first expired (or, in our case, not yet committed) lease is returned
hw = []byte{1, 2, 3, 4, 5, 6}
p.SetCHAddr(hw)
lease, _ = s.reserveLease(p)
check(t, bytes.Equal(lease.HWAddr, hw), "lease.HWAddr")
check(t, bytes.Equal(lease.IP, []byte{1, 1, 1, 1}), "lease.IP")
// Decline request for a lease which doesn't match our internal state
hw = []byte{1, 2, 3, 4, 5, 6}
p.SetCHAddr(hw)
p.SetCIAddr([]byte{0, 0, 0, 0})
opt = make(dhcp4.Options, 10)
// ask a different IP
opt[dhcp4.OptionRequestedIPAddress] = []byte{1, 1, 1, 2}
p2 = s.handleDHCP4Request(p, opt)
opt = p2.ParseOptions()
check(t, bytes.Equal(opt[dhcp4.OptionDHCPMessageType], []byte{byte(dhcp4.NAK)}), "dhcp4.NAK")
// Commit the previously reserved lease
hw = []byte{1, 2, 3, 4, 5, 6}
p.SetCHAddr(hw)
p.SetCIAddr([]byte{0, 0, 0, 0})
opt = make(dhcp4.Options, 10)
opt[dhcp4.OptionRequestedIPAddress] = []byte{1, 1, 1, 1}
p2 = s.handleDHCP4Request(p, opt)
opt = p2.ParseOptions()
check(t, bytes.Equal(opt[dhcp4.OptionDHCPMessageType], []byte{byte(dhcp4.ACK)}), "dhcp4.ACK")
check(t, bytes.Equal(p2.YIAddr(), []byte{1, 1, 1, 1}), "p2.YIAddr")
check(t, bytes.Equal(p2.CHAddr(), hw), "p2.CHAddr")
check(t, bytes.Equal(opt[dhcp4.OptionIPAddressLeaseTime], dhcp4.OptionsLeaseTime(5*time.Second)), "OptionIPAddressLeaseTime")
check(t, bytes.Equal(opt[dhcp4.OptionServerIdentifier], s.ipnet.IP), "OptionServerIdentifier")
check(t, bytes.Equal(s.FindIPbyMAC(hw), []byte{1, 1, 1, 1}), "FindIPbyMAC")
// Commit the previously reserved lease #2
hw = []byte{2, 2, 3, 4, 5, 6}
p.SetCHAddr(hw)
p.SetCIAddr([]byte{0, 0, 0, 0})
opt = make(dhcp4.Options, 10)
opt[dhcp4.OptionRequestedIPAddress] = []byte{1, 1, 1, 2}
p2 = s.handleDHCP4Request(p, opt)
check(t, bytes.Equal(p2.YIAddr(), []byte{1, 1, 1, 2}), "p2.YIAddr")
// Reserve an IP - we have no more available IPs
hw = []byte{3, 2, 3, 4, 5, 6}
p.SetCHAddr(hw)
lease, _ = s.reserveLease(p)
check(t, lease == nil, "lease == nil")
s.reset()
testStaticLeases(t, &s)
testStaticLeaseReplaceByMAC(t, &s)
s.reset()
misc(t, &s)
}
func testStaticLeases(t *testing.T, s *Server) {
var err error
var l Lease
l.IP = []byte{1, 1, 1, 1}
l.HWAddr = []byte{1, 2, 3, 4, 5, 6}
s.leases = append(s.leases, &l)
// replace dynamic lease with a static (same IP)
l.HWAddr = []byte{2, 2, 3, 4, 5, 6}
err = s.AddStaticLease(l)
check(t, err == nil, "AddStaticLease")
ll := s.Leases(LeasesAll)
assert.True(t, len(ll) == 1)
assert.True(t, bytes.Equal(ll[0].IP, []byte{1, 1, 1, 1}))
assert.True(t, bytes.Equal(ll[0].HWAddr, []byte{2, 2, 3, 4, 5, 6}))
assert.True(t, ll[0].Expiry.Unix() == leaseExpireStatic)
err = s.RemoveStaticLease(l)
assert.True(t, err == nil)
ll = s.Leases(LeasesAll)
assert.True(t, len(ll) == 0)
}
func testStaticLeaseReplaceByMAC(t *testing.T, s *Server) {
var err error
var l Lease
l.HWAddr = []byte{1, 2, 3, 4, 5, 6}
l.IP = []byte{1, 1, 1, 1}
l.Expiry = time.Now().Add(time.Hour)
s.leases = append(s.leases, &l)
// replace dynamic lease with a static (same MAC)
l.IP = []byte{2, 1, 1, 1}
err = s.AddStaticLease(l)
assert.True(t, err == nil)
ll := s.Leases(LeasesAll)
assert.True(t, len(ll) == 1)
assert.True(t, bytes.Equal(ll[0].IP, []byte{2, 1, 1, 1}))
assert.True(t, bytes.Equal(ll[0].HWAddr, []byte{1, 2, 3, 4, 5, 6}))
}
// Small tests that don't require a static server's state
func misc(t *testing.T, s *Server) {
var p, p2 dhcp4.Packet
var hw net.HardwareAddr
var opt dhcp4.Options
p = make(dhcp4.Packet, 241)
// Try to commit a lease for an IP without prior Discover-Offer packets
hw = []byte{2, 2, 3, 4, 5, 6}
p.SetCHAddr(hw)
p.SetCIAddr([]byte{0, 0, 0, 0})
opt = make(dhcp4.Options, 10)
opt[dhcp4.OptionRequestedIPAddress] = []byte{1, 1, 1, 1}
p2 = s.handleDHCP4Request(p, opt)
opt = p2.ParseOptions()
check(t, bytes.Equal(opt[dhcp4.OptionDHCPMessageType], []byte{byte(dhcp4.NAK)}), "dhcp4.NAK")
} }
// Leases database store/load // Leases database store/load
func TestDB(t *testing.T) { func TestDB(t *testing.T) {
var s = Server{} var err error
s := Server{}
s.conf.DBFilePath = dbFilename s.conf.DBFilePath = dbFilename
var p dhcp4.Packet
var hw1, hw2 net.HardwareAddr
var lease *Lease
s.reset() conf := V4ServerConf{
s.leaseStart = []byte{1, 1, 1, 1} Enabled: true,
s.leaseStop = []byte{1, 1, 1, 2} RangeStart: "192.168.10.100",
s.leaseTime = 5 * time.Second RangeEnd: "192.168.10.200",
s.leaseOptions = dhcp4.Options{} GatewayIP: "192.168.10.1",
s.ipnet = &net.IPNet{ SubnetMask: "255.255.255.0",
IP: []byte{1, 2, 3, 4}, notify: testNotify,
Mask: []byte{0xff, 0xff, 0xff, 0xff},
} }
s.srv4, err = v4Create(conf)
assert.True(t, err == nil)
p = make(dhcp4.Packet, 241) s.srv6, err = v6Create(V6ServerConf{})
assert.True(t, err == nil)
hw1 = []byte{1, 2, 3, 4, 5, 6} l := Lease{}
p.SetCHAddr(hw1) l.IP = net.ParseIP("192.168.10.100").To4()
lease, _ = s.reserveLease(p) l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa")
lease.Expiry = time.Unix(4000000001, 0) exp1 := time.Now().Add(time.Hour)
l.Expiry = exp1
s.srv4.(*v4Server).addLease(&l)
hw2 = []byte{2, 2, 3, 4, 5, 6} l2 := Lease{}
p.SetCHAddr(hw2) l2.IP = net.ParseIP("192.168.10.101").To4()
lease, _ = s.reserveLease(p) l2.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:bb")
lease.Expiry = time.Unix(4000000002, 0) s.srv4.AddStaticLease(l2)
_ = os.Remove("leases.db") _ = os.Remove("leases.db")
s.dbStore() s.dbStore()
s.reset() s.srv4.ResetLeases(nil)
s.dbLoad() s.dbLoad()
check(t, bytes.Equal(s.leases[0].HWAddr, hw1), "leases[0].HWAddr")
check(t, bytes.Equal(s.leases[0].IP, []byte{1, 1, 1, 1}), "leases[0].IP")
check(t, s.leases[0].Expiry.Unix() == 4000000001, "leases[0].Expiry")
check(t, bytes.Equal(s.leases[1].HWAddr, hw2), "leases[1].HWAddr") ll := s.srv4.GetLeases(LeasesAll)
check(t, bytes.Equal(s.leases[1].IP, []byte{1, 1, 1, 2}), "leases[1].IP")
check(t, s.leases[1].Expiry.Unix() == 4000000002, "leases[1].Expiry") assert.Equal(t, "aa:aa:aa:aa:aa:bb", ll[0].HWAddr.String())
assert.Equal(t, "192.168.10.101", ll[0].IP.String())
assert.Equal(t, int64(leaseExpireStatic), ll[0].Expiry.Unix())
assert.Equal(t, "aa:aa:aa:aa:aa:aa", ll[1].HWAddr.String())
assert.Equal(t, "192.168.10.100", ll[1].IP.String())
assert.Equal(t, exp1.Unix(), ll[1].Expiry.Unix())
_ = os.Remove("leases.db") _ = os.Remove("leases.db")
} }
func TestIsValidSubnetMask(t *testing.T) { func TestIsValidSubnetMask(t *testing.T) {
if !isValidSubnetMask([]byte{255, 255, 255, 0}) { assert.True(t, isValidSubnetMask([]byte{255, 255, 255, 0}))
t.Fatalf("isValidSubnetMask([]byte{255,255,255,0})") assert.True(t, isValidSubnetMask([]byte{255, 255, 254, 0}))
} assert.True(t, isValidSubnetMask([]byte{255, 255, 252, 0}))
if isValidSubnetMask([]byte{255, 255, 253, 0}) { assert.True(t, !isValidSubnetMask([]byte{255, 255, 253, 0}))
t.Fatalf("isValidSubnetMask([]byte{255,255,253,0})") assert.True(t, !isValidSubnetMask([]byte{255, 255, 255, 1}))
}
if isValidSubnetMask([]byte{0, 255, 255, 255}) {
t.Fatalf("isValidSubnetMask([]byte{255,255,253,0})")
}
} }
func TestNormalizeLeases(t *testing.T) { func TestNormalizeLeases(t *testing.T) {

View File

@ -1,63 +0,0 @@
package dhcpd
import (
"net"
"github.com/joomcode/errorx"
"golang.org/x/net/ipv4"
)
// filterConn listens to 0.0.0.0:67, but accepts packets only from specific interface
// This is necessary for DHCP daemon to work, since binding to IP address doesn't
// us access to see Discover/Request packets from clients.
//
// TODO: on windows, controlmessage does not work, try to find out another way
// https://github.com/golang/net/blob/master/ipv4/payload.go#L13
type filterConn struct {
iface net.Interface
conn *ipv4.PacketConn
}
func newFilterConn(iface net.Interface, address string) (*filterConn, error) {
c, err := net.ListenPacket("udp4", address)
if err != nil {
return nil, errorx.Decorate(err, "Couldn't listen to %s on UDP4", address)
}
p := ipv4.NewPacketConn(c)
err = p.SetControlMessage(ipv4.FlagInterface, true)
if err != nil {
c.Close()
return nil, errorx.Decorate(err, "Couldn't set control message FlagInterface on connection")
}
return &filterConn{iface: iface, conn: p}, nil
}
func (f *filterConn) ReadFrom(b []byte) (int, net.Addr, error) {
for { // read until we find a suitable packet
n, cm, addr, err := f.conn.ReadFrom(b)
if err != nil {
return 0, addr, errorx.Decorate(err, "Error when reading from socket")
}
if cm == nil {
// no controlmessage was passed, so pass the packet to the caller
return n, addr, nil
}
if cm.IfIndex == f.iface.Index {
return n, addr, nil
}
// packet doesn't match criteria, drop it
}
}
func (f *filterConn) WriteTo(b []byte, addr net.Addr) (int, error) {
cm := ipv4.ControlMessage{
IfIndex: f.iface.Index,
}
return f.conn.WriteTo(b, &cm, addr)
}
func (f *filterConn) Close() error {
return f.conn.Close()
}

View File

@ -17,32 +17,24 @@ func isTimeout(err error) bool {
return operr.Timeout() return operr.Timeout()
} }
// return first IPv4 address of an interface, if there is any // Get IPv4 address list
func getIfaceIPv4(iface *net.Interface) *net.IPNet { func getIfaceIPv4(iface net.Interface) []net.IP {
ifaceAddrs, err := iface.Addrs() addrs, err := iface.Addrs()
if err != nil { if err != nil {
panic(err) return nil
} }
for _, addr := range ifaceAddrs { var res []net.IP
ipnet, ok := addr.(*net.IPNet) for _, a := range addrs {
ipnet, ok := a.(*net.IPNet)
if !ok { 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 continue
} }
if ipnet.IP.To4() != nil {
log.Tracef("Got IP that is IPv4: %v", ipnet.IP) res = append(res, ipnet.IP.To4())
return &net.IPNet{
IP: ipnet.IP.To4(),
Mask: ipnet.Mask,
} }
} }
return nil return res
} }
func wrapErrPrint(err error, message string, args ...interface{}) error { func wrapErrPrint(err error, message string, args ...interface{}) error {

View File

@ -1,3 +1,5 @@
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
package dhcpd package dhcpd
import ( import (

81
dhcpd/server.go Normal file
View File

@ -0,0 +1,81 @@
package dhcpd
import (
"net"
"time"
)
// DHCPServer - DHCP server interface
type DHCPServer interface {
// ResetLeases - reset leases
ResetLeases(leases []*Lease)
// GetLeases - get leases
GetLeases(flags int) []Lease
// GetLeasesRef - get reference to leases array
GetLeasesRef() []*Lease
// AddStaticLease - add a static lease
AddStaticLease(lease Lease) error
// RemoveStaticLease - remove a static lease
RemoveStaticLease(l Lease) error
// FindMACbyIP - find a MAC address by IP address in the currently active DHCP leases
FindMACbyIP(ip net.IP) net.HardwareAddr
// WriteDiskConfig4 - copy disk configuration
WriteDiskConfig4(c *V4ServerConf)
// WriteDiskConfig6 - copy disk configuration
WriteDiskConfig6(c *V6ServerConf)
// Start - start server
Start() error
// Stop - stop server
Stop()
}
// V4ServerConf - server configuration
type V4ServerConf struct {
Enabled bool `yaml:"-"`
InterfaceName string `yaml:"-"`
GatewayIP string `yaml:"gateway_ip"`
SubnetMask string `yaml:"subnet_mask"`
// The first & the last IP address for dynamic leases
// Bytes [0..2] of the last allowed IP address must match the first IP
RangeStart string `yaml:"range_start"`
RangeEnd string `yaml:"range_end"`
LeaseDuration uint32 `yaml:"lease_duration"` // in seconds
// IP conflict detector: time (ms) to wait for ICMP reply
// 0: disable
ICMPTimeout uint32 `yaml:"icmp_timeout_msec"`
ipStart net.IP // starting IP address for dynamic leases
ipEnd net.IP // ending IP address for dynamic leases
leaseTime time.Duration // the time during which a dynamic lease is considered valid
dnsIPAddrs []net.IP // IPv4 addresses to return to DHCP clients as DNS server addresses
routerIP net.IP // value for Option Router
subnetMask net.IPMask // value for Option SubnetMask
// Server calls this function when leases data changes
notify func(uint32)
}
// V6ServerConf - server configuration
type V6ServerConf struct {
Enabled bool `yaml:"-"`
InterfaceName string `yaml:"-"`
// The first IP address for dynamic leases
// The last allowed IP address ends with 0xff byte
RangeStart string `yaml:"range_start"`
LeaseDuration uint32 `yaml:"lease_duration"` // in seconds
ipStart net.IP // starting IP address for dynamic leases
leaseTime time.Duration // the time during which a dynamic lease is considered valid
dnsIPAddrs []net.IP // IPv6 addresses to return to DHCP clients as DNS server addresses
// Server calls this function when leases data changes
notify func(uint32)
}

View File

@ -1,115 +0,0 @@
package main
import (
"net"
"os"
"os/signal"
"syscall"
"time"
"github.com/AdguardTeam/AdGuardHome/dhcpd"
"github.com/AdguardTeam/golibs/log"
"github.com/krolaw/dhcp4"
)
func main() {
if len(os.Args) < 2 {
log.Printf("Usage: %s <interface name>", os.Args[0])
os.Exit(64)
}
ifaceName := os.Args[1]
present, err := dhcpd.CheckIfOtherDHCPServersPresent(ifaceName)
if err != nil {
panic(err)
}
log.Printf("Found DHCP server? %v", present)
if present {
log.Printf("Will not start DHCP server because there's already running one on the network")
os.Exit(1)
}
iface, err := net.InterfaceByName(ifaceName)
if err != nil {
panic(err)
}
// get ipv4 address of an interface
ifaceIPNet := getIfaceIPv4(iface)
if ifaceIPNet == nil {
panic(err)
}
// append 10 to server's IP address as start
start := dhcp4.IPAdd(ifaceIPNet.IP, 10)
// lease range is 100 IP's, but TODO: don't go beyond end of subnet mask
stop := dhcp4.IPAdd(start, 100)
server := dhcpd.Server{}
config := dhcpd.ServerConfig{
InterfaceName: ifaceName,
RangeStart: start.String(),
RangeEnd: stop.String(),
SubnetMask: "255.255.255.0",
GatewayIP: "192.168.7.1",
}
log.Printf("Starting DHCP server")
err = server.Init(config)
if err != nil {
panic(err)
}
err = server.Start()
if err != nil {
panic(err)
}
time.Sleep(time.Second)
log.Printf("Stopping DHCP server")
err = server.Stop()
if err != nil {
panic(err)
}
log.Printf("Starting DHCP server")
err = server.Start()
if err != nil {
panic(err)
}
log.Printf("Starting DHCP server while it's already running")
err = server.Start()
if err != nil {
panic(err)
}
log.Printf("Now serving DHCP")
signalChannel := make(chan os.Signal, 1)
signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM)
<-signalChannel
}
// 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.Printf("Got IP that is not IPv4: %v", ipnet.IP)
continue
}
log.Printf("Got IP that is IPv4: %v", ipnet.IP)
return &net.IPNet{
IP: ipnet.IP.To4(),
Mask: ipnet.Mask,
}
}
return nil
}

623
dhcpd/v4.go Normal file
View File

@ -0,0 +1,623 @@
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
package dhcpd
import (
"bytes"
"encoding/binary"
"fmt"
"net"
"sync"
"time"
"github.com/AdguardTeam/golibs/log"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv4/server4"
"github.com/sparrc/go-ping"
)
// v4Server - DHCPv4 server
type v4Server struct {
srv *server4.Server
leasesLock sync.Mutex
leases []*Lease
ipAddrs [256]byte
conf V4ServerConf
}
// WriteDiskConfig4 - write configuration
func (s *v4Server) WriteDiskConfig4(c *V4ServerConf) {
*c = s.conf
}
// WriteDiskConfig6 - write configuration
func (s *v4Server) WriteDiskConfig6(c *V6ServerConf) {
}
// Return TRUE if IP address is within range [start..stop]
func ip4InRange(start net.IP, stop net.IP, ip net.IP) bool {
if len(start) != 4 || len(stop) != 4 {
return false
}
from := binary.BigEndian.Uint32(start)
to := binary.BigEndian.Uint32(stop)
check := binary.BigEndian.Uint32(ip)
return from <= check && check <= to
}
// ResetLeases - reset leases
func (s *v4Server) ResetLeases(leases []*Lease) {
s.leases = nil
for _, l := range leases {
if l.Expiry.Unix() != leaseExpireStatic &&
!ip4InRange(s.conf.ipStart, s.conf.ipEnd, l.IP) {
log.Debug("DHCPv4: skipping a lease with IP %v: not within current IP range", l.IP)
continue
}
s.addLease(l)
}
}
// GetLeasesRef - get leases
func (s *v4Server) GetLeasesRef() []*Lease {
return s.leases
}
// Return TRUE if this lease holds a blacklisted IP
func (s *v4Server) blacklisted(l *Lease) bool {
return l.HWAddr.String() == "00:00:00:00:00:00"
}
// 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 && !s.blacklisted(lease)) ||
((flags&LeasesStatic) != 0 && lease.Expiry.Unix() == leaseExpireStatic) {
result = append(result, *lease)
}
}
s.leasesLock.Unlock()
return result
}
// FindMACbyIP - find a MAC address by IP address in the currently active DHCP leases
func (s *v4Server) FindMACbyIP(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
}
// Add the specified IP to the black list for a time period
func (s *v4Server) blacklistLease(lease *Lease) {
hw := make(net.HardwareAddr, 6)
lease.HWAddr = hw
lease.Hostname = ""
lease.Expiry = time.Now().Add(s.conf.leaseTime)
}
// Remove (swap) lease by index
func (s *v4Server) leaseRemoveSwapByIndex(i int) {
s.ipAddrs[s.leases[i].IP[3]] = 0
log.Debug("DHCPv4: removed lease %s", s.leases[i].HWAddr)
n := len(s.leases)
if i != n-1 {
s.leases[i] = s.leases[n-1] // swap with the last element
}
s.leases = s.leases[:n-1]
}
// Remove a dynamic lease with the same properties
// Return error if a static lease is found
func (s *v4Server) rmDynamicLease(lease Lease) error {
for i := 0; i < len(s.leases); i++ {
l := s.leases[i]
if bytes.Equal(l.HWAddr, lease.HWAddr) {
if l.Expiry.Unix() == leaseExpireStatic {
return fmt.Errorf("static lease already exists")
}
s.leaseRemoveSwapByIndex(i)
l = s.leases[i]
}
if net.IP.Equal(l.IP, lease.IP) {
if l.Expiry.Unix() == leaseExpireStatic {
return fmt.Errorf("static lease already exists")
}
s.leaseRemoveSwapByIndex(i)
}
}
return nil
}
// Add a lease
func (s *v4Server) addLease(l *Lease) {
s.leases = append(s.leases, l)
s.ipAddrs[l.IP[3]] = 1
log.Debug("DHCPv4: added lease %s <-> %s", l.IP, l.HWAddr)
}
// Remove a lease with the same properties
func (s *v4Server) rmLease(lease Lease) error {
for i, l := range s.leases {
if net.IP.Equal(l.IP, lease.IP) {
if !bytes.Equal(l.HWAddr, lease.HWAddr) ||
l.Hostname != lease.Hostname {
return fmt.Errorf("Lease not found")
}
s.leaseRemoveSwapByIndex(i)
return nil
}
}
return fmt.Errorf("lease not found")
}
// AddStaticLease adds a static lease (thread-safe)
func (s *v4Server) AddStaticLease(lease Lease) error {
if len(lease.IP) != 4 {
return fmt.Errorf("invalid IP")
}
if len(lease.HWAddr) != 6 {
return fmt.Errorf("invalid MAC")
}
lease.Expiry = time.Unix(leaseExpireStatic, 0)
s.leasesLock.Lock()
err := s.rmDynamicLease(lease)
if err != nil {
s.leasesLock.Unlock()
return err
}
s.addLease(&lease)
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()
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) {
reply = true
}
log.Debug("DHCPv4: Sending ICMP Echo to %v", target)
pinger.Run()
if reply {
log.Info("DHCPv4: IP conflict: %v is already used by another device", target)
return false
}
log.Debug("DHCPv4: ICMP procedure is complete: %v", target)
return true
}
// Find lease by MAC
func (s *v4Server) findLease(mac net.HardwareAddr) *Lease {
for i := range s.leases {
if bytes.Equal(mac, s.leases[i].HWAddr) {
return s.leases[i]
}
}
return nil
}
// Get next free IP
func (s *v4Server) findFreeIP() net.IP {
for i := s.conf.ipStart[3]; ; i++ {
if s.ipAddrs[i] == 0 {
ip := make([]byte, 4)
copy(ip, s.conf.ipStart)
ip[3] = i
return ip
}
if i == s.conf.ipEnd[3] {
break
}
}
return nil
}
// Find an expired lease and return its index or -1
func (s *v4Server) findExpiredLease() int {
now := time.Now().Unix()
for i, lease := range s.leases {
if lease.Expiry.Unix() != leaseExpireStatic &&
lease.Expiry.Unix() <= now {
return i
}
}
return -1
}
// Reserve lease for MAC
func (s *v4Server) reserveLease(mac net.HardwareAddr) *Lease {
l := Lease{}
l.HWAddr = make([]byte, 6)
copy(l.HWAddr, mac)
l.IP = s.findFreeIP()
if l.IP == nil {
i := s.findExpiredLease()
if i < 0 {
return nil
}
copy(s.leases[i].HWAddr, mac)
return s.leases[i]
}
s.addLease(&l)
return &l
}
func (s *v4Server) commitLease(l *Lease) {
l.Expiry = time.Now().Add(s.conf.leaseTime)
s.leasesLock.Lock()
s.conf.notify(LeaseChangedDBStore)
s.leasesLock.Unlock()
s.conf.notify(LeaseChangedAdded)
}
// Process Discover request and return lease
func (s *v4Server) processDiscover(req *dhcpv4.DHCPv4, resp *dhcpv4.DHCPv4) *Lease {
mac := req.ClientHWAddr
s.leasesLock.Lock()
defer s.leasesLock.Unlock()
lease := s.findLease(mac)
if lease == nil {
toStore := false
for lease == nil {
lease = s.reserveLease(mac)
if lease == nil {
log.Debug("DHCPv4: No more IP addresses")
if toStore {
s.conf.notify(LeaseChangedDBStore)
}
return nil
}
toStore = true
if !s.addrAvailable(lease.IP) {
s.blacklistLease(lease)
lease = nil
continue
}
break
}
s.conf.notify(LeaseChangedDBStore)
// s.conf.notify(LeaseChangedBlacklisted)
} else {
reqIP := req.Options.Get(dhcpv4.OptionRequestedIPAddress)
if len(reqIP) != 0 &&
!bytes.Equal(reqIP, lease.IP) {
log.Debug("DHCPv4: different RequestedIP: %v != %v", reqIP, lease.IP)
}
}
hostname := req.Options.Get(dhcpv4.OptionHostName)
lease.Hostname = string(hostname)
resp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer))
return lease
}
// Process Request request and return lease
// Return false if we don't need to reply
func (s *v4Server) processRequest(req *dhcpv4.DHCPv4, resp *dhcpv4.DHCPv4) (*Lease, bool) {
var lease *Lease
mac := req.ClientHWAddr
hostname := req.Options.Get(dhcpv4.OptionHostName)
reqIP := req.Options.Get(dhcpv4.OptionRequestedIPAddress)
if reqIP == nil {
reqIP = req.ClientIPAddr
}
sid := req.Options.Get(dhcpv4.OptionServerIdentifier)
if len(sid) != 0 &&
!bytes.Equal(sid, s.conf.dnsIPAddrs[0]) {
log.Debug("DHCPv4: Bad OptionServerIdentifier in Request message for %s", mac)
return nil, false
}
if len(reqIP) != 4 {
log.Debug("DHCPv4: Bad OptionRequestedIPAddress in Request message for %s", mac)
return nil, false
}
s.leasesLock.Lock()
for _, l := range s.leases {
if bytes.Equal(l.HWAddr, mac) {
if !bytes.Equal(l.IP, reqIP) {
s.leasesLock.Unlock()
log.Debug("DHCPv4: Mismatched OptionRequestedIPAddress in Request message for %s", mac)
return nil, true
}
if !bytes.Equal([]byte(l.Hostname), hostname) {
s.leasesLock.Unlock()
log.Debug("DHCPv4: Mismatched OptionHostName in Request message for %s", mac)
return nil, true
}
lease = l
break
}
}
s.leasesLock.Unlock()
if lease == nil {
log.Debug("DHCPv4: No lease for %s", mac)
return nil, true
}
if lease.Expiry.Unix() != leaseExpireStatic {
s.commitLease(lease)
}
resp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck))
return lease, true
}
// Find a lease associated with MAC and prepare response
// Return 1: OK
// Return 0: error; reply with Nak
// Return -1: error; don't reply
func (s *v4Server) process(req *dhcpv4.DHCPv4, resp *dhcpv4.DHCPv4) int {
var lease *Lease
resp.UpdateOption(dhcpv4.OptServerIdentifier(s.conf.dnsIPAddrs[0]))
switch req.MessageType() {
case dhcpv4.MessageTypeDiscover:
lease = s.processDiscover(req, resp)
if lease == nil {
return 0
}
case dhcpv4.MessageTypeRequest:
var toReply bool
lease, toReply = s.processRequest(req, resp)
if lease == nil {
if toReply {
return 0
}
return -1 // drop packet
}
}
resp.YourIPAddr = make([]byte, 4)
copy(resp.YourIPAddr, lease.IP)
resp.UpdateOption(dhcpv4.OptIPAddressLeaseTime(s.conf.leaseTime))
resp.UpdateOption(dhcpv4.OptRouter(s.conf.routerIP))
resp.UpdateOption(dhcpv4.OptSubnetMask(s.conf.subnetMask))
resp.UpdateOption(dhcpv4.OptDNS(s.conf.dnsIPAddrs...))
return 1
}
// client(0.0.0.0:68) -> (Request:ClientMAC,Type=Discover,ClientID,ReqIP,HostName) -> server(255.255.255.255:67)
// client(255.255.255.255:68) <- (Reply:YourIP,ClientMAC,Type=Offer,ServerID,SubnetMask,LeaseTime) <- server(<IP>:67)
// client(0.0.0.0:68) -> (Request:ClientMAC,Type=Request,ClientID,ReqIP||ClientIP,HostName,ServerID,ParamReqList) -> server(255.255.255.255:67)
// client(255.255.255.255:68) <- (Reply:YourIP,ClientMAC,Type=ACK,ServerID,SubnetMask,LeaseTime) <- server(<IP>:67)
func (s *v4Server) packetHandler(conn net.PacketConn, peer net.Addr, req *dhcpv4.DHCPv4) {
log.Debug("DHCPv4: received message: %s", req.Summary())
switch req.MessageType() {
case dhcpv4.MessageTypeDiscover,
dhcpv4.MessageTypeRequest:
//
default:
log.Debug("DHCPv4: unsupported message type %d", req.MessageType())
return
}
resp, err := dhcpv4.NewReplyFromRequest(req)
if err != nil {
log.Debug("DHCPv4: dhcpv4.New: %s", err)
return
}
if len(req.ClientHWAddr) != 6 {
log.Debug("DHCPv4: Invalid ClientHWAddr")
return
}
r := s.process(req, resp)
if r < 0 {
return
} else if r == 0 {
resp.Options.Update(dhcpv4.OptMessageType(dhcpv4.MessageTypeNak))
}
log.Debug("DHCPv4: sending: %s", resp.Summary())
_, err = conn.WriteTo(resp.ToBytes(), peer)
if err != nil {
log.Error("DHCPv4: conn.Write to %s failed: %s", peer, err)
return
}
}
// Start - start server
func (s *v4Server) Start() error {
if !s.conf.Enabled {
return nil
}
iface, err := net.InterfaceByName(s.conf.InterfaceName)
if err != nil {
return fmt.Errorf("DHCPv4: Couldn't find interface by name %s: %s", s.conf.InterfaceName, err)
}
log.Debug("DHCPv4: starting...")
s.conf.dnsIPAddrs = getIfaceIPv4(*iface)
if len(s.conf.dnsIPAddrs) == 0 {
log.Debug("DHCPv4: no IPv6 address for interface %s", iface.Name)
return nil
}
laddr := &net.UDPAddr{
IP: net.ParseIP("0.0.0.0"),
Port: dhcpv4.ServerPort,
}
s.srv, err = server4.NewServer(iface.Name, laddr, s.packetHandler, server4.WithDebugLogger())
if err != nil {
return err
}
log.Info("DHCPv4: listening")
go func() {
err = s.srv.Serve()
log.Debug("DHCPv4: srv.Serve: %s", err)
}()
return nil
}
// Stop - stop server
func (s *v4Server) Stop() {
if s.srv == nil {
return
}
log.Debug("DHCPv4: stopping")
err := s.srv.Close()
if err != nil {
log.Error("DHCPv4: srv.Close: %s", err)
}
// now s.srv.Serve() will return
s.srv = nil
}
// Create DHCPv4 server
func v4Create(conf V4ServerConf) (DHCPServer, error) {
s := &v4Server{}
s.conf = conf
if !conf.Enabled {
return s, nil
}
var err error
s.conf.routerIP, err = parseIPv4(s.conf.GatewayIP)
if err != nil {
return s, fmt.Errorf("DHCPv4: %s", err)
}
subnet, err := parseIPv4(s.conf.SubnetMask)
if err != nil || !isValidSubnetMask(subnet) {
return s, fmt.Errorf("DHCPv4: invalid subnet mask: %s", s.conf.SubnetMask)
}
s.conf.subnetMask = make([]byte, 4)
copy(s.conf.subnetMask, subnet)
s.conf.ipStart, err = parseIPv4(conf.RangeStart)
if s.conf.ipStart == nil {
return s, fmt.Errorf("DHCPv4: %s", err)
}
if s.conf.ipStart[0] == 0 {
return s, fmt.Errorf("DHCPv4: invalid range start IP")
}
s.conf.ipEnd, err = parseIPv4(conf.RangeEnd)
if s.conf.ipEnd == nil {
return s, fmt.Errorf("DHCPv4: %s", err)
}
if !net.IP.Equal(s.conf.ipStart[:3], s.conf.ipEnd[:3]) ||
s.conf.ipStart[3] > s.conf.ipEnd[3] {
return s, fmt.Errorf("DHCPv4: range end IP should match range start IP")
}
if conf.LeaseDuration == 0 {
s.conf.leaseTime = time.Hour * 24
s.conf.LeaseDuration = uint32(s.conf.leaseTime.Seconds())
} else {
s.conf.leaseTime = time.Second * time.Duration(conf.LeaseDuration)
}
return s, nil
}

47
dhcpd/v46_windows.go Normal file
View File

@ -0,0 +1,47 @@
package dhcpd
// 'u-root/u-root' package, a dependency of 'insomniacslk/dhcp' package, doesn't build on Windows
import "net"
type winServer struct {
}
func (s *winServer) ResetLeases(leases []*Lease) {
}
func (s *winServer) GetLeases(flags int) []Lease {
return nil
}
func (s *winServer) GetLeasesRef() []*Lease {
return nil
}
func (s *winServer) AddStaticLease(lease Lease) error {
return nil
}
func (s *winServer) RemoveStaticLease(l Lease) error {
return nil
}
func (s *winServer) FindMACbyIP(ip net.IP) net.HardwareAddr {
return nil
}
func (s *winServer) WriteDiskConfig4(c *V4ServerConf) {
}
func (s *winServer) WriteDiskConfig6(c *V6ServerConf) {
}
func (s *winServer) Start() error {
return nil
}
func (s *winServer) Stop() {
}
func (s *winServer) Reset() {
}
func v4Create(conf V4ServerConf) (DHCPServer, error) {
return &winServer{}, nil
}
func v6Create(conf V6ServerConf) (DHCPServer, error) {
return &winServer{}, nil
}

232
dhcpd/v4_test.go Normal file
View File

@ -0,0 +1,232 @@
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
package dhcpd
import (
"net"
"testing"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/stretchr/testify/assert"
)
func notify4(flags uint32) {
}
func TestV4StaticLeaseAddRemove(t *testing.T) {
conf := V4ServerConf{
Enabled: true,
RangeStart: "192.168.10.100",
RangeEnd: "192.168.10.200",
GatewayIP: "192.168.10.1",
SubnetMask: "255.255.255.0",
notify: notify4,
}
s, err := v4Create(conf)
assert.True(t, err == nil)
ls := s.GetLeases(LeasesStatic)
assert.Equal(t, 0, len(ls))
// add static lease
l := Lease{}
l.IP = net.ParseIP("192.168.10.150").To4()
l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa")
assert.True(t, s.AddStaticLease(l) == nil)
// try to add the same static lease - fail
assert.True(t, s.AddStaticLease(l) != nil)
// check
ls = s.GetLeases(LeasesStatic)
assert.Equal(t, 1, len(ls))
assert.Equal(t, "192.168.10.150", ls[0].IP.String())
assert.Equal(t, "aa:aa:aa:aa:aa:aa", ls[0].HWAddr.String())
assert.True(t, ls[0].Expiry.Unix() == leaseExpireStatic)
// try to remove static lease - fail
l.IP = net.ParseIP("192.168.10.110").To4()
l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa")
assert.True(t, s.RemoveStaticLease(l) != nil)
// remove static lease
l.IP = net.ParseIP("192.168.10.150").To4()
l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa")
assert.True(t, s.RemoveStaticLease(l) == nil)
// check
ls = s.GetLeases(LeasesStatic)
assert.Equal(t, 0, len(ls))
}
func TestV4StaticLeaseAddReplaceDynamic(t *testing.T) {
conf := V4ServerConf{
Enabled: true,
RangeStart: "192.168.10.100",
RangeEnd: "192.168.10.200",
GatewayIP: "192.168.10.1",
SubnetMask: "255.255.255.0",
notify: notify4,
}
sIface, err := v4Create(conf)
s := sIface.(*v4Server)
assert.True(t, err == nil)
// add dynamic lease
ld := Lease{}
ld.IP = net.ParseIP("192.168.10.150").To4()
ld.HWAddr, _ = net.ParseMAC("11:aa:aa:aa:aa:aa")
s.addLease(&ld)
// add dynamic lease
{
ld := Lease{}
ld.IP = net.ParseIP("192.168.10.151").To4()
ld.HWAddr, _ = net.ParseMAC("22:aa:aa:aa:aa:aa")
s.addLease(&ld)
}
// add static lease with the same IP
l := Lease{}
l.IP = net.ParseIP("192.168.10.150").To4()
l.HWAddr, _ = net.ParseMAC("33:aa:aa:aa:aa:aa")
assert.True(t, s.AddStaticLease(l) == nil)
// add static lease with the same MAC
l = Lease{}
l.IP = net.ParseIP("192.168.10.152").To4()
l.HWAddr, _ = net.ParseMAC("22:aa:aa:aa:aa:aa")
assert.True(t, s.AddStaticLease(l) == nil)
// check
ls := s.GetLeases(LeasesStatic)
assert.Equal(t, 2, len(ls))
assert.Equal(t, "192.168.10.150", ls[0].IP.String())
assert.Equal(t, "33:aa:aa:aa:aa:aa", ls[0].HWAddr.String())
assert.True(t, ls[0].Expiry.Unix() == leaseExpireStatic)
assert.Equal(t, "192.168.10.152", ls[1].IP.String())
assert.Equal(t, "22:aa:aa:aa:aa:aa", ls[1].HWAddr.String())
assert.True(t, ls[1].Expiry.Unix() == leaseExpireStatic)
}
func TestV4StaticLeaseGet(t *testing.T) {
conf := V4ServerConf{
Enabled: true,
RangeStart: "192.168.10.100",
RangeEnd: "192.168.10.200",
GatewayIP: "192.168.10.1",
SubnetMask: "255.255.255.0",
notify: notify4,
}
sIface, err := v4Create(conf)
s := sIface.(*v4Server)
assert.True(t, err == nil)
s.conf.dnsIPAddrs = []net.IP{net.ParseIP("192.168.10.1").To4()}
l := Lease{}
l.IP = net.ParseIP("192.168.10.150").To4()
l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa")
assert.True(t, s.AddStaticLease(l) == nil)
// "Discover"
mac, _ := net.ParseMAC("aa:aa:aa:aa:aa:aa")
req, _ := dhcpv4.NewDiscovery(mac)
resp, _ := dhcpv4.NewReplyFromRequest(req)
assert.Equal(t, 1, s.process(req, resp))
// check "Offer"
assert.Equal(t, dhcpv4.MessageTypeOffer, resp.MessageType())
assert.Equal(t, "aa:aa:aa:aa:aa:aa", resp.ClientHWAddr.String())
assert.Equal(t, "192.168.10.150", resp.YourIPAddr.String())
assert.Equal(t, "192.168.10.1", resp.Router()[0].String())
assert.Equal(t, "192.168.10.1", resp.ServerIdentifier().String())
assert.Equal(t, "255.255.255.0", net.IP(resp.SubnetMask()).String())
assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds())
// "Request"
req, _ = dhcpv4.NewRequestFromOffer(resp)
resp, _ = dhcpv4.NewReplyFromRequest(req)
assert.Equal(t, 1, s.process(req, resp))
// check "Ack"
assert.Equal(t, dhcpv4.MessageTypeAck, resp.MessageType())
assert.Equal(t, "aa:aa:aa:aa:aa:aa", resp.ClientHWAddr.String())
assert.Equal(t, "192.168.10.150", resp.YourIPAddr.String())
assert.Equal(t, "192.168.10.1", resp.Router()[0].String())
assert.Equal(t, "192.168.10.1", resp.ServerIdentifier().String())
assert.Equal(t, "255.255.255.0", net.IP(resp.SubnetMask()).String())
assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds())
dnsAddrs := resp.DNS()
assert.Equal(t, 1, len(dnsAddrs))
assert.Equal(t, "192.168.10.1", dnsAddrs[0].String())
// check lease
ls := s.GetLeases(LeasesStatic)
assert.Equal(t, 1, len(ls))
assert.Equal(t, "192.168.10.150", ls[0].IP.String())
assert.Equal(t, "aa:aa:aa:aa:aa:aa", ls[0].HWAddr.String())
}
func TestV4DynamicLeaseGet(t *testing.T) {
conf := V4ServerConf{
Enabled: true,
RangeStart: "192.168.10.100",
RangeEnd: "192.168.10.200",
GatewayIP: "192.168.10.1",
SubnetMask: "255.255.255.0",
notify: notify4,
}
sIface, err := v4Create(conf)
s := sIface.(*v4Server)
assert.True(t, err == nil)
s.conf.dnsIPAddrs = []net.IP{net.ParseIP("192.168.10.1").To4()}
// "Discover"
mac, _ := net.ParseMAC("aa:aa:aa:aa:aa:aa")
req, _ := dhcpv4.NewDiscovery(mac)
resp, _ := dhcpv4.NewReplyFromRequest(req)
assert.Equal(t, 1, s.process(req, resp))
// check "Offer"
assert.Equal(t, dhcpv4.MessageTypeOffer, resp.MessageType())
assert.Equal(t, "aa:aa:aa:aa:aa:aa", resp.ClientHWAddr.String())
assert.Equal(t, "192.168.10.100", resp.YourIPAddr.String())
assert.Equal(t, "192.168.10.1", resp.Router()[0].String())
assert.Equal(t, "192.168.10.1", resp.ServerIdentifier().String())
assert.Equal(t, "255.255.255.0", net.IP(resp.SubnetMask()).String())
assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds())
// "Request"
req, _ = dhcpv4.NewRequestFromOffer(resp)
resp, _ = dhcpv4.NewReplyFromRequest(req)
assert.Equal(t, 1, s.process(req, resp))
// check "Ack"
assert.Equal(t, dhcpv4.MessageTypeAck, resp.MessageType())
assert.Equal(t, "aa:aa:aa:aa:aa:aa", resp.ClientHWAddr.String())
assert.Equal(t, "192.168.10.100", resp.YourIPAddr.String())
assert.Equal(t, "192.168.10.1", resp.Router()[0].String())
assert.Equal(t, "192.168.10.1", resp.ServerIdentifier().String())
assert.Equal(t, "255.255.255.0", net.IP(resp.SubnetMask()).String())
assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds())
dnsAddrs := resp.DNS()
assert.Equal(t, 1, len(dnsAddrs))
assert.Equal(t, "192.168.10.1", dnsAddrs[0].String())
// check lease
ls := s.GetLeases(LeasesDynamic)
assert.Equal(t, 1, len(ls))
assert.Equal(t, "192.168.10.100", ls[0].IP.String())
assert.Equal(t, "aa:aa:aa:aa:aa:aa", ls[0].HWAddr.String())
start := net.ParseIP("192.168.10.100").To4()
stop := net.ParseIP("192.168.10.200").To4()
assert.True(t, !ip4InRange(start, stop, net.ParseIP("192.168.10.99").To4()))
assert.True(t, !ip4InRange(start, stop, net.ParseIP("192.168.11.100").To4()))
assert.True(t, !ip4InRange(start, stop, net.ParseIP("192.168.11.201").To4()))
assert.True(t, ip4InRange(start, stop, net.ParseIP("192.168.10.100").To4()))
}

638
dhcpd/v6.go Normal file
View File

@ -0,0 +1,638 @@
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
package dhcpd
import (
"bytes"
"fmt"
"net"
"sync"
"time"
"github.com/AdguardTeam/golibs/log"
"github.com/insomniacslk/dhcp/dhcpv6"
"github.com/insomniacslk/dhcp/dhcpv6/server6"
"github.com/insomniacslk/dhcp/iana"
)
const valueIAID = "ADGH" // value for IANA.ID
// v6Server - DHCPv6 server
type v6Server struct {
srv *server6.Server
leasesLock sync.Mutex
leases []*Lease
ipAddrs [256]byte
sid dhcpv6.Duid
conf V6ServerConf
}
// WriteDiskConfig4 - write configuration
func (s *v6Server) WriteDiskConfig4(c *V4ServerConf) {
}
// WriteDiskConfig6 - write configuration
func (s *v6Server) WriteDiskConfig6(c *V6ServerConf) {
*c = s.conf
}
// Return TRUE if IP address is within range [start..0xff]
// nolint(staticcheck)
func ip6InRange(start net.IP, ip net.IP) bool {
if len(start) != 16 {
return false
}
if !bytes.Equal(start[:15], ip[:15]) {
return false
}
return start[15] <= ip[15]
}
// ResetLeases - reset leases
func (s *v6Server) ResetLeases(ll []*Lease) {
s.leases = nil
for _, l := range ll {
if l.Expiry.Unix() != leaseExpireStatic &&
!ip6InRange(s.conf.ipStart, l.IP) {
log.Debug("DHCPv6: skipping a lease with IP %v: not within current IP range", l.IP)
continue
}
s.addLease(l)
}
}
// GetLeases - get current leases
func (s *v6Server) GetLeases(flags int) []Lease {
var result []Lease
s.leasesLock.Lock()
for _, lease := range s.leases {
if lease.Expiry.Unix() == leaseExpireStatic {
if (flags & LeasesStatic) != 0 {
result = append(result, *lease)
}
} else {
if (flags & LeasesDynamic) != 0 {
result = append(result, *lease)
}
}
}
s.leasesLock.Unlock()
return result
}
// GetLeasesRef - get leases
func (s *v6Server) GetLeasesRef() []*Lease {
return s.leases
}
// FindMACbyIP - find a MAC address by IP address in the currently active DHCP leases
func (s *v6Server) FindMACbyIP(ip net.IP) net.HardwareAddr {
now := time.Now().Unix()
s.leasesLock.Lock()
defer s.leasesLock.Unlock()
for _, l := range s.leases {
if l.IP.Equal(ip) {
unix := l.Expiry.Unix()
if unix > now || unix == leaseExpireStatic {
return l.HWAddr
}
}
}
return nil
}
// Remove (swap) lease by index
func (s *v6Server) leaseRemoveSwapByIndex(i int) {
s.ipAddrs[s.leases[i].IP[15]] = 0
log.Debug("DHCPv6: removed lease %s", s.leases[i].HWAddr)
n := len(s.leases)
if i != n-1 {
s.leases[i] = s.leases[n-1] // swap with the last element
}
s.leases = s.leases[:n-1]
}
// Remove a dynamic lease with the same properties
// Return error if a static lease is found
func (s *v6Server) rmDynamicLease(lease Lease) error {
for i := 0; i < len(s.leases); i++ {
l := s.leases[i]
if bytes.Equal(l.HWAddr, lease.HWAddr) {
if l.Expiry.Unix() == leaseExpireStatic {
return fmt.Errorf("static lease already exists")
}
s.leaseRemoveSwapByIndex(i)
l = s.leases[i]
}
if net.IP.Equal(l.IP, lease.IP) {
if l.Expiry.Unix() == leaseExpireStatic {
return fmt.Errorf("static lease already exists")
}
s.leaseRemoveSwapByIndex(i)
}
}
return nil
}
// AddStaticLease - add a static lease
func (s *v6Server) AddStaticLease(l Lease) error {
if len(l.IP) != 16 {
return fmt.Errorf("invalid IP")
}
if len(l.HWAddr) != 6 {
return fmt.Errorf("invalid MAC")
}
l.Expiry = time.Unix(leaseExpireStatic, 0)
s.leasesLock.Lock()
err := s.rmDynamicLease(l)
if err != nil {
s.leasesLock.Unlock()
return err
}
s.addLease(&l)
s.conf.notify(LeaseChangedDBStore)
s.leasesLock.Unlock()
s.conf.notify(LeaseChangedAddedStatic)
return nil
}
// RemoveStaticLease - remove a static lease
func (s *v6Server) RemoveStaticLease(l Lease) error {
if len(l.IP) != 16 {
return fmt.Errorf("invalid IP")
}
if len(l.HWAddr) != 6 {
return fmt.Errorf("invalid MAC")
}
s.leasesLock.Lock()
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
}
// Add a lease
func (s *v6Server) addLease(l *Lease) {
s.leases = append(s.leases, l)
s.ipAddrs[l.IP[15]] = 1
log.Debug("DHCPv6: added lease %s <-> %s", l.IP, l.HWAddr)
}
// Remove a lease with the same properties
func (s *v6Server) rmLease(lease Lease) error {
for i, l := range s.leases {
if net.IP.Equal(l.IP, lease.IP) {
if !bytes.Equal(l.HWAddr, lease.HWAddr) ||
l.Hostname != lease.Hostname {
return fmt.Errorf("Lease not found")
}
s.leaseRemoveSwapByIndex(i)
return nil
}
}
return fmt.Errorf("lease not found")
}
// Find lease by MAC
func (s *v6Server) findLease(mac net.HardwareAddr) *Lease {
s.leasesLock.Lock()
defer s.leasesLock.Unlock()
for i := range s.leases {
if bytes.Equal(mac, s.leases[i].HWAddr) {
return s.leases[i]
}
}
return nil
}
// Find an expired lease and return its index or -1
func (s *v6Server) findExpiredLease() int {
now := time.Now().Unix()
for i, lease := range s.leases {
if lease.Expiry.Unix() != leaseExpireStatic &&
lease.Expiry.Unix() <= now {
return i
}
}
return -1
}
// Get next free IP
func (s *v6Server) findFreeIP() net.IP {
for i := s.conf.ipStart[15]; ; i++ {
if s.ipAddrs[i] == 0 {
ip := make([]byte, 16)
copy(ip, s.conf.ipStart)
ip[15] = i
return ip
}
if i == 0xff {
break
}
}
return nil
}
// Reserve lease for MAC
func (s *v6Server) reserveLease(mac net.HardwareAddr) *Lease {
l := Lease{}
l.HWAddr = make([]byte, 6)
copy(l.HWAddr, mac)
s.leasesLock.Lock()
defer s.leasesLock.Unlock()
copy(l.IP, s.conf.ipStart)
l.IP = s.findFreeIP()
if l.IP == nil {
i := s.findExpiredLease()
if i < 0 {
return nil
}
copy(s.leases[i].HWAddr, mac)
return s.leases[i]
}
s.addLease(&l)
return &l
}
func (s *v6Server) commitDynamicLease(l *Lease) {
l.Expiry = time.Now().Add(s.conf.leaseTime)
s.leasesLock.Lock()
s.conf.notify(LeaseChangedDBStore)
s.leasesLock.Unlock()
s.conf.notify(LeaseChangedAdded)
}
// Check Client ID
func (s *v6Server) checkCID(msg *dhcpv6.Message) error {
if msg.Options.ClientID() == nil {
return fmt.Errorf("DHCPv6: no ClientID option in request")
}
return nil
}
// Check ServerID policy
func (s *v6Server) checkSID(msg *dhcpv6.Message) error {
sid := msg.Options.ServerID()
switch msg.Type() {
case dhcpv6.MessageTypeSolicit,
dhcpv6.MessageTypeConfirm,
dhcpv6.MessageTypeRebind:
if sid != nil {
return fmt.Errorf("DHCPv6: drop packet: ServerID option in message %s", msg.Type().String())
}
case dhcpv6.MessageTypeRequest,
dhcpv6.MessageTypeRenew,
dhcpv6.MessageTypeRelease,
dhcpv6.MessageTypeDecline:
if sid == nil {
return fmt.Errorf("DHCPv6: drop packet: no ServerID option in message %s", msg.Type().String())
}
if !sid.Equal(s.sid) {
return fmt.Errorf("DHCPv6: drop packet: mismatched ServerID option in message %s: %s",
msg.Type().String(), sid.String())
}
}
return nil
}
// . IAAddress must be equal to the lease's IP
func (s *v6Server) checkIA(msg *dhcpv6.Message, lease *Lease) error {
switch msg.Type() {
case dhcpv6.MessageTypeRequest,
dhcpv6.MessageTypeConfirm,
dhcpv6.MessageTypeRenew,
dhcpv6.MessageTypeRebind:
oia := msg.Options.OneIANA()
if oia == nil {
return fmt.Errorf("no IANA option in %s", msg.Type().String())
}
oiaAddr := oia.Options.OneAddress()
if oiaAddr == nil {
return fmt.Errorf("no IANA.Addr option in %s", msg.Type().String())
}
if !oiaAddr.IPv6Addr.Equal(lease.IP) {
return fmt.Errorf("invalid IANA.Addr option in %s", msg.Type().String())
}
}
return nil
}
// Store lease in DB (if necessary) and return lease life time
func (s *v6Server) commitLease(msg *dhcpv6.Message, lease *Lease) time.Duration {
lifetime := s.conf.leaseTime
switch msg.Type() {
case dhcpv6.MessageTypeSolicit:
//
case dhcpv6.MessageTypeConfirm:
lifetime = lease.Expiry.Sub(time.Now())
case dhcpv6.MessageTypeRequest,
dhcpv6.MessageTypeRenew,
dhcpv6.MessageTypeRebind:
if lease.Expiry.Unix() != leaseExpireStatic {
s.commitDynamicLease(lease)
}
}
return lifetime
}
// Find a lease associated with MAC and prepare response
func (s *v6Server) process(msg *dhcpv6.Message, req dhcpv6.DHCPv6, resp dhcpv6.DHCPv6) bool {
switch msg.Type() {
case dhcpv6.MessageTypeSolicit,
dhcpv6.MessageTypeRequest,
dhcpv6.MessageTypeConfirm,
dhcpv6.MessageTypeRenew,
dhcpv6.MessageTypeRebind:
// continue
default:
return false
}
mac, err := dhcpv6.ExtractMAC(req)
if err != nil {
log.Debug("DHCPv6: dhcpv6.ExtractMAC: %s", err)
return false
}
lease := s.findLease(mac)
if lease == nil {
log.Debug("DHCPv6: no lease for: %s", mac)
switch msg.Type() {
case dhcpv6.MessageTypeSolicit:
lease = s.reserveLease(mac)
if lease == nil {
return false
}
default:
return false
}
}
err = s.checkIA(msg, lease)
if err != nil {
log.Debug("DHCPv6: %s", err)
return false
}
lifetime := s.commitLease(msg, lease)
oia := &dhcpv6.OptIANA{
T1: lifetime / 2,
T2: time.Duration(float32(lifetime) / 1.5),
}
roia := msg.Options.OneIANA()
if roia != nil {
copy(oia.IaId[:], roia.IaId[:])
} else {
copy(oia.IaId[:], []byte(valueIAID))
}
oiaAddr := &dhcpv6.OptIAAddress{
IPv6Addr: lease.IP,
PreferredLifetime: lifetime,
ValidLifetime: lifetime,
}
oia.Options = dhcpv6.IdentityOptions{
Options: []dhcpv6.Option{oiaAddr},
}
resp.AddOption(oia)
if msg.IsOptionRequested(dhcpv6.OptionDNSRecursiveNameServer) {
resp.UpdateOption(dhcpv6.OptDNS(s.conf.dnsIPAddrs...))
}
fqdn := msg.GetOneOption(dhcpv6.OptionFQDN)
if fqdn != nil {
resp.AddOption(fqdn)
}
resp.AddOption(&dhcpv6.OptStatusCode{
StatusCode: iana.StatusSuccess,
StatusMessage: "success",
})
return true
}
// 1.
// fe80::* (client) --(Solicit + ClientID+IANA())-> ff02::1:2
// server -(Advertise + ClientID+ServerID+IANA(IAAddress)> fe80::*
// fe80::* --(Request + ClientID+ServerID+IANA(IAAddress))-> ff02::1:2
// server -(Reply + ClientID+ServerID+IANA(IAAddress)+DNS)> fe80::*
//
// 2.
// fe80::* --(Confirm|Renew|Rebind + ClientID+IANA(IAAddress))-> ff02::1:2
// server -(Reply + ClientID+ServerID+IANA(IAAddress)+DNS)> fe80::*
//
// 3.
// fe80::* --(Release + ClientID+ServerID+IANA(IAAddress))-> ff02::1:2
func (s *v6Server) packetHandler(conn net.PacketConn, peer net.Addr, req dhcpv6.DHCPv6) {
msg, err := req.GetInnerMessage()
if err != nil {
log.Error("DHCPv6: %s", err)
return
}
log.Debug("DHCPv6: received: %s", req.Summary())
err = s.checkCID(msg)
if err != nil {
log.Debug("%s", err)
return
}
err = s.checkSID(msg)
if err != nil {
log.Debug("%s", err)
return
}
var resp dhcpv6.DHCPv6
switch msg.Type() {
case dhcpv6.MessageTypeSolicit:
if msg.GetOneOption(dhcpv6.OptionRapidCommit) == nil {
resp, err = dhcpv6.NewAdvertiseFromSolicit(msg)
break
}
fallthrough
case dhcpv6.MessageTypeRequest,
dhcpv6.MessageTypeConfirm,
dhcpv6.MessageTypeRenew,
dhcpv6.MessageTypeRebind,
dhcpv6.MessageTypeRelease,
dhcpv6.MessageTypeInformationRequest:
resp, err = dhcpv6.NewReplyFromMessage(msg)
default:
log.Error("DHCPv6: message type %d not supported", msg.Type())
return
}
if err != nil {
log.Error("DHCPv6: %s", err)
return
}
resp.AddOption(dhcpv6.OptServerID(s.sid))
_ = s.process(msg, req, resp)
log.Debug("DHCPv6: sending: %s", resp.Summary())
_, err = conn.WriteTo(resp.ToBytes(), peer)
if err != nil {
log.Error("DHCPv6: conn.Write to %s failed: %s", peer, err)
return
}
}
// Get IPv6 address list
func getIfaceIPv6(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 *v6Server) Start() error {
if !s.conf.Enabled {
return nil
}
iface, err := net.InterfaceByName(s.conf.InterfaceName)
if err != nil {
return wrapErrPrint(err, "Couldn't find interface by name %s", s.conf.InterfaceName)
}
log.Debug("DHCPv6: starting...")
s.conf.dnsIPAddrs = getIfaceIPv6(*iface)
if len(s.conf.dnsIPAddrs) == 0 {
log.Debug("DHCPv6: no IPv6 address for interface %s", iface.Name)
return nil
}
if len(iface.HardwareAddr) != 6 {
return fmt.Errorf("DHCPv6: invalid MAC %s", iface.HardwareAddr)
}
s.sid = dhcpv6.Duid{
Type: dhcpv6.DUID_LLT,
HwType: iana.HWTypeEthernet,
LinkLayerAddr: iface.HardwareAddr,
Time: dhcpv6.GetTime(),
}
laddr := &net.UDPAddr{
IP: net.ParseIP("::"),
Port: dhcpv6.DefaultServerPort,
}
s.srv, err = server6.NewServer(iface.Name, laddr, s.packetHandler, server6.WithDebugLogger())
if err != nil {
return err
}
go func() {
err = s.srv.Serve()
log.Debug("DHCPv6: srv.Serve: %s", err)
}()
return nil
}
// Stop - stop server
func (s *v6Server) Stop() {
if s.srv == nil {
return
}
log.Debug("DHCPv6: stopping")
err := s.srv.Close()
if err != nil {
log.Error("DHCPv6: srv.Close: %s", err)
}
// now server.Serve() will return
s.srv = nil
}
// Create DHCPv6 server
func v6Create(conf V6ServerConf) (DHCPServer, error) {
s := &v6Server{}
s.conf = conf
if !conf.Enabled {
return s, nil
}
s.conf.ipStart = net.ParseIP(conf.RangeStart)
if s.conf.ipStart == nil {
return s, fmt.Errorf("DHCPv6: invalid range-start IP: %s", conf.RangeStart)
}
if conf.LeaseDuration == 0 {
s.conf.leaseTime = time.Hour * 24
s.conf.LeaseDuration = uint32(s.conf.leaseTime.Seconds())
} else {
s.conf.leaseTime = time.Second * time.Duration(conf.LeaseDuration)
}
return s, nil
}

225
dhcpd/v6_test.go Normal file
View File

@ -0,0 +1,225 @@
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
package dhcpd
import (
"net"
"testing"
"github.com/insomniacslk/dhcp/dhcpv6"
"github.com/insomniacslk/dhcp/iana"
"github.com/stretchr/testify/assert"
)
func notify6(flags uint32) {
}
func TestV6StaticLeaseAddRemove(t *testing.T) {
conf := V6ServerConf{
Enabled: true,
RangeStart: "2001::1",
notify: notify6,
}
s, err := v6Create(conf)
assert.True(t, err == nil)
ls := s.GetLeases(LeasesStatic)
assert.Equal(t, 0, len(ls))
// add static lease
l := Lease{}
l.IP = net.ParseIP("2001::1")
l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa")
assert.True(t, s.AddStaticLease(l) == nil)
// try to add static lease - fail
assert.True(t, s.AddStaticLease(l) != nil)
// check
ls = s.GetLeases(LeasesStatic)
assert.Equal(t, 1, len(ls))
assert.Equal(t, "2001::1", ls[0].IP.String())
assert.Equal(t, "aa:aa:aa:aa:aa:aa", ls[0].HWAddr.String())
assert.True(t, ls[0].Expiry.Unix() == leaseExpireStatic)
// try to remove static lease - fail
l.IP = net.ParseIP("2001::2")
l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa")
assert.True(t, s.RemoveStaticLease(l) != nil)
// remove static lease
l.IP = net.ParseIP("2001::1")
l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa")
assert.True(t, s.RemoveStaticLease(l) == nil)
// check
ls = s.GetLeases(LeasesStatic)
assert.Equal(t, 0, len(ls))
}
func TestV6StaticLeaseAddReplaceDynamic(t *testing.T) {
conf := V6ServerConf{
Enabled: true,
RangeStart: "2001::1",
notify: notify6,
}
sIface, err := v6Create(conf)
s := sIface.(*v6Server)
assert.True(t, err == nil)
// add dynamic lease
ld := Lease{}
ld.IP = net.ParseIP("2001::1")
ld.HWAddr, _ = net.ParseMAC("11:aa:aa:aa:aa:aa")
s.addLease(&ld)
// add dynamic lease
{
ld := Lease{}
ld.IP = net.ParseIP("2001::2")
ld.HWAddr, _ = net.ParseMAC("22:aa:aa:aa:aa:aa")
s.addLease(&ld)
}
// add static lease with the same IP
l := Lease{}
l.IP = net.ParseIP("2001::1")
l.HWAddr, _ = net.ParseMAC("33:aa:aa:aa:aa:aa")
assert.True(t, s.AddStaticLease(l) == nil)
// add static lease with the same MAC
l = Lease{}
l.IP = net.ParseIP("2001::3")
l.HWAddr, _ = net.ParseMAC("22:aa:aa:aa:aa:aa")
assert.True(t, s.AddStaticLease(l) == nil)
// check
ls := s.GetLeases(LeasesStatic)
assert.Equal(t, 2, len(ls))
assert.Equal(t, "2001::1", ls[0].IP.String())
assert.Equal(t, "33:aa:aa:aa:aa:aa", ls[0].HWAddr.String())
assert.True(t, ls[0].Expiry.Unix() == leaseExpireStatic)
assert.Equal(t, "2001::3", ls[1].IP.String())
assert.Equal(t, "22:aa:aa:aa:aa:aa", ls[1].HWAddr.String())
assert.True(t, ls[1].Expiry.Unix() == leaseExpireStatic)
}
func TestV6GetLease(t *testing.T) {
conf := V6ServerConf{
Enabled: true,
RangeStart: "2001::1",
notify: notify6,
}
sIface, err := v6Create(conf)
s := sIface.(*v6Server)
assert.True(t, err == nil)
s.conf.dnsIPAddrs = []net.IP{net.ParseIP("2000::1")}
s.sid = dhcpv6.Duid{
Type: dhcpv6.DUID_LLT,
HwType: iana.HWTypeEthernet,
}
s.sid.LinkLayerAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa")
l := Lease{}
l.IP = net.ParseIP("2001::1")
l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa")
assert.True(t, s.AddStaticLease(l) == nil)
// "Solicit"
mac, _ := net.ParseMAC("aa:aa:aa:aa:aa:aa")
req, _ := dhcpv6.NewSolicit(mac)
msg, _ := req.GetInnerMessage()
resp, _ := dhcpv6.NewAdvertiseFromSolicit(msg)
assert.True(t, s.process(msg, req, resp))
resp.AddOption(dhcpv6.OptServerID(s.sid))
// check "Advertise"
assert.Equal(t, dhcpv6.MessageTypeAdvertise, resp.Type())
oia := resp.Options.OneIANA()
oiaAddr := oia.Options.OneAddress()
assert.Equal(t, "2001::1", oiaAddr.IPv6Addr.String())
assert.Equal(t, s.conf.leaseTime.Seconds(), oiaAddr.ValidLifetime.Seconds())
// "Request"
req, _ = dhcpv6.NewRequestFromAdvertise(resp)
msg, _ = req.GetInnerMessage()
resp, _ = dhcpv6.NewReplyFromMessage(msg)
assert.True(t, s.process(msg, req, resp))
// check "Reply"
assert.Equal(t, dhcpv6.MessageTypeReply, resp.Type())
oia = resp.Options.OneIANA()
oiaAddr = oia.Options.OneAddress()
assert.Equal(t, "2001::1", oiaAddr.IPv6Addr.String())
assert.Equal(t, s.conf.leaseTime.Seconds(), oiaAddr.ValidLifetime.Seconds())
dnsAddrs := resp.Options.DNS()
assert.Equal(t, 1, len(dnsAddrs))
assert.Equal(t, "2000::1", dnsAddrs[0].String())
// check lease
ls := s.GetLeases(LeasesStatic)
assert.Equal(t, 1, len(ls))
assert.Equal(t, "2001::1", ls[0].IP.String())
assert.Equal(t, "aa:aa:aa:aa:aa:aa", ls[0].HWAddr.String())
}
func TestV6GetDynamicLease(t *testing.T) {
conf := V6ServerConf{
Enabled: true,
RangeStart: "2001::2",
notify: notify6,
}
sIface, err := v6Create(conf)
s := sIface.(*v6Server)
assert.True(t, err == nil)
s.conf.dnsIPAddrs = []net.IP{net.ParseIP("2000::1")}
s.sid = dhcpv6.Duid{
Type: dhcpv6.DUID_LLT,
HwType: iana.HWTypeEthernet,
}
s.sid.LinkLayerAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa")
// "Solicit"
mac, _ := net.ParseMAC("aa:aa:aa:aa:aa:aa")
req, _ := dhcpv6.NewSolicit(mac)
msg, _ := req.GetInnerMessage()
resp, _ := dhcpv6.NewAdvertiseFromSolicit(msg)
assert.True(t, s.process(msg, req, resp))
resp.AddOption(dhcpv6.OptServerID(s.sid))
// check "Advertise"
assert.Equal(t, dhcpv6.MessageTypeAdvertise, resp.Type())
oia := resp.Options.OneIANA()
oiaAddr := oia.Options.OneAddress()
assert.Equal(t, "2001::2", oiaAddr.IPv6Addr.String())
// "Request"
req, _ = dhcpv6.NewRequestFromAdvertise(resp)
msg, _ = req.GetInnerMessage()
resp, _ = dhcpv6.NewReplyFromMessage(msg)
assert.True(t, s.process(msg, req, resp))
// check "Reply"
assert.Equal(t, dhcpv6.MessageTypeReply, resp.Type())
oia = resp.Options.OneIANA()
oiaAddr = oia.Options.OneAddress()
assert.Equal(t, "2001::2", oiaAddr.IPv6Addr.String())
dnsAddrs := resp.Options.DNS()
assert.Equal(t, 1, len(dnsAddrs))
assert.Equal(t, "2000::1", dnsAddrs[0].String())
// check lease
ls := s.GetLeases(LeasesDynamic)
assert.Equal(t, 1, len(ls))
assert.Equal(t, "2001::2", ls[0].IP.String())
assert.Equal(t, "aa:aa:aa:aa:aa:aa", ls[0].HWAddr.String())
assert.True(t, !ip6InRange(net.ParseIP("2001::2"), net.ParseIP("2001::1")))
assert.True(t, !ip6InRange(net.ParseIP("2001::2"), net.ParseIP("2002::2")))
assert.True(t, ip6InRange(net.ParseIP("2001::2"), net.ParseIP("2001::2")))
assert.True(t, ip6InRange(net.ParseIP("2001::2"), net.ParseIP("2001::3")))
}

View File

@ -44,10 +44,10 @@ var webRegistered bool
// //
// The zero Server is empty and ready for use. // The zero Server is empty and ready for use.
type Server struct { type Server struct {
dnsProxy *proxy.Proxy // DNS proxy instance dnsProxy *proxy.Proxy // DNS proxy instance
dnsFilter *dnsfilter.Dnsfilter // DNS filter instance dnsFilter *dnsfilter.Dnsfilter // DNS filter instance
dhcpServer *dhcpd.Server // DHCP server instance (optional) dhcpServer dhcpd.ServerInterface // DHCP server instance (optional)
queryLog querylog.QueryLog // Query log instance queryLog querylog.QueryLog // Query log instance
stats stats.Stats stats stats.Stats
access *accessCtx access *accessCtx
@ -72,7 +72,7 @@ type DNSCreateParams struct {
DNSFilter *dnsfilter.Dnsfilter DNSFilter *dnsfilter.Dnsfilter
Stats stats.Stats Stats stats.Stats
QueryLog querylog.QueryLog QueryLog querylog.QueryLog
DHCPServer *dhcpd.Server DHCPServer dhcpd.ServerInterface
} }
// NewServer creates a new instance of the dnsforward.Server // NewServer creates a new instance of the dnsforward.Server

View File

@ -1018,9 +1018,22 @@ func TestMatchDNSName(t *testing.T) {
assert.True(t, !matchDNSName(dnsNames, "*.host2")) assert.True(t, !matchDNSName(dnsNames, "*.host2"))
} }
type testDHCP struct {
}
func (d *testDHCP) Leases(flags int) []dhcpd.Lease {
l := dhcpd.Lease{}
l.IP = net.ParseIP("127.0.0.1").To4()
l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa")
l.Hostname = "localhost"
return []dhcpd.Lease{l}
}
func (d *testDHCP) SetOnLeaseChanged(onLeaseChanged dhcpd.OnLeaseChangedT) {
return
}
func TestPTRResponse(t *testing.T) { func TestPTRResponse(t *testing.T) {
dhcp := &dhcpd.Server{} dhcp := &testDHCP{}
dhcp.IPpool = make(map[[4]byte]net.HardwareAddr)
c := dnsfilter.Config{} c := dnsfilter.Config{}
f := dnsfilter.New(&c, nil) f := dnsfilter.New(&c, nil)
@ -1033,12 +1046,6 @@ func TestPTRResponse(t *testing.T) {
assert.True(t, err == nil) assert.True(t, err == nil)
assert.Nil(t, s.Start()) assert.Nil(t, s.Start())
l := dhcpd.Lease{}
l.IP = net.ParseIP("127.0.0.1").To4()
l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa")
l.Hostname = "localhost"
dhcp.AddStaticLease(l)
addr := s.dnsProxy.Addr(proxy.ProtoUDP) addr := s.dnsProxy.Addr(proxy.ProtoUDP)
req := createTestMessage("1.0.0.127.in-addr.arpa.") req := createTestMessage("1.0.0.127.in-addr.arpa.")
req.Question[0].Qtype = dns.TypePTR req.Question[0].Qtype = dns.TypePTR

6
go.mod
View File

@ -9,13 +9,17 @@ require (
github.com/NYTimes/gziphandler v1.1.1 github.com/NYTimes/gziphandler v1.1.1
github.com/fsnotify/fsnotify v1.4.7 github.com/fsnotify/fsnotify v1.4.7
github.com/gobuffalo/packr v1.30.1 github.com/gobuffalo/packr v1.30.1
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 // indirect
github.com/insomniacslk/dhcp v0.0.0-20200420235442-ed3125c2efe7
github.com/joomcode/errorx v1.0.1 github.com/joomcode/errorx v1.0.1
github.com/kardianos/service v1.1.0 github.com/kardianos/service v1.1.0
github.com/krolaw/dhcp4 v0.0.0-20180925202202-7cead472c414 github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 // indirect
github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 // indirect
github.com/miekg/dns v1.1.29 github.com/miekg/dns v1.1.29
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/sparrc/go-ping v0.0.0-20190613174326-4e5b6552494c github.com/sparrc/go-ping v0.0.0-20190613174326-4e5b6552494c
github.com/stretchr/testify v1.5.1 github.com/stretchr/testify v1.5.1
github.com/u-root/u-root v6.0.0+incompatible // indirect
go.etcd.io/bbolt v1.3.4 go.etcd.io/bbolt v1.3.4
golang.org/x/crypto v0.0.0-20200403201458-baeed622b8d8 golang.org/x/crypto v0.0.0-20200403201458-baeed622b8d8
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e

24
go.sum
View File

@ -49,9 +49,18 @@ github.com/gobuffalo/packr v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wK
github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk= github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk=
github.com/gobuffalo/packr/v2 v2.5.1 h1:TFOeY2VoGamPjQLiNDT3mn//ytzk236VMO2j7iHxJR4= github.com/gobuffalo/packr/v2 v2.5.1 h1:TFOeY2VoGamPjQLiNDT3mn//ytzk236VMO2j7iHxJR4=
github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw= github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8=
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/insomniacslk/dhcp v0.0.0-20200420235442-ed3125c2efe7 h1:iaCm+9nZdYb8XCSU2TfIb0qYTcAlIv2XzyKR2d2xZ38=
github.com/insomniacslk/dhcp v0.0.0-20200420235442-ed3125c2efe7/go.mod h1:CfMdguCK66I5DAUJgGKyNz8aB6vO5dZzkm9Xep6WGvw=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
@ -69,9 +78,12 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/krolaw/dhcp4 v0.0.0-20180925202202-7cead472c414 h1:6wnYc2S/lVM7BvR32BM74ph7bPgqMztWopMYKgVyEho=
github.com/krolaw/dhcp4 v0.0.0-20180925202202-7cead472c414/go.mod h1:0AqAH3ZogsCrvrtUpvc6EtVKbc3w6xwZhkvGLuqyi3o=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 h1:lez6TS6aAau+8wXUP3G9I3TGlmPFEq2CTxBaRqY6AGE=
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y=
github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 h1:aFkJ6lx4FPip+S+Uw4aTegFMct9shDvP+79PsSxpm3w=
github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg= github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg=
github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@ -104,11 +116,14 @@ github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/u-root/u-root v6.0.0+incompatible h1:YqPGmRoRyYmeg17KIWFRSyVq6LX5T6GSzawyA6wG6EE=
github.com/u-root/u-root v6.0.0+incompatible/go.mod h1:RYkpo8pTHrNjW08opNd/U6p/RJE7K0D8fXO0d47+3YY=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg= go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg=
@ -123,6 +138,9 @@ golang.org/x/crypto v0.0.0-20200403201458-baeed622b8d8/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
@ -134,8 +152,10 @@ golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@ -86,6 +86,7 @@ type clientsContainer struct {
} }
// Init initializes clients container // Init initializes clients container
// dhcpServer: optional
// Note: this function must be called only once // Note: this function must be called only once
func (clients *clientsContainer) Init(objects []clientObject, dhcpServer *dhcpd.Server, autoHosts *util.AutoHosts) { func (clients *clientsContainer) Init(objects []clientObject, dhcpServer *dhcpd.Server, autoHosts *util.AutoHosts) {
if clients.list != nil { if clients.list != nil {
@ -106,7 +107,9 @@ func (clients *clientsContainer) Init(objects []clientObject, dhcpServer *dhcpd.
if !clients.testing { if !clients.testing {
clients.addFromDHCP() clients.addFromDHCP()
clients.dhcpServer.SetOnLeaseChanged(clients.onDHCPLeaseChanged) if clients.dhcpServer != nil {
clients.dhcpServer.SetOnLeaseChanged(clients.onDHCPLeaseChanged)
}
clients.autoHosts.SetOnChanged(clients.onHostsChanged) clients.autoHosts.SetOnChanged(clients.onHostsChanged)
} }
} }

View File

@ -127,10 +127,6 @@ var config = configuration{
PortHTTPS: 443, PortHTTPS: 443,
PortDNSOverTLS: 853, // needs to be passed through to dnsproxy PortDNSOverTLS: 853, // needs to be passed through to dnsproxy
}, },
DHCP: dhcpd.ServerConfig{
LeaseDuration: 86400,
ICMPTimeout: 1000,
},
logSettings: logSettings{ logSettings: logSettings{
LogCompress: false, LogCompress: false,
LogLocalTime: false, LogLocalTime: false,
@ -156,6 +152,10 @@ func initConfig() {
config.DNS.DnsfilterConf.ParentalCacheSize = 1 * 1024 * 1024 config.DNS.DnsfilterConf.ParentalCacheSize = 1 * 1024 * 1024
config.DNS.DnsfilterConf.CacheTime = 30 config.DNS.DnsfilterConf.CacheTime = 30
config.Filters = defaultFilters() config.Filters = defaultFilters()
config.DHCP.Conf4.LeaseDuration = 86400
config.DHCP.Conf4.ICMPTimeout = 1000
config.DHCP.Conf6.LeaseDuration = 86400
} }
// getConfigFilename returns path to the current config file // getConfigFilename returns path to the current config file

View File

@ -56,6 +56,7 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
"protection_enabled": c.ProtectionEnabled, "protection_enabled": c.ProtectionEnabled,
} }
data["dhcp_available"] = (Context.dhcpServer != nil)
jsonVal, err := json.Marshal(data) jsonVal, err := json.Marshal(data)
if err != nil { if err != nil {

View File

@ -1,36 +0,0 @@
package home
import (
"github.com/joomcode/errorx"
)
func startDHCPServer() error {
if !config.DHCP.Enabled {
// not enabled, don't do anything
return nil
}
err := Context.dhcpServer.Init(config.DHCP)
if err != nil {
return errorx.Decorate(err, "Couldn't init DHCP server")
}
err = Context.dhcpServer.Start()
if err != nil {
return errorx.Decorate(err, "Couldn't start DHCP server")
}
return nil
}
func stopDHCPServer() error {
if !config.DHCP.Enabled {
return nil
}
err := Context.dhcpServer.Stop()
if err != nil {
return errorx.Decorate(err, "Couldn't stop DHCP server")
}
return nil
}

View File

@ -219,10 +219,8 @@ func run(args options) {
config.DHCP.WorkDir = Context.workDir config.DHCP.WorkDir = Context.workDir
config.DHCP.HTTPRegister = httpRegister config.DHCP.HTTPRegister = httpRegister
config.DHCP.ConfigModified = onConfigModified config.DHCP.ConfigModified = onConfigModified
Context.dhcpServer = dhcpd.Create(config.DHCP) if runtime.GOOS != "windows" {
if Context.dhcpServer == nil { Context.dhcpServer = dhcpd.Create(config.DHCP)
log.Error("Failed to initialize DHCP server, exiting")
os.Exit(1)
} }
Context.autoHosts.Init("") Context.autoHosts.Init("")
@ -317,9 +315,8 @@ func run(args options) {
} }
}() }()
err = startDHCPServer() if Context.dhcpServer != nil {
if err != nil { _ = Context.dhcpServer.Start()
log.Fatal(err)
} }
} }
@ -505,9 +502,9 @@ func cleanup() {
if err != nil { if err != nil {
log.Error("Couldn't stop DNS server: %s", err) log.Error("Couldn't stop DNS server: %s", err)
} }
err = stopDHCPServer()
if err != nil { if Context.dhcpServer != nil {
log.Error("Couldn't stop DHCP server: %s", err) Context.dhcpServer.Stop()
} }
Context.autoHosts.Close() Context.autoHosts.Close()

View File

@ -348,8 +348,6 @@ paths:
application/json: application/json:
schema: schema:
$ref: "#/components/schemas/DhcpConfig" $ref: "#/components/schemas/DhcpConfig"
description: DHCP configuration JSON
required: true
responses: responses:
"200": "200":
description: OK description: OK
@ -965,6 +963,8 @@ components:
maximum: 65535 maximum: 65535
protection_enabled: protection_enabled:
type: boolean type: boolean
dhcp_available:
type: boolean
querylog_enabled: querylog_enabled:
type: boolean type: boolean
running: running:
@ -999,6 +999,8 @@ components:
- tls://1.0.0.1 - tls://1.0.0.1
protection_enabled: protection_enabled:
type: boolean type: boolean
dhcp_available:
type: boolean
ratelimit: ratelimit:
type: integer type: integer
blocking_mode: blocking_mode:
@ -1259,17 +1261,18 @@ components:
description: Time period to keep data (1 | 7 | 30 | 90) description: Time period to keep data (1 | 7 | 30 | 90)
DhcpConfig: DhcpConfig:
type: object type: object
description: Built-in DHCP server configuration
required:
- enabled
- gateway_ip
- subnet_mask
- range_start
- range_end
- lease_duration
properties: properties:
enabled: enabled:
type: boolean type: boolean
interface_name:
type: string
v4:
$ref: "#/components/schemas/DhcpConfigV4"
v6:
$ref: "#/components/schemas/DhcpConfigV6"
DhcpConfigV4:
type: object
properties:
gateway_ip: gateway_ip:
type: string type: string
example: 192.168.1.1 example: 192.168.1.1
@ -1283,8 +1286,14 @@ components:
type: string type: string
example: 192.168.10.50 example: 192.168.10.50
lease_duration: lease_duration:
type: integer
DhcpConfigV6:
type: object
properties:
range_start:
type: string type: string
example: 12h lease_duration:
type: integer
DhcpLease: DhcpLease:
type: object type: object
description: DHCP lease information description: DHCP lease information
@ -1305,8 +1314,7 @@ components:
example: dell example: dell
expires: expires:
type: string type: string
format: date-time example: "2017-07-21T17:32:28Z"
example: 2017-07-21T17:32:28Z
DhcpStaticLease: DhcpStaticLease:
type: object type: object
description: DHCP static lease information description: DHCP static lease information
@ -1332,8 +1340,14 @@ components:
- config - config
- leases - leases
properties: properties:
config: enabled:
$ref: "#/components/schemas/DhcpConfig" type: boolean
interface_name:
type: string
v4:
$ref: "#/components/schemas/DhcpConfigV4"
v6:
$ref: "#/components/schemas/DhcpConfigV6"
leases: leases:
type: array type: array
items: items: