diff --git a/AGHTechDoc.md b/AGHTechDoc.md index e50982ba..809b0c72 100644 --- a/AGHTechDoc.md +++ b/AGHTechDoc.md @@ -12,6 +12,7 @@ Contents: * Updating * Get version command * Update command +* API: Get global status * TLS * API: Get TLS configuration * API: Set TLS configuration @@ -24,6 +25,7 @@ Contents: * API: Find clients by IP * DHCP server * DHCP server in DNS + * "Show DHCP interfaces" command * "Show DHCP status" command * "Check DHCP" command * "Enable DHCP" command @@ -376,6 +378,28 @@ Error response: 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 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. +### "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 Request: @@ -416,16 +462,19 @@ Response: 200 OK { - "config":{ - "enabled":false, - "interface_name":"...", + "enabled":false, + "interface_name":"...", + "v4":{ "gateway_ip":"...", "subnet_mask":"...", - "range_start":"...", + "range_start":"...", // if empty: DHCPv4 won't be enabled "range_end":"...", "lease_duration":60, - "icmp_timeout_msec":0 }, + "v6":{ + "range_start":"...", // if empty: DHCPv6 won't be enabled + "lease_duration":60, + } "leases":[ {"ip":"...","mac":"...","hostname":"...","expires":"..."} ... @@ -484,14 +533,19 @@ Request: POST /control/dhcp/set_config { - "enabled":true, - "interface_name":"vboxnet0", + "enabled":true, + "interface_name":"vboxnet0", + "v4":{ "gateway_ip":"192.169.56.1", "subnet_mask":"255.255.255.0", - "range_start":"192.169.56.3", - "range_end":"192.169.56.3", + "range_start":"192.169.56.100", + "range_end":"192.169.56.200", // Note: first 3 octects must match "range_start" "lease_duration":60, - "icmp_timeout_msec":0 + }, + "v6":{ + "range_start":"...", + "lease_duration":60, + } } Response: @@ -500,6 +554,10 @@ Response: 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 diff --git a/dhcpd/check_other_dhcp.go b/dhcpd/check_other_dhcp.go index fb7fe98c..83c9ca1b 100644 --- a/dhcpd/check_other_dhcp.go +++ b/dhcpd/check_other_dhcp.go @@ -1,23 +1,22 @@ +// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris + package dhcpd import ( "bytes" - "crypto/rand" - "encoding/binary" "fmt" - "math" "net" "os" "time" "github.com/AdguardTeam/golibs/log" - "github.com/krolaw/dhcp4" + "github.com/insomniacslk/dhcp/dhcpv4" + "github.com/insomniacslk/dhcp/iana" "golang.org/x/net/ipv4" ) // CheckIfOtherDHCPServersPresent sends a DHCP request to the specified network interface, // and waits for a response for a period defined by defaultDiscoverTime -// nolint func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) { iface, err := net.InterfaceByName(ifaceName) if err != nil { @@ -25,60 +24,29 @@ func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) { } // get ipv4 address of an interface - ifaceIPNet := getIfaceIPv4(iface) - if ifaceIPNet == nil { - return false, fmt.Errorf("Couldn't find IPv4 address of interface %s %+v", ifaceName, iface) + ifaceIPNet := getIfaceIPv4(*iface) + if len(ifaceIPNet) == 0 { + return false, fmt.Errorf("couldn't find IPv4 address of interface %s %+v", ifaceName, iface) } - srcIP := ifaceIPNet.IP + srcIP := ifaceIPNet[0] src := net.JoinHostPort(srcIP.String(), "68") dst := "255.255.255.255:67" - // form a DHCP request packet, try to emulate existing client as much as possible - xID := make([]byte, 4) - n, err := rand.Read(xID) - if n != 4 && err == nil { - err = fmt.Errorf("Generated less than 4 bytes") - } + hostname, _ := os.Hostname() + + req, err := dhcpv4.NewDiscovery(iface.HardwareAddr) if err != nil { - return false, wrapErrPrint(err, "Couldn't generate random bytes") + return false, fmt.Errorf("dhcpv4.NewDiscovery: %s", err) } - hostname, err := os.Hostname() - if err != nil { - 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) + req.Options.Update(dhcpv4.OptClientIdentifier(iface.HardwareAddr)) + req.Options.Update(dhcpv4.OptHostName(hostname)) // resolve 0.0.0.0:68 udpAddr, err := net.ResolveUDPAddr("udp4", src) if err != nil { return false, wrapErrPrint(err, "Couldn't resolve UDP address %s", src) } - // spew.Dump(udpAddr, err) if !udpAddr.IP.To4().Equal(srcIP) { 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 cm := ipv4.ControlMessage{} - _, err = c.WriteTo(packet, &cm, dstAddr) + _, err = c.WriteTo(req.ToBytes(), &cm, dstAddr) if err != nil { 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 b := make([]byte, 1500) _ = c.SetReadDeadline(time.Now().Add(defaultDiscoverTime)) - n, _, _, err = c.ReadFrom(b) + n, _, _, err := c.ReadFrom(b) if isTimeout(err) { // timed out -- no DHCP servers return false, nil @@ -121,27 +89,24 @@ func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) { if err != nil { return false, wrapErrPrint(err, "Couldn't receive packet") } - // spew.Dump(n, fromAddr, err, b) log.Tracef("Received packet (%v bytes)", n) - if n < 240 { - // packet too small for dhcp + response, err := dhcpv4.FromBytes(b[:n]) + if err != nil { + log.Debug("DHCPv4: dhcpv4.FromBytes: %s", err) continue } - response := dhcp4.Packet(b[:n]) - if response.OpCode() != dhcp4.BootReply || - response.HType() != 1 /*Ethernet*/ || - response.HLen() > 16 || - !bytes.Equal(response.CHAddr(), iface.HardwareAddr) || - !bytes.Equal(response.XId(), xID) { - continue - } + log.Debug("DHCPv4: received message from server: %s", response.Summary()) - parsedOptions := response.ParseOptions() - if t := parsedOptions[dhcp4.OptionDHCPMessageType]; len(t) != 1 { - continue //packet without DHCP message type + if !(response.OpCode == dhcpv4.OpcodeBootReply && + response.HWType == iana.HWTypeEthernet && + 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") diff --git a/dhcpd/check_other_dhcp_windows.go b/dhcpd/check_other_dhcp_windows.go new file mode 100644 index 00000000..bd3849b8 --- /dev/null +++ b/dhcpd/check_other_dhcp_windows.go @@ -0,0 +1,7 @@ +package dhcpd + +import "fmt" + +func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) { + return false, fmt.Errorf("not supported") +} diff --git a/dhcpd/db.go b/dhcpd/db.go index fdf94059..b01f9635 100644 --- a/dhcpd/db.go +++ b/dhcpd/db.go @@ -11,7 +11,6 @@ import ( "github.com/AdguardTeam/golibs/file" "github.com/AdguardTeam/golibs/log" - "github.com/krolaw/dhcp4" ) const dbFilename = "leases.db" @@ -31,21 +30,12 @@ func normalizeIP(ip net.IP) net.IP { return ip } -// Safe version of dhcp4.IPInRange() -func ipInRange(start, stop, ip net.IP) bool { - if len(start) != len(stop) || - len(start) != len(ip) { - return false - } - return dhcp4.IPInRange(start, stop, ip) -} - // Load lease table from DB func (s *Server) dbLoad() { - s.leases = nil - s.IPpool = make(map[[4]byte]net.HardwareAddr) dynLeases := []*Lease{} staticLeases := []*Lease{} + v6StaticLeases := []*Lease{} + v6DynLeases := []*Lease{} data, err := ioutil.ReadFile(s.conf.DBFilePath) if err != nil { @@ -66,10 +56,8 @@ func (s *Server) dbLoad() { for i := range obj { obj[i].IP = normalizeIP(obj[i].IP) - if obj[i].Expiry != leaseExpireStatic && - !ipInRange(s.leaseStart, s.leaseStop, obj[i].IP) { - - log.Tracef("Skipping a lease with IP %v: not within current IP range", obj[i].IP) + if !(len(obj[i].IP) == 4 || len(obj[i].IP) == 16) { + log.Info("DHCP: invalid IP: %s", obj[i].IP) continue } @@ -80,20 +68,32 @@ func (s *Server) dbLoad() { Expiry: time.Unix(obj[i].Expiry, 0), } - if obj[i].Expiry == leaseExpireStatic { - staticLeases = append(staticLeases, &lease) + if len(obj[i].IP) == 16 { + if obj[i].Expiry == leaseExpireStatic { + v6StaticLeases = append(v6StaticLeases, &lease) + } else { + v6DynLeases = append(v6DynLeases, &lease) + } + } else { - dynLeases = append(dynLeases, &lease) + if obj[i].Expiry == leaseExpireStatic { + staticLeases = append(staticLeases, &lease) + } else { + dynLeases = append(dynLeases, &lease) + } } } - s.leases = normalizeLeases(staticLeases, dynLeases) + leases4 := normalizeLeases(staticLeases, dynLeases) + s.srv4.ResetLeases(leases4) - for _, lease := range s.leases { - s.reserveIP(lease.IP, lease.HWAddr) + leases6 := normalizeLeases(v6StaticLeases, v6DynLeases) + 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 @@ -127,19 +127,36 @@ func normalizeLeases(staticLeases, dynLeases []*Lease) []*Lease { func (s *Server) dbStore() { var leases []leaseJSON - for i := range s.leases { - if s.leases[i].Expiry.Unix() == 0 { + leases4 := s.srv4.GetLeasesRef() + for _, l := range leases4 { + if l.Expiry.Unix() == 0 { continue } lease := leaseJSON{ - HWAddr: s.leases[i].HWAddr, - IP: s.leases[i].IP, - Hostname: s.leases[i].Hostname, - Expiry: s.leases[i].Expiry.Unix(), + HWAddr: l.HWAddr, + IP: l.IP, + Hostname: l.Hostname, + Expiry: l.Expiry.Unix(), } leases = append(leases, lease) } + 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) if err != nil { log.Error("json.Marshal: %v", err) diff --git a/dhcpd/dhcp_http.go b/dhcpd/dhcp_http.go index 9105d76b..7ae6075a 100644 --- a/dhcpd/dhcp_http.go +++ b/dhcpd/dhcp_http.go @@ -40,13 +40,70 @@ func convertLeases(inputLeases []Lease, includeExpires bool) []map[string]string 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) { leases := convertLeases(s.Leases(LeasesDynamic), true) staticLeases := convertLeases(s.Leases(LeasesStatic), false) + + v4conf := V4ServerConf{} + s.srv4.WriteDiskConfig4(&v4conf) + + v6conf := V6ServerConf{} + s.srv6.WriteDiskConfig6(&v6conf) + status := map[string]interface{}{ - "config": s.conf, - "leases": leases, - "static_leases": staticLeases, + "enabled": s.conf.Enabled, + "interface_name": s.conf.InterfaceName, + "v4": v4ServerConfToJSON(v4conf), + "v6": v6ServerConfToJSON(v6conf), + "leases": leases, + "static_leases": staticLeases, } w.Header().Set("Content-Type", "application/json") @@ -64,8 +121,10 @@ type staticLeaseJSON struct { } type dhcpServerConfigJSON struct { - ServerConfig `json:",inline"` - StaticLeases []staticLeaseJSON `json:"static_leases"` + Enabled bool `json:"enabled"` + InterfaceName string `json:"interface_name"` + V4 v4ServerConfJSON `json:"v4"` + V6 v6ServerConfJSON `json:"v6"` } 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 } - 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 { - httpError(r, w, http.StatusBadRequest, "Invalid DHCP configuration: %s", err) + httpError(r, w, http.StatusBadRequest, "Invalid DHCPv4 configuration: %s", err) return } - err = s.Stop() - if err != nil { - log.Error("failed to stop the DHCP server: %s", err) + v6conf := v6JSONToServerConf(newconfig.V6) + v6conf.Enabled = newconfig.Enabled + if len(v6conf.RangeStart) == 0 { + v6conf.Enabled = false } - - err = s.Init(newconfig.ServerConfig) - if err != nil { - httpError(r, w, http.StatusBadRequest, "Invalid DHCP configuration: %s", err) + v6conf.InterfaceName = newconfig.InterfaceName + v6conf.notify = s.onNotify + s6, err := v6Create(v6conf) + if s6 == nil { + httpError(r, w, http.StatusBadRequest, "Invalid DHCPv6 configuration: %s", err) 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() if newconfig.Enabled { @@ -246,20 +332,45 @@ func (s *Server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request 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 { httpError(r, w, http.StatusBadRequest, "invalid IP") 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{ IP: ip, HWAddr: mac, Hostname: lj.Hostname, } - err = s.AddStaticLease(lease) + err = s.srv4.AddStaticLease(lease) if err != nil { httpError(r, w, http.StatusBadRequest, "%s", err) return @@ -275,7 +386,28 @@ func (s *Server) handleDHCPRemoveStaticLease(w http.ResponseWriter, r *http.Requ 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 { httpError(r, w, http.StatusBadRequest, "invalid IP") return @@ -288,7 +420,7 @@ func (s *Server) handleDHCPRemoveStaticLease(w http.ResponseWriter, r *http.Requ HWAddr: mac, Hostname: lj.Hostname, } - err = s.RemoveStaticLease(lease) + err = s.srv4.RemoveStaticLease(lease) if err != nil { httpError(r, w, http.StatusBadRequest, "%s", err) return @@ -296,24 +428,29 @@ func (s *Server) handleDHCPRemoveStaticLease(w http.ResponseWriter, r *http.Requ } func (s *Server) handleReset(w http.ResponseWriter, r *http.Request) { - err := s.Stop() - if err != nil { - log.Error("DHCP: Stop: %s", err) - } + s.Stop() - err = os.Remove(s.conf.DBFilePath) + err := os.Remove(s.conf.DBFilePath) if err != nil && !os.IsNotExist(err) { log.Error("DHCP: os.Remove: %s: %s", s.conf.DBFilePath, err) } oldconf := s.conf s.conf = ServerConfig{} - s.conf.LeaseDuration = 86400 - s.conf.ICMPTimeout = 1000 s.conf.WorkDir = oldconf.WorkDir s.conf.HTTPRegister = oldconf.HTTPRegister s.conf.ConfigModified = oldconf.ConfigModified s.conf.DBFilePath = oldconf.DBFilePath + + 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() } diff --git a/dhcpd/dhcpd.go b/dhcpd/dhcpd.go index 0ec83370..11937eb1 100644 --- a/dhcpd/dhcpd.go +++ b/dhcpd/dhcpd.go @@ -1,18 +1,12 @@ package dhcpd import ( - "bytes" - "fmt" "net" "net/http" "path/filepath" - "strings" - "sync" "time" "github.com/AdguardTeam/golibs/log" - "github.com/krolaw/dhcp4" - ping "github.com/sparrc/go-ping" ) const defaultDiscoverTime = time.Second * 3 @@ -21,9 +15,8 @@ const leaseExpireStatic = 1 var webHandlersRegistered = false // Lease contains the necessary information about a DHCP lease -// field ordering is important -- yaml fields will mirror ordering from here type Lease struct { - HWAddr net.HardwareAddr `json:"mac" yaml:"hwaddr"` + HWAddr net.HardwareAddr `json:"mac"` IP net.IP `json:"ip"` Hostname string `json:"hostname"` @@ -35,29 +28,23 @@ type Lease struct { // ServerConfig - DHCP server configuration // field ordering is important -- yaml fields will mirror ordering from here type ServerConfig struct { - Enabled bool `json:"enabled" yaml:"enabled"` - InterfaceName string `json:"interface_name" yaml:"interface_name"` // eth0, en0 and so on - GatewayIP string `json:"gateway_ip" yaml:"gateway_ip"` - SubnetMask string `json:"subnet_mask" yaml:"subnet_mask"` - RangeStart string `json:"range_start" yaml:"range_start"` - RangeEnd string `json:"range_end" yaml:"range_end"` - LeaseDuration uint32 `json:"lease_duration" yaml:"lease_duration"` // in seconds + Enabled bool `yaml:"enabled"` + InterfaceName string `yaml:"interface_name"` - // IP conflict detector: time (ms) to wait for ICMP reply. - // 0: disable - ICMPTimeout uint32 `json:"icmp_timeout_msec" yaml:"icmp_timeout_msec"` + Conf4 V4ServerConf `yaml:"dhcpv4"` + Conf6 V6ServerConf `yaml:"dhcpv6"` - WorkDir string `json:"-" yaml:"-"` - DBFilePath string `json:"-" yaml:"-"` // path to DB file + WorkDir string `yaml:"-"` + DBFilePath string `yaml:"-"` // path to DB file // Called when the configuration is changed by HTTP request - ConfigModified func() `json:"-" yaml:"-"` + ConfigModified func() `yaml:"-"` // 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() const ( @@ -65,87 +52,91 @@ const ( LeaseChangedAddedStatic LeaseChangedRemovedStatic LeaseChangedBlacklisted + + LeaseChangedDBStore ) // Server - the current state of the DHCP server type Server struct { - conn *filterConn // listening UDP socket - - 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 + srv4 DHCPServer + srv6 DHCPServer conf ServerConfig // Called when the leases DB is modified - onLeaseChanged []onLeaseChangedT + onLeaseChanged []OnLeaseChangedT } -// Print information about the available network interfaces -func printInterfaces() { - ifaces, _ := net.Interfaces() - var buf strings.Builder - for i := range ifaces { - buf.WriteString(fmt.Sprintf("\"%s\", ", ifaces[i].Name)) - } - log.Info("Available network interfaces: %s", buf.String()) +type ServerInterface interface { + Leases(flags int) []Lease + SetOnLeaseChanged(onLeaseChanged OnLeaseChangedT) } // CheckConfig checks the configuration func (s *Server) CheckConfig(config ServerConfig) error { - tmpServer := Server{} - return tmpServer.setConfig(config) + return nil } // Create - create object func Create(config ServerConfig) *Server { s := Server{} - s.conf = config + s.conf.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) - if s.conf.Enabled { - err := s.setConfig(config) - if err != nil { - log.Error("DHCP: %s", err) - return nil - } - } if !webHandlersRegistered && s.conf.HTTPRegister != nil { webHandlersRegistered = true 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, // because we need static leases functionality available beforehand s.dbLoad() return &s } -// Init checks the configuration and initializes the server -func (s *Server) Init(config ServerConfig) error { - err := s.setConfig(config) - if err != nil { - return err +// server calls this function after DB is updated +func (s *Server) onNotify(flags uint32) { + if flags == LeaseChangedDBStore { + s.dbStore() + return } - return nil + + s.notify(int(flags)) } // SetOnLeaseChanged - set callback -func (s *Server) SetOnLeaseChanged(onLeaseChanged onLeaseChangedT) { +func (s *Server) SetOnLeaseChanged(onLeaseChanged OnLeaseChangedT) { s.onLeaseChanged = append(s.onLeaseChanged, onLeaseChanged) } @@ -160,563 +151,33 @@ func (s *Server) notify(flags int) { // WriteDiskConfig - write configuration func (s *Server) WriteDiskConfig(c *ServerConfig) { - *c = s.conf -} - -func (s *Server) setConfig(config ServerConfig) error { - iface, err := net.InterfaceByName(config.InterfaceName) - if err != nil { - printInterfaces() - return wrapErrPrint(err, "Couldn't find interface by name %s", config.InterfaceName) - } - - // get ipv4 address of an interface - s.ipnet = getIfaceIPv4(iface) - if s.ipnet == nil { - return wrapErrPrint(err, "Couldn't find IPv4 address of interface %s %+v", config.InterfaceName, iface) - } - - if config.LeaseDuration == 0 { - s.leaseTime = time.Hour * 2 - } else { - s.leaseTime = time.Second * time.Duration(config.LeaseDuration) - } - - s.leaseStart, err = parseIPv4(config.RangeStart) - if err != nil { - return wrapErrPrint(err, "Failed to parse range start address %s", config.RangeStart) - } - - s.leaseStop, err = parseIPv4(config.RangeEnd) - if err != nil { - return wrapErrPrint(err, "Failed to parse range end address %s", config.RangeEnd) - } - if dhcp4.IPRange(s.leaseStart, s.leaseStop) <= 0 { - return wrapErrPrint(err, "DHCP: Incorrect range_start/range_end values") - } - - subnet, err := parseIPv4(config.SubnetMask) - if err != nil || !isValidSubnetMask(subnet) { - return wrapErrPrint(err, "Failed to parse subnet mask %s", config.SubnetMask) - } - - // if !bytes.Equal(subnet, s.ipnet.Mask) { - // return wrapErrPrint(err, "specified subnet mask %s does not meatch interface %s subnet mask %s", s.SubnetMask, s.InterfaceName, s.ipnet.Mask) - // } - - router, err := parseIPv4(config.GatewayIP) - if err != nil { - return wrapErrPrint(err, "Failed to parse gateway IP %s", config.GatewayIP) - } - - s.leaseOptions = dhcp4.Options{ - dhcp4.OptionSubnetMask: subnet, - dhcp4.OptionRouter: router, - dhcp4.OptionDomainNameServer: s.ipnet.IP, - } - - oldconf := s.conf - s.conf = config - s.conf.WorkDir = oldconf.WorkDir - s.conf.HTTPRegister = oldconf.HTTPRegister - s.conf.ConfigModified = oldconf.ConfigModified - s.conf.DBFilePath = oldconf.DBFilePath - return nil + c.Enabled = s.conf.Enabled + c.InterfaceName = s.conf.InterfaceName + s.srv4.WriteDiskConfig4(&c.Conf4) + s.srv6.WriteDiskConfig6(&c.Conf6) } // Start will listen on port 67 and serve DHCP requests. func (s *Server) Start() error { - // TODO: don't close if interface and addresses are the same - if s.conn != nil { - _ = s.closeConn() - } - - iface, err := net.InterfaceByName(s.conf.InterfaceName) + err := s.srv4.Start() 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 { - 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 } // Stop closes the listening UDP socket -func (s *Server) Stop() error { - if s.conn == nil { - // nothing to do, return silently - return nil - } - - s.stopping = true - - err := s.closeConn() - if err != nil { - return wrapErrPrint(err, "Couldn't close UDP listening socket") - } - - // We've just closed the listening socket. - // Worker thread should exit right after it tries to read from the socket. - s.mutex.Lock() - for s.running { - s.cond.Wait() - } - s.mutex.Unlock() - return nil -} - -// closeConn will close the connection and set it to zero -func (s *Server) closeConn() error { - if s.conn == nil { - return nil - } - err := s.conn.Close() - s.conn = nil - return err -} - -// Reserve a lease for the client -func (s *Server) reserveLease(p dhcp4.Packet) (*Lease, error) { - // WARNING: do not remove copy() - // the given hwaddr by p.CHAddr() in the packet survives only during ServeDHCP() call - // since we need to retain it we need to make our own copy - hwaddrCOW := p.CHAddr() - hwaddr := make(net.HardwareAddr, len(hwaddrCOW)) - copy(hwaddr, hwaddrCOW) - // not assigned a lease, create new one, find IP from LRU - hostname := p.ParseOptions()[dhcp4.OptionHostName] - lease := &Lease{HWAddr: hwaddr, Hostname: string(hostname)} - - log.Tracef("Lease not found for %s: creating new one", hwaddr) - - s.leasesLock.Lock() - defer s.leasesLock.Unlock() - - ip, err := s.findFreeIP(hwaddr) - if err != nil { - i := s.findExpiredLease() - if i < 0 { - return nil, wrapErrPrint(err, "Couldn't find free IP for the lease %s", hwaddr.String()) - } - - log.Tracef("Assigning IP address %s to %s (lease for %s expired at %s)", - s.leases[i].IP, hwaddr, s.leases[i].HWAddr, s.leases[i].Expiry) - lease.IP = s.leases[i].IP - s.leases[i] = lease - - s.reserveIP(lease.IP, hwaddr) - return lease, nil - } - - log.Tracef("Assigning to %s IP address %s", hwaddr, ip.String()) - lease.IP = ip - s.leases = append(s.leases, lease) - return lease, nil -} - -// Find a lease for the client -func (s *Server) findLease(p dhcp4.Packet) *Lease { - hwaddr := p.CHAddr() - for i := range s.leases { - if bytes.Equal([]byte(hwaddr), []byte(s.leases[i].HWAddr)) { - // log.Tracef("bytes.Equal(%s, %s) returned true", hwaddr, s.leases[i].hwaddr) - return s.leases[i] - } - } - return nil -} - -// Find an expired lease and return its index or -1 -func (s *Server) findExpiredLease() int { - now := time.Now().Unix() - for i, lease := range s.leases { - if lease.Expiry.Unix() <= now && lease.Expiry.Unix() != leaseExpireStatic { - return i - } - } - return -1 -} - -func (s *Server) findFreeIP(hwaddr net.HardwareAddr) (net.IP, error) { - // go from start to end, find unreserved IP - var foundIP net.IP - for i := 0; i < dhcp4.IPRange(s.leaseStart, s.leaseStop); i++ { - newIP := dhcp4.IPAdd(s.leaseStart, i) - foundHWaddr := s.findReservedHWaddr(newIP) - log.Tracef("tried IP %v, got hwaddr %v", newIP, foundHWaddr) - if foundHWaddr != nil && len(foundHWaddr) != 0 { - // if !bytes.Equal(foundHWaddr, hwaddr) { - // log.Tracef("SHOULD NOT HAPPEN: hwaddr in IP pool %s is not equal to hwaddr in lease %s", foundHWaddr, hwaddr) - // } - continue - } - foundIP = newIP - break - } - - if foundIP == nil { - // TODO: LRU - return nil, fmt.Errorf("couldn't find free entry in IP pool") - } - - s.reserveIP(foundIP, hwaddr) - - return foundIP, nil -} - -func (s *Server) findReservedHWaddr(ip net.IP) net.HardwareAddr { - rawIP := []byte(ip) - IP4 := [4]byte{rawIP[0], rawIP[1], rawIP[2], rawIP[3]} - return s.IPpool[IP4] -} - -func (s *Server) reserveIP(ip net.IP, hwaddr net.HardwareAddr) { - rawIP := []byte(ip) - IP4 := [4]byte{rawIP[0], rawIP[1], rawIP[2], rawIP[3]} - s.IPpool[IP4] = hwaddr -} - -func (s *Server) unreserveIP(ip net.IP) { - rawIP := []byte(ip) - IP4 := [4]byte{rawIP[0], rawIP[1], rawIP[2], rawIP[3]} - delete(s.IPpool, IP4) -} - -// ServeDHCP handles an incoming DHCP request -func (s *Server) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options dhcp4.Options) dhcp4.Packet { - s.printLeases() - - switch msgType { - case dhcp4.Discover: // Broadcast Packet From Client - Can I have an IP? - return s.handleDiscover(p, options) - - case dhcp4.Request: // Broadcast From Client - I'll take that IP (Also start for renewals) - // start/renew a lease -- update lease time - // some clients (OSX) just go right ahead and do Request first from previously known IP, if they get NAK, they restart full cycle with Discover then Request - return s.handleDHCP4Request(p, options) - - case dhcp4.Decline: // Broadcast From Client - Sorry I can't use that IP - return s.handleDecline(p, options) - - case dhcp4.Release: // From Client, I don't need that IP anymore - return s.handleRelease(p, options) - - case dhcp4.Inform: // From Client, I have this IP and there's nothing you can do about it - return s.handleInform(p, options) - - // from server -- ignore those but enumerate just in case - case dhcp4.Offer: // Broadcast From Server - Here's an IP - log.Printf("DHCP: received message from %s: Offer", p.CHAddr()) - - case dhcp4.ACK: // From Server, Yes you can have that IP - log.Printf("DHCP: received message from %s: ACK", p.CHAddr()) - - case dhcp4.NAK: // From Server, No you cannot have that IP - log.Printf("DHCP: received message from %s: NAK", p.CHAddr()) - - default: - log.Printf("DHCP: unknown packet %v from %s", msgType, p.CHAddr()) - return nil - } - return nil -} - -// Send ICMP to the specified machine -// Return TRUE if it doesn't reply, which probably means that the IP is available -func (s *Server) addrAvailable(target net.IP) bool { - - if s.conf.ICMPTimeout == 0 { - return true - } - - pinger, err := ping.NewPinger(target.String()) - if err != nil { - log.Error("ping.NewPinger(): %v", err) - return true - } - - pinger.SetPrivileged(true) - pinger.Timeout = time.Duration(s.conf.ICMPTimeout) * time.Millisecond - pinger.Count = 1 - reply := false - pinger.OnRecv = func(pkt *ping.Packet) { - // log.Tracef("Received ICMP Reply from %v", target) - reply = true - } - log.Tracef("Sending ICMP Echo to %v", target) - pinger.Run() - - if reply { - log.Info("DHCP: IP conflict: %v is already used by another device", target) - return false - } - - log.Tracef("ICMP procedure is complete: %v", target) - return true -} - -// Add the specified IP to the black list for a time period -func (s *Server) blacklistLease(lease *Lease) { - hw := make(net.HardwareAddr, 6) - s.leasesLock.Lock() - s.reserveIP(lease.IP, hw) - lease.HWAddr = hw - lease.Hostname = "" - lease.Expiry = time.Now().Add(s.leaseTime) - s.dbStore() - s.leasesLock.Unlock() - s.notify(LeaseChangedBlacklisted) -} - -// Return TRUE if DHCP packet is correct -func isValidPacket(p dhcp4.Packet) bool { - hw := p.CHAddr() - zeroes := make([]byte, len(hw)) - if bytes.Equal(hw, zeroes) { - log.Tracef("Packet has empty CHAddr") - return false - } - return true -} - -func (s *Server) handleDiscover(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet { - // find a lease, but don't update lease time - var lease *Lease - var err error - - reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress]) - hostname := p.ParseOptions()[dhcp4.OptionHostName] - log.Tracef("Message from client: Discover. ReqIP: %s HW: %s Hostname: %s", - reqIP, p.CHAddr(), hostname) - - if !isValidPacket(p) { - return nil - } - - lease = s.findLease(p) - for lease == nil { - lease, err = s.reserveLease(p) - if err != nil { - log.Error("Couldn't find free lease: %s", err) - return nil - } - - if !s.addrAvailable(lease.IP) { - s.blacklistLease(lease) - lease = nil - continue - } - - break - } - - opt := s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList]) - reply := dhcp4.ReplyPacket(p, dhcp4.Offer, s.ipnet.IP, lease.IP, s.leaseTime, opt) - log.Tracef("Replying with offer: offered IP %v for %v with options %+v", lease.IP, s.leaseTime, reply.ParseOptions()) - return reply -} - -func (s *Server) handleDHCP4Request(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet { - var lease *Lease - - reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress]) - log.Tracef("Message from client: Request. IP: %s ReqIP: %s HW: %s", - p.CIAddr(), reqIP, p.CHAddr()) - - if !isValidPacket(p) { - return nil - } - - server := options[dhcp4.OptionServerIdentifier] - if server != nil && !net.IP(server).Equal(s.ipnet.IP) { - log.Tracef("Request message not for this DHCP server (%v vs %v)", server, s.ipnet.IP) - return nil // Message not for this dhcp server - } - - if reqIP == nil { - reqIP = p.CIAddr() - - } else if reqIP == nil || reqIP.To4() == nil { - log.Tracef("Requested IP isn't a valid IPv4: %s", reqIP) - return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil) - } - - lease = s.findLease(p) - if lease == nil { - log.Tracef("Lease for %s isn't found", p.CHAddr()) - return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil) - } - - if !lease.IP.Equal(reqIP) { - log.Tracef("Lease for %s doesn't match requested/client IP: %s vs %s", - lease.HWAddr, lease.IP, reqIP) - return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil) - } - - if lease.Expiry.Unix() != leaseExpireStatic { - lease.Expiry = time.Now().Add(s.leaseTime) - s.leasesLock.Lock() - s.dbStore() - s.leasesLock.Unlock() - s.notify(LeaseChangedAdded) // Note: maybe we shouldn't call this function if only expiration time is updated - } - log.Tracef("Replying with ACK. IP: %s HW: %s Expire: %s", - lease.IP, lease.HWAddr, lease.Expiry) - opt := s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList]) - return dhcp4.ReplyPacket(p, dhcp4.ACK, s.ipnet.IP, lease.IP, s.leaseTime, opt) -} - -func (s *Server) handleInform(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet { - log.Tracef("Message from client: Inform. IP: %s HW: %s", - p.CIAddr(), p.CHAddr()) - - return nil -} - -func (s *Server) handleRelease(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet { - log.Tracef("Message from client: Release. IP: %s HW: %s", - p.CIAddr(), p.CHAddr()) - - return nil -} - -func (s *Server) handleDecline(p dhcp4.Packet, options dhcp4.Options) dhcp4.Packet { - reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress]) - log.Tracef("Message from client: Decline. IP: %s HW: %s", - reqIP, p.CHAddr()) - - return nil -} - -// AddStaticLease adds a static lease (thread-safe) -func (s *Server) AddStaticLease(l Lease) error { - if len(l.IP) != 4 { - return fmt.Errorf("invalid IP") - } - if len(l.HWAddr) != 6 { - return fmt.Errorf("invalid MAC") - } - l.Expiry = time.Unix(leaseExpireStatic, 0) - - s.leasesLock.Lock() - - if s.findReservedHWaddr(l.IP) != nil { - err := s.rmDynamicLeaseWithIP(l.IP) - if err != nil { - s.leasesLock.Unlock() - return err - } - } else { - err := s.rmDynamicLeaseWithMAC(l.HWAddr) - if err != nil { - s.leasesLock.Unlock() - return err - } - } - s.leases = append(s.leases, &l) - s.reserveIP(l.IP, l.HWAddr) - s.dbStore() - s.leasesLock.Unlock() - s.notify(LeaseChangedAddedStatic) - return nil -} - -// Remove a dynamic lease by IP address -func (s *Server) rmDynamicLeaseWithIP(ip net.IP) error { - var newLeases []*Lease - for _, lease := range s.leases { - if net.IP.Equal(lease.IP.To4(), ip) { - if lease.Expiry.Unix() == leaseExpireStatic { - return fmt.Errorf("static lease with the same IP already exists") - } - continue - } - newLeases = append(newLeases, lease) - } - s.leases = newLeases - s.unreserveIP(ip) - return nil -} - -// Remove a dynamic lease by IP address -func (s *Server) rmDynamicLeaseWithMAC(mac net.HardwareAddr) error { - var newLeases []*Lease - for _, lease := range s.leases { - if bytes.Equal(lease.HWAddr, mac) { - if lease.Expiry.Unix() == leaseExpireStatic { - return fmt.Errorf("static lease with the same IP already exists") - } - s.unreserveIP(lease.IP) - continue - } - newLeases = append(newLeases, lease) - } - s.leases = newLeases - return nil -} - -// Remove a lease -func (s *Server) rmLease(l Lease) error { - var newLeases []*Lease - for _, lease := range s.leases { - if net.IP.Equal(lease.IP.To4(), l.IP) { - if !bytes.Equal(lease.HWAddr, l.HWAddr) || - lease.Hostname != l.Hostname { - return fmt.Errorf("Lease not found") - } - continue - } - newLeases = append(newLeases, lease) - } - s.leases = newLeases - s.unreserveIP(l.IP) - return nil -} - -// RemoveStaticLease removes a static lease (thread-safe) -func (s *Server) RemoveStaticLease(l Lease) error { - if len(l.IP) != 4 { - return fmt.Errorf("invalid IP") - } - if len(l.HWAddr) != 6 { - return fmt.Errorf("invalid MAC") - } - - s.leasesLock.Lock() - - if s.findReservedHWaddr(l.IP) == nil { - s.leasesLock.Unlock() - return fmt.Errorf("lease not found") - } - - err := s.rmLease(l) - if err != nil { - s.leasesLock.Unlock() - return err - } - s.dbStore() - s.leasesLock.Unlock() - s.notify(LeaseChangedRemovedStatic) - return nil +func (s *Server) Stop() { + s.srv4.Stop() + s.srv6.Stop() } // flags for Leases() function @@ -728,69 +189,23 @@ const ( // Leases returns the list of current DHCP leases (thread-safe) func (s *Server) Leases(flags int) []Lease { - var result []Lease - now := time.Now().Unix() - s.leasesLock.RLock() - for _, lease := range s.leases { - if ((flags&LeasesDynamic) != 0 && lease.Expiry.Unix() > now) || - ((flags&LeasesStatic) != 0 && lease.Expiry.Unix() == leaseExpireStatic) { - result = append(result, *lease) - } - } - s.leasesLock.RUnlock() + result := s.srv4.GetLeases(flags) + + v6leases := s.srv6.GetLeases(flags) + result = append(result, v6leases...) return result } -// Print information about the current leases -func (s *Server) printLeases() { - log.Tracef("Leases:") - for i, lease := range s.leases { - log.Tracef("Lease #%d: hwaddr %s, ip %s, expiry %s", - i, lease.HWAddr, lease.IP, lease.Expiry) - } -} - -// FindIPbyMAC finds an IP address by MAC address in the currently active DHCP leases -func (s *Server) FindIPbyMAC(mac net.HardwareAddr) net.IP { - now := time.Now().Unix() - s.leasesLock.RLock() - defer s.leasesLock.RUnlock() - for _, l := range s.leases { - if l.Expiry.Unix() > now && bytes.Equal(mac, l.HWAddr) { - return l.IP - } - } - return nil -} - // FindMACbyIP - find a MAC address by IP address in the currently active DHCP leases func (s *Server) FindMACbyIP(ip net.IP) net.HardwareAddr { - now := time.Now().Unix() - - s.leasesLock.RLock() - defer s.leasesLock.RUnlock() - - ip4 := ip.To4() - if ip4 == nil { - return nil + if ip.To4() != nil { + return s.srv4.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 + return s.srv6.FindMACbyIP(ip) } -// Reset internal state -func (s *Server) reset() { - s.leasesLock.Lock() - s.leases = nil - s.IPpool = make(map[[4]byte]net.HardwareAddr) - s.leasesLock.Unlock() +// AddStaticLease - add static v4 lease +func (s *Server) AddStaticLease(lease Lease) error { + return s.srv4.AddStaticLease(lease) } diff --git a/dhcpd/dhcpd_test.go b/dhcpd/dhcpd_test.go index b08ffb86..ab31c900 100644 --- a/dhcpd/dhcpd_test.go +++ b/dhcpd/dhcpd_test.go @@ -1,3 +1,5 @@ +// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris + package dhcpd import ( @@ -7,7 +9,6 @@ import ( "testing" "time" - "github.com/krolaw/dhcp4" "github.com/stretchr/testify/assert" ) @@ -17,234 +18,66 @@ func check(t *testing.T, result bool, msg string) { } } -// Tests performed: -// . 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") +func testNotify(flags uint32) { } // Leases database store/load func TestDB(t *testing.T) { - var s = Server{} + var err error + s := Server{} s.conf.DBFilePath = dbFilename - var p dhcp4.Packet - var hw1, hw2 net.HardwareAddr - var lease *Lease - 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}, + 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: testNotify, } + 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} - p.SetCHAddr(hw1) - lease, _ = s.reserveLease(p) - lease.Expiry = time.Unix(4000000001, 0) + l := Lease{} + l.IP = net.ParseIP("192.168.10.100").To4() + l.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:aa") + exp1 := time.Now().Add(time.Hour) + l.Expiry = exp1 + s.srv4.(*v4Server).addLease(&l) - hw2 = []byte{2, 2, 3, 4, 5, 6} - p.SetCHAddr(hw2) - lease, _ = s.reserveLease(p) - lease.Expiry = time.Unix(4000000002, 0) + l2 := Lease{} + l2.IP = net.ParseIP("192.168.10.101").To4() + l2.HWAddr, _ = net.ParseMAC("aa:aa:aa:aa:aa:bb") + s.srv4.AddStaticLease(l2) _ = os.Remove("leases.db") s.dbStore() - s.reset() + s.srv4.ResetLeases(nil) 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") - 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") + ll := s.srv4.GetLeases(LeasesAll) + + 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") } func TestIsValidSubnetMask(t *testing.T) { - if !isValidSubnetMask([]byte{255, 255, 255, 0}) { - t.Fatalf("isValidSubnetMask([]byte{255,255,255,0})") - } - if isValidSubnetMask([]byte{255, 255, 253, 0}) { - t.Fatalf("isValidSubnetMask([]byte{255,255,253,0})") - } - if isValidSubnetMask([]byte{0, 255, 255, 255}) { - t.Fatalf("isValidSubnetMask([]byte{255,255,253,0})") - } + assert.True(t, isValidSubnetMask([]byte{255, 255, 255, 0})) + assert.True(t, isValidSubnetMask([]byte{255, 255, 254, 0})) + assert.True(t, isValidSubnetMask([]byte{255, 255, 252, 0})) + assert.True(t, !isValidSubnetMask([]byte{255, 255, 253, 0})) + assert.True(t, !isValidSubnetMask([]byte{255, 255, 255, 1})) } func TestNormalizeLeases(t *testing.T) { diff --git a/dhcpd/filter_conn.go b/dhcpd/filter_conn.go deleted file mode 100644 index 70cfa939..00000000 --- a/dhcpd/filter_conn.go +++ /dev/null @@ -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() -} diff --git a/dhcpd/helpers.go b/dhcpd/helpers.go index 9553fca9..6c02110f 100644 --- a/dhcpd/helpers.go +++ b/dhcpd/helpers.go @@ -17,32 +17,24 @@ func isTimeout(err error) bool { return operr.Timeout() } -// return first IPv4 address of an interface, if there is any -func getIfaceIPv4(iface *net.Interface) *net.IPNet { - ifaceAddrs, err := iface.Addrs() +// Get IPv4 address list +func getIfaceIPv4(iface net.Interface) []net.IP { + addrs, err := iface.Addrs() if err != nil { - panic(err) + return nil } - for _, addr := range ifaceAddrs { - ipnet, ok := addr.(*net.IPNet) + var res []net.IP + for _, a := range addrs { + ipnet, ok := a.(*net.IPNet) if !ok { - // not an IPNet, should not happen - log.Fatalf("SHOULD NOT HAPPEN: got iface.Addrs() element %s that is not net.IPNet", addr) - } - - if ipnet.IP.To4() == nil { - log.Tracef("Got IP that is not IPv4: %v", ipnet.IP) continue } - - log.Tracef("Got IP that is IPv4: %v", ipnet.IP) - return &net.IPNet{ - IP: ipnet.IP.To4(), - Mask: ipnet.Mask, + if ipnet.IP.To4() != nil { + res = append(res, ipnet.IP.To4()) } } - return nil + return res } func wrapErrPrint(err error, message string, args ...interface{}) error { diff --git a/dhcpd/network_utils_test.go b/dhcpd/network_utils_test.go index 2957a411..fb6ef11e 100644 --- a/dhcpd/network_utils_test.go +++ b/dhcpd/network_utils_test.go @@ -1,3 +1,5 @@ +// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris + package dhcpd import ( diff --git a/dhcpd/server.go b/dhcpd/server.go new file mode 100644 index 00000000..5aea9497 --- /dev/null +++ b/dhcpd/server.go @@ -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) +} diff --git a/dhcpd/standalone/main.go b/dhcpd/standalone/main.go deleted file mode 100644 index a9aab9ee..00000000 --- a/dhcpd/standalone/main.go +++ /dev/null @@ -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 ", 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 -} diff --git a/dhcpd/v4.go b/dhcpd/v4.go new file mode 100644 index 00000000..ac0f9808 --- /dev/null +++ b/dhcpd/v4.go @@ -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(: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(: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 +} diff --git a/dhcpd/v46_windows.go b/dhcpd/v46_windows.go new file mode 100644 index 00000000..152899b9 --- /dev/null +++ b/dhcpd/v46_windows.go @@ -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 +} diff --git a/dhcpd/v4_test.go b/dhcpd/v4_test.go new file mode 100644 index 00000000..208dd8fb --- /dev/null +++ b/dhcpd/v4_test.go @@ -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())) +} diff --git a/dhcpd/v6.go b/dhcpd/v6.go new file mode 100644 index 00000000..8f042e25 --- /dev/null +++ b/dhcpd/v6.go @@ -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 +} diff --git a/dhcpd/v6_test.go b/dhcpd/v6_test.go new file mode 100644 index 00000000..7d7dd678 --- /dev/null +++ b/dhcpd/v6_test.go @@ -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"))) +} diff --git a/dnsforward/dnsforward.go b/dnsforward/dnsforward.go index e73824b0..8d5ec767 100644 --- a/dnsforward/dnsforward.go +++ b/dnsforward/dnsforward.go @@ -44,10 +44,10 @@ var webRegistered bool // // The zero Server is empty and ready for use. type Server struct { - dnsProxy *proxy.Proxy // DNS proxy instance - dnsFilter *dnsfilter.Dnsfilter // DNS filter instance - dhcpServer *dhcpd.Server // DHCP server instance (optional) - queryLog querylog.QueryLog // Query log instance + dnsProxy *proxy.Proxy // DNS proxy instance + dnsFilter *dnsfilter.Dnsfilter // DNS filter instance + dhcpServer dhcpd.ServerInterface // DHCP server instance (optional) + queryLog querylog.QueryLog // Query log instance stats stats.Stats access *accessCtx @@ -72,7 +72,7 @@ type DNSCreateParams struct { DNSFilter *dnsfilter.Dnsfilter Stats stats.Stats QueryLog querylog.QueryLog - DHCPServer *dhcpd.Server + DHCPServer dhcpd.ServerInterface } // NewServer creates a new instance of the dnsforward.Server diff --git a/dnsforward/dnsforward_test.go b/dnsforward/dnsforward_test.go index 051178cd..a3eb94f1 100644 --- a/dnsforward/dnsforward_test.go +++ b/dnsforward/dnsforward_test.go @@ -1018,9 +1018,22 @@ func TestMatchDNSName(t *testing.T) { 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) { - dhcp := &dhcpd.Server{} - dhcp.IPpool = make(map[[4]byte]net.HardwareAddr) + dhcp := &testDHCP{} c := dnsfilter.Config{} f := dnsfilter.New(&c, nil) @@ -1033,12 +1046,6 @@ func TestPTRResponse(t *testing.T) { assert.True(t, err == nil) 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) req := createTestMessage("1.0.0.127.in-addr.arpa.") req.Question[0].Qtype = dns.TypePTR diff --git a/go.mod b/go.mod index a9f5a605..fc774a4f 100644 --- a/go.mod +++ b/go.mod @@ -9,13 +9,17 @@ require ( github.com/NYTimes/gziphandler v1.1.1 github.com/fsnotify/fsnotify v1.4.7 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/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/pkg/errors v0.9.1 github.com/sparrc/go-ping v0.0.0-20190613174326-4e5b6552494c 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 golang.org/x/crypto v0.0.0-20200403201458-baeed622b8d8 golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e diff --git a/go.sum b/go.sum index 4271f61e..2df1d656 100644 --- a/go.sum +++ b/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/v2 v2.5.1 h1:TFOeY2VoGamPjQLiNDT3mn//ytzk236VMO2j7iHxJR4= 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/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/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/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 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.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 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/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/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= 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/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.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= 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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 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/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/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= @@ -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/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-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-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 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-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-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-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-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/home/clients.go b/home/clients.go index 369be2bc..1e6ae413 100644 --- a/home/clients.go +++ b/home/clients.go @@ -86,6 +86,7 @@ type clientsContainer struct { } // Init initializes clients container +// dhcpServer: optional // Note: this function must be called only once func (clients *clientsContainer) Init(objects []clientObject, dhcpServer *dhcpd.Server, autoHosts *util.AutoHosts) { if clients.list != nil { @@ -106,7 +107,9 @@ func (clients *clientsContainer) Init(objects []clientObject, dhcpServer *dhcpd. if !clients.testing { clients.addFromDHCP() - clients.dhcpServer.SetOnLeaseChanged(clients.onDHCPLeaseChanged) + if clients.dhcpServer != nil { + clients.dhcpServer.SetOnLeaseChanged(clients.onDHCPLeaseChanged) + } clients.autoHosts.SetOnChanged(clients.onHostsChanged) } } diff --git a/home/config.go b/home/config.go index 42e08cc2..d0fc6396 100644 --- a/home/config.go +++ b/home/config.go @@ -127,10 +127,6 @@ var config = configuration{ PortHTTPS: 443, PortDNSOverTLS: 853, // needs to be passed through to dnsproxy }, - DHCP: dhcpd.ServerConfig{ - LeaseDuration: 86400, - ICMPTimeout: 1000, - }, logSettings: logSettings{ LogCompress: false, LogLocalTime: false, @@ -156,6 +152,10 @@ func initConfig() { config.DNS.DnsfilterConf.ParentalCacheSize = 1 * 1024 * 1024 config.DNS.DnsfilterConf.CacheTime = 30 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 diff --git a/home/control.go b/home/control.go index 744fc327..e8b2fa8a 100644 --- a/home/control.go +++ b/home/control.go @@ -56,6 +56,7 @@ func handleStatus(w http.ResponseWriter, r *http.Request) { "protection_enabled": c.ProtectionEnabled, } + data["dhcp_available"] = (Context.dhcpServer != nil) jsonVal, err := json.Marshal(data) if err != nil { diff --git a/home/dhcp.go b/home/dhcp.go deleted file mode 100644 index 799333b0..00000000 --- a/home/dhcp.go +++ /dev/null @@ -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 -} diff --git a/home/home.go b/home/home.go index e297173c..4482b6b2 100644 --- a/home/home.go +++ b/home/home.go @@ -219,10 +219,8 @@ func run(args options) { config.DHCP.WorkDir = Context.workDir config.DHCP.HTTPRegister = httpRegister config.DHCP.ConfigModified = onConfigModified - Context.dhcpServer = dhcpd.Create(config.DHCP) - if Context.dhcpServer == nil { - log.Error("Failed to initialize DHCP server, exiting") - os.Exit(1) + if runtime.GOOS != "windows" { + Context.dhcpServer = dhcpd.Create(config.DHCP) } Context.autoHosts.Init("") @@ -317,9 +315,8 @@ func run(args options) { } }() - err = startDHCPServer() - if err != nil { - log.Fatal(err) + if Context.dhcpServer != nil { + _ = Context.dhcpServer.Start() } } @@ -505,9 +502,9 @@ func cleanup() { if err != nil { log.Error("Couldn't stop DNS server: %s", err) } - err = stopDHCPServer() - if err != nil { - log.Error("Couldn't stop DHCP server: %s", err) + + if Context.dhcpServer != nil { + Context.dhcpServer.Stop() } Context.autoHosts.Close() diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 22579caa..7b29f96c 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -348,8 +348,6 @@ paths: application/json: schema: $ref: "#/components/schemas/DhcpConfig" - description: DHCP configuration JSON - required: true responses: "200": description: OK @@ -965,6 +963,8 @@ components: maximum: 65535 protection_enabled: type: boolean + dhcp_available: + type: boolean querylog_enabled: type: boolean running: @@ -999,6 +999,8 @@ components: - tls://1.0.0.1 protection_enabled: type: boolean + dhcp_available: + type: boolean ratelimit: type: integer blocking_mode: @@ -1259,17 +1261,18 @@ components: description: Time period to keep data (1 | 7 | 30 | 90) DhcpConfig: type: object - description: Built-in DHCP server configuration - required: - - enabled - - gateway_ip - - subnet_mask - - range_start - - range_end - - lease_duration properties: enabled: type: boolean + interface_name: + type: string + v4: + $ref: "#/components/schemas/DhcpConfigV4" + v6: + $ref: "#/components/schemas/DhcpConfigV6" + DhcpConfigV4: + type: object + properties: gateway_ip: type: string example: 192.168.1.1 @@ -1283,8 +1286,14 @@ components: type: string example: 192.168.10.50 lease_duration: + type: integer + DhcpConfigV6: + type: object + properties: + range_start: type: string - example: 12h + lease_duration: + type: integer DhcpLease: type: object description: DHCP lease information @@ -1305,8 +1314,7 @@ components: example: dell expires: type: string - format: date-time - example: 2017-07-21T17:32:28Z + example: "2017-07-21T17:32:28Z" DhcpStaticLease: type: object description: DHCP static lease information @@ -1332,8 +1340,14 @@ components: - config - leases properties: - config: - $ref: "#/components/schemas/DhcpConfig" + enabled: + type: boolean + interface_name: + type: string + v4: + $ref: "#/components/schemas/DhcpConfigV4" + v6: + $ref: "#/components/schemas/DhcpConfigV6" leases: type: array items: