+ dhcpv6 server; rewrite dhcpv4 server; changed API
This commit is contained in:
parent
c3123473cf
commit
a3317c08c4
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
package dhcpd
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) {
|
||||||
|
return false, fmt.Errorf("not supported")
|
||||||
|
}
|
75
dhcpd/db.go
75
dhcpd/db.go
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
749
dhcpd/dhcpd.go
749
dhcpd/dhcpd.go
|
@ -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()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
|
@ -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 {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
|
||||||
|
|
||||||
package dhcpd
|
package dhcpd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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()))
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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")))
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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
6
go.mod
|
@ -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
24
go.sum
|
@ -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=
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
36
home/dhcp.go
36
home/dhcp.go
|
@ -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
|
|
||||||
}
|
|
17
home/home.go
17
home/home.go
|
@ -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()
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in New Issue