diff --git a/config.go b/config.go index b0da6532..1c12f1c6 100644 --- a/config.go +++ b/config.go @@ -10,6 +10,7 @@ import ( "github.com/AdguardTeam/AdGuardHome/dhcpd" "github.com/AdguardTeam/AdGuardHome/dnsfilter" "github.com/AdguardTeam/AdGuardHome/dnsforward" + "github.com/AdguardTeam/golibs/file" "github.com/AdguardTeam/golibs/log" yaml "gopkg.in/yaml.v2" ) @@ -217,7 +218,7 @@ func (c *configuration) write() error { log.Error("Couldn't generate YAML file: %s", err) return err } - err = safeWriteFile(configFile, yamlText) + err = file.SafeWrite(configFile, yamlText) if err != nil { log.Error("Couldn't save YAML config: %s", err) return err diff --git a/control.go b/control.go index 0cd9d504..0bd7a5d7 100644 --- a/control.go +++ b/control.go @@ -12,6 +12,7 @@ import ( "sort" "strconv" "strings" + "sync" "time" "github.com/AdguardTeam/AdGuardHome/dnsforward" @@ -36,6 +37,8 @@ var client = &http.Client{ Timeout: time.Second * 30, } +var controlLock sync.Mutex + // ---------------- // helper functions // ---------------- diff --git a/dhcp.go b/dhcp.go index fe780305..ac9c36c5 100644 --- a/dhcp.go +++ b/dhcp.go @@ -17,6 +17,7 @@ import ( var dhcpServer = dhcpd.Server{} func handleDHCPStatus(w http.ResponseWriter, r *http.Request) { + log.Tracef("%s %v", r.Method, r.URL) rawLeases := dhcpServer.Leases() leases := []map[string]string{} for i := range rawLeases { @@ -43,6 +44,7 @@ func handleDHCPStatus(w http.ResponseWriter, r *http.Request) { } func handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) { + log.Tracef("%s %v", r.Method, r.URL) newconfig := dhcpd.ServerConfig{} err := json.NewDecoder(r.Body).Decode(&newconfig) if err != nil { @@ -50,6 +52,11 @@ func handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) { return } + err = dhcpServer.Stop() + if err != nil { + log.Error("failed to stop the DHCP server: %s", err) + } + if newconfig.Enabled { err := dhcpServer.Start(&newconfig) if err != nil { @@ -57,17 +64,13 @@ func handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) { return } } - if !newconfig.Enabled { - err := dhcpServer.Stop() - if err != nil { - log.Error("failed to stop the DHCP server: %s", err) - } - } + config.DHCP = newconfig httpUpdateConfigReloadDNSReturnOK(w, r) } func handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) { + log.Tracef("%s %v", r.Method, r.URL) response := map[string]interface{}{} ifaces, err := getValidNetInterfaces() @@ -128,6 +131,7 @@ func handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) { } func handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Request) { + log.Tracef("%s %v", r.Method, r.URL) body, err := ioutil.ReadAll(r.Body) if err != nil { errorText := fmt.Sprintf("failed to read request body: %s", err) diff --git a/dhcpd/README.md b/dhcpd/README.md new file mode 100644 index 00000000..83e48183 --- /dev/null +++ b/dhcpd/README.md @@ -0,0 +1,53 @@ +# DHCP server + +Contents: +* [Test setup with Virtual Box](#vbox) + + +## Test setup with Virtual Box + +To set up a test environment for DHCP server you need: + +* Linux host machine +* Virtual Box +* Virtual machine (guest OS doesn't matter) + +### Configure client + +1. Install Virtual Box and run the following command to create a Host-Only network: + + $ VBoxManage hostonlyif create + + You can check its status by `ip a` command. + + You can also set up Host-Only network using Virtual Box menu: + + File -> Host Network Manager... + +2. Create your virtual machine and set up its network: + + VM Settings -> Network -> Host-only Adapter + +3. Start your VM, install an OS. Configure your network interface to use DHCP and the OS should ask for a IP address from our DHCP server. + +### Configure server + +1. Edit server configuration file 'AdGuardHome.yaml', for example: + + dhcp: + enabled: true + interface_name: vboxnet0 + gateway_ip: 192.168.56.1 + subnet_mask: 255.255.255.0 + range_start: 192.168.56.2 + range_end: 192.168.56.2 + lease_duration: 86400 + icmp_timeout_msec: 1000 + +2. Start the server + + ./AdGuardHome + + There should be a message in log which shows that DHCP server is ready: + + [info] DHCP: listening on 0.0.0.0:67 diff --git a/dhcpd/db.go b/dhcpd/db.go new file mode 100644 index 00000000..1caa7d81 --- /dev/null +++ b/dhcpd/db.go @@ -0,0 +1,98 @@ +// On-disk database for lease table + +package dhcpd + +import ( + "encoding/json" + "io/ioutil" + "net" + "os" + "time" + + "github.com/AdguardTeam/golibs/file" + "github.com/AdguardTeam/golibs/log" + "github.com/krolaw/dhcp4" +) + +const dbFilename = "leases.db" + +type leaseJSON struct { + HWAddr []byte `json:"mac"` + IP []byte `json:"ip"` + Hostname string `json:"host"` + Expiry int64 `json:"exp"` +} + +// Load lease table from DB +func (s *Server) dbLoad() { + data, err := ioutil.ReadFile(dbFilename) + if err != nil { + if !os.IsNotExist(err) { + log.Error("DHCP: can't read file %s: %v", dbFilename, err) + } + return + } + + obj := []leaseJSON{} + err = json.Unmarshal(data, &obj) + if err != nil { + log.Error("DHCP: invalid DB: %v", err) + return + } + + s.leases = nil + s.IPpool = make(map[[4]byte]net.HardwareAddr) + + numLeases := len(obj) + for i := range obj { + + if !dhcp4.IPInRange(s.leaseStart, s.leaseStop, obj[i].IP) { + log.Tracef("Skipping a lease with IP %s: not within current IP range", obj[i].IP) + continue + } + + lease := Lease{ + HWAddr: obj[i].HWAddr, + IP: obj[i].IP, + Hostname: obj[i].Hostname, + Expiry: time.Unix(obj[i].Expiry, 0), + } + + s.leases = append(s.leases, &lease) + + s.reserveIP(lease.IP, lease.HWAddr) + } + log.Info("DHCP: loaded %d leases from DB", numLeases) +} + +// Store lease table in DB +func (s *Server) dbStore() { + var leases []leaseJSON + + for i := range s.leases { + if s.leases[i].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(), + } + leases = append(leases, lease) + } + + data, err := json.Marshal(leases) + if err != nil { + log.Error("json.Marshal: %v", err) + return + } + + err = file.SafeWrite(dbFilename, data) + if err != nil { + log.Error("DHCP: can't store lease table on disk: %v filename: %s", + err, dbFilename) + return + } + log.Info("DHCP: stored %d leases in DB", len(leases)) +} diff --git a/dhcpd/dhcpd.go b/dhcpd/dhcpd.go index 05d753b8..b54195a8 100644 --- a/dhcpd/dhcpd.go +++ b/dhcpd/dhcpd.go @@ -4,11 +4,13 @@ import ( "bytes" "fmt" "net" + "strings" "sync" "time" "github.com/AdguardTeam/golibs/log" "github.com/krolaw/dhcp4" + "github.com/sparrc/go-ping" ) const defaultDiscoverTime = time.Second * 3 @@ -32,6 +34,10 @@ type ServerConfig struct { RangeStart string `json:"range_start" yaml:"range_start"` RangeEnd string `json:"range_end" yaml:"range_end"` LeaseDuration uint `json:"lease_duration" yaml:"lease_duration"` // in seconds + + // IP conflict detector: time (ms) to wait for ICMP reply. + // 0: disable + ICMPTimeout uint `json:"icmp_timeout_msec" yaml:"icmp_timeout_msec"` } // Server - the current state of the DHCP server @@ -40,6 +46,11 @@ type Server struct { 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 leaseStart net.IP // parsed from config RangeStart @@ -54,6 +65,16 @@ type Server struct { sync.RWMutex } +// 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()) +} + // Start will listen on port 67 and serve DHCP requests. // Even though config can be nil, it is not optional (at least for now), since there are no default values (yet). func (s *Server) Start(config *ServerConfig) error { @@ -64,6 +85,7 @@ func (s *Server) Start(config *ServerConfig) error { iface, err := net.InterfaceByName(s.InterfaceName) if err != nil { s.closeConn() // in case it was already started + printInterfaces() return wrapErrPrint(err, "Couldn't find interface by name %s", s.InterfaceName) } @@ -122,20 +144,27 @@ func (s *Server) Start(config *ServerConfig) error { s.closeConn() } + s.dbLoad() + 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 if err != nil { return wrapErrPrint(err, "Couldn't start listening socket on 0.0.0.0:67") } + 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 { + 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 @@ -147,11 +176,23 @@ func (s *Server) Stop() error { // 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() + + s.dbStore() return nil } @@ -165,6 +206,7 @@ func (s *Server) closeConn() error { 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 @@ -172,27 +214,39 @@ func (s *Server) reserveLease(p dhcp4.Packet) (*Lease, error) { hwaddrCOW := p.CHAddr() hwaddr := make(net.HardwareAddr, len(hwaddrCOW)) copy(hwaddr, hwaddrCOW) - foundLease := s.locateLease(p) - if foundLease != nil { - // log.Tracef("found lease for %s: %+v", hwaddr, foundLease) - return foundLease, nil - } // 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) ip, err := s.findFreeIP(hwaddr) if err != nil { - return nil, wrapErrPrint(err, "Couldn't find free IP for the lease %s", hwaddr.String()) + 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.Lock() + s.leases[i] = lease + s.Unlock() + + s.reserveIP(lease.IP, hwaddr) + return lease, nil } + log.Tracef("Assigning to %s IP address %s", hwaddr, ip.String()) - hostname := p.ParseOptions()[dhcp4.OptionHostName] - lease := &Lease{HWAddr: hwaddr, IP: ip, Hostname: string(hostname)} + lease.IP = ip s.Lock() s.leases = append(s.leases, lease) s.Unlock() return lease, nil } -func (s *Server) locateLease(p dhcp4.Packet) *Lease { +// 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)) { @@ -203,6 +257,17 @@ func (s *Server) locateLease(p dhcp4.Packet) *Lease { 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 { + return i + } + } + return -1 +} + func (s *Server) findFreeIP(hwaddr net.HardwareAddr) (net.IP, error) { // if IP pool is nil, lazy initialize it if s.IPpool == nil { @@ -213,13 +278,12 @@ func (s *Server) findFreeIP(hwaddr net.HardwareAddr) (net.IP, error) { var foundIP net.IP for i := 0; i < dhcp4.IPRange(s.leaseStart, s.leaseStop); i++ { newIP := dhcp4.IPAdd(s.leaseStart, i) - foundHWaddr := s.getIPpool(newIP) + 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) // } - log.Tracef("will try again") continue } foundIP = newIP @@ -236,7 +300,7 @@ func (s *Server) findFreeIP(hwaddr net.HardwareAddr) (net.IP, error) { return foundIP, nil } -func (s *Server) getIPpool(ip net.IP) net.HardwareAddr { +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] @@ -256,133 +320,223 @@ func (s *Server) unreserveIP(ip net.IP) { // ServeDHCP handles an incoming DHCP request func (s *Server) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options dhcp4.Options) dhcp4.Packet { - log.Tracef("Got %v message", msgType) - 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) - } - log.Tracef("IP pool:") - for ip, hwaddr := range s.IPpool { - log.Tracef("IP pool entry %s -> %s", net.IPv4(ip[0], ip[1], ip[2], ip[3]), hwaddr) - } + s.printLeases() switch msgType { case dhcp4.Discover: // Broadcast Packet From Client - Can I have an IP? - // find a lease, but don't update lease time - log.Tracef("Got from client: Discover") - lease, err := s.reserveLease(p) - if err != nil { - log.Tracef("Couldn't find free lease: %s", err) - // couldn't find lease, don't respond - return nil - } - reply := dhcp4.ReplyPacket(p, dhcp4.Offer, s.ipnet.IP, lease.IP, s.leaseTime, s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList])) - log.Tracef("Replying with offer: offered IP %v for %v with options %+v", lease.IP, s.leaseTime, reply.ParseOptions()) - return reply + 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 - log.Tracef("Got from client: Decline") + return s.handleDecline(p, options) case dhcp4.Release: // From Client, I don't need that IP anymore - log.Tracef("Got from client: Release") + return s.handleRelease(p, options) case dhcp4.Inform: // From Client, I have this IP and there's nothing you can do about it - log.Tracef("Got from client: Inform") - // do nothing + 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("SHOULD NOT HAPPEN -- FROM ANOTHER DHCP SERVER: Offer") + log.Printf("DHCP: received message from %s: Offer", p.CHAddr()) + case dhcp4.ACK: // From Server, Yes you can have that IP - log.Printf("SHOULD NOT HAPPEN -- FROM ANOTHER DHCP SERVER: ACK") + log.Printf("DHCP: received message from %s: ACK", p.CHAddr()) + case dhcp4.NAK: // From Server, No you cannot have that IP - log.Printf("SHOULD NOT HAPPEN -- FROM ANOTHER DHCP SERVER: NAK") + log.Printf("DHCP: received message from %s: NAK", p.CHAddr()) + default: - log.Printf("Unknown DHCP packet detected, ignoring: %v", msgType) + 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.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.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.reserveIP(lease.IP, hw) + s.Lock() + lease.HWAddr = hw + lease.Hostname = "" + lease.Expiry = time.Now().Add(s.leaseTime) + s.Unlock() +} + +// 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 { - log.Tracef("Got from client: Request") - if server, ok := options[dhcp4.OptionServerIdentifier]; ok && !net.IP(server).Equal(s.ipnet.IP) { + 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 } - reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress]) if reqIP == nil { reqIP = p.CIAddr() - } - if reqIP.To4() == nil { - log.Tracef("Replying with NAK: request IP isn't valid IPv4: %s", reqIP) + } 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) } - if reqIP.Equal(net.IPv4zero) { - log.Tracef("Replying with NAK: request IP is 0.0.0.0") + 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) } - log.Tracef("requested IP is %s", reqIP) - lease, err := s.reserveLease(p) - if err != nil { - log.Tracef("Couldn't find free lease: %s", err) - // couldn't find lease, don't respond - return nil - } - - if lease.IP.Equal(reqIP) { - // IP matches lease IP, nothing else to do - lease.Expiry = time.Now().Add(s.leaseTime) - log.Tracef("Replying with ACK: request IP matches lease IP, nothing else to do. IP %v for %v", lease.IP, p.CHAddr()) - return dhcp4.ReplyPacket(p, dhcp4.ACK, s.ipnet.IP, lease.IP, s.leaseTime, s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList])) - } - - // - // requested IP different from lease - // - - log.Tracef("lease IP is different from requested IP: %s vs %s", lease.IP, reqIP) - - hwaddr := s.getIPpool(reqIP) - if hwaddr == nil { - // not in pool, check if it's in DHCP range - if dhcp4.IPInRange(s.leaseStart, s.leaseStop, reqIP) { - // okay, we can give it to our client -- it's in our DHCP range and not taken, so let them use their IP - log.Tracef("Replying with ACK: request IP %v is not taken, so assigning lease IP %v to it, for %v", reqIP, lease.IP, p.CHAddr()) - s.unreserveIP(lease.IP) - lease.IP = reqIP - s.reserveIP(reqIP, p.CHAddr()) - lease.Expiry = time.Now().Add(s.leaseTime) - return dhcp4.ReplyPacket(p, dhcp4.ACK, s.ipnet.IP, lease.IP, s.leaseTime, s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList])) - } - } - - if hwaddr != nil && !bytes.Equal(hwaddr, lease.HWAddr) { - log.Printf("SHOULD NOT HAPPEN: IP pool hwaddr does not match lease hwaddr: %s vs %s", hwaddr, lease.HWAddr) - } - - // requsted IP is not sufficient, reply with NAK - if hwaddr != nil { - log.Tracef("Replying with NAK: request IP %s is taken, asked by %v", reqIP, p.CHAddr()) + 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) } - // requested IP is outside of DHCP range - log.Tracef("Replying with NAK: request IP %s is outside of DHCP range [%s, %s], asked by %v", reqIP, s.leaseStart, s.leaseStop, p.CHAddr()) - return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil) + lease.Expiry = time.Now().Add(s.leaseTime) + 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) } -// Leases returns the list of current DHCP leases +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 +} + +// Leases returns the list of current DHCP leases (thread-safe) func (s *Server) Leases() []*Lease { s.RLock() result := s.leases s.RUnlock() 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) + } +} + +// Reset internal state +func (s *Server) reset() { + s.Lock() + s.leases = nil + s.Unlock() + s.IPpool = make(map[[4]byte]net.HardwareAddr) +} diff --git a/dhcpd/dhcpd_test.go b/dhcpd/dhcpd_test.go new file mode 100644 index 00000000..366606d4 --- /dev/null +++ b/dhcpd/dhcpd_test.go @@ -0,0 +1,152 @@ +package dhcpd + +import ( + "bytes" + "net" + "os" + "testing" + "time" + + "github.com/krolaw/dhcp4" +) + +func check(t *testing.T, result bool, msg string) { + if !result { + t.Fatal(msg) + } +} + +// Tests performed: +// . Handle Discover message (lease reserve) +// . Handle Request message (lease commit) +func TestDHCP(t *testing.T) { + var s = Server{} + var p, p2 dhcp4.Packet + var hw net.HardwareAddr + var lease *Lease + var opt dhcp4.Options + + 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) + + // Reserve an IP + 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") + 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 + p.SetCHAddr([]byte{3, 2, 3, 4, 5, 6}) + lease, _ = s.reserveLease(p) + check(t, lease == nil, "lease == nil") + + // 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") + + s.reset() + misc(t, &s) +} + +// Small tests that don't require a static server's state +func misc(t *testing.T, s *Server) { + var p, p2 dhcp4.Packet + var hw net.HardwareAddr + var opt dhcp4.Options + + p = make(dhcp4.Packet, 241) + + // Try to commit a lease for an IP without prior Discover-Offer packets + hw = []byte{2, 2, 3, 4, 5, 6} + p.SetCHAddr(hw) + p.SetCIAddr([]byte{0, 0, 0, 0}) + opt = make(dhcp4.Options, 10) + opt[dhcp4.OptionRequestedIPAddress] = []byte{1, 1, 1, 1} + p2 = s.handleDHCP4Request(p, opt) + opt = p2.ParseOptions() + check(t, bytes.Equal(opt[dhcp4.OptionDHCPMessageType], []byte{byte(dhcp4.NAK)}), "dhcp4.NAK") +} + +// Leases database store/load +func TestDB(t *testing.T) { + var s = Server{} + var p dhcp4.Packet + var hw1, hw2 net.HardwareAddr + var lease *Lease + + 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) + + hw1 = []byte{1, 2, 3, 4, 5, 6} + p.SetCHAddr(hw1) + lease, _ = s.reserveLease(p) + lease.Expiry = time.Unix(4000000001, 0) + + hw2 = []byte{2, 2, 3, 4, 5, 6} + p.SetCHAddr(hw2) + lease, _ = s.reserveLease(p) + lease.Expiry = time.Unix(4000000002, 0) + + os.Remove("leases.db") + s.dbStore() + s.reset() + + 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") + + os.Remove("leases.db") +} diff --git a/filter.go b/filter.go index 280c403f..6b61d504 100644 --- a/filter.go +++ b/filter.go @@ -12,6 +12,7 @@ import ( "time" "github.com/AdguardTeam/AdGuardHome/dnsfilter" + "github.com/AdguardTeam/golibs/file" "github.com/AdguardTeam/golibs/log" ) @@ -220,7 +221,7 @@ func (filter *filter) save() error { log.Printf("Saving filter %d contents to: %s", filter.ID, filterFilePath) body := []byte(strings.Join(filter.Rules, "\n")) - err := safeWriteFile(filterFilePath, body) + err := file.SafeWrite(filterFilePath, body) // update LastUpdated field after saving the file filter.LastUpdated = filter.LastTimeUpdated() diff --git a/go.mod b/go.mod index 589d0982..62035c9c 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.12 require ( github.com/AdguardTeam/dnsproxy v0.11.2 - github.com/AdguardTeam/golibs v0.1.0 + github.com/AdguardTeam/golibs v0.1.1 github.com/StackExchange/wmi v0.0.0-20180725035823-b12b22c5341f // indirect github.com/bluele/gcache v0.0.0-20171010155617-472614239ac7 github.com/go-ole/go-ole v1.2.1 // indirect @@ -17,6 +17,7 @@ require ( github.com/miekg/dns v1.1.1 github.com/shirou/gopsutil v2.18.10+incompatible github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 // indirect + github.com/sparrc/go-ping v0.0.0-20181106165434-ef3ab45e41b0 github.com/stretchr/testify v1.2.2 go.uber.org/goleak v0.10.0 golang.org/x/net v0.0.0-20190119204137-ed066c81e75e diff --git a/go.sum b/go.sum index 6e4a79f1..c4bced69 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,8 @@ github.com/AdguardTeam/dnsproxy v0.11.2 h1:S/Ag2q9qoZsmW1fvMohPZP7/5amEtz8NmFCp8kxUalQ= github.com/AdguardTeam/dnsproxy v0.11.2/go.mod h1:EPp92b5cYR7HZpO+OQu6xC7AyhUoBaXW3sfa3exq/0I= -github.com/AdguardTeam/golibs v0.1.0 h1:Mo1QNKC8eSbqczhxfdBXYCrUMwvgCyCwZFyWv+2Gdng= github.com/AdguardTeam/golibs v0.1.0/go.mod h1:zhi6xGwK4cMpjDocybhhLgvcGkstiSIjlpKbvyxC5Yc= +github.com/AdguardTeam/golibs v0.1.1 h1:aepIN7yulf8I4Ub2c0cAaIizfSHPVXB2wrh8j4BJxl4= +github.com/AdguardTeam/golibs v0.1.1/go.mod h1:b0XkhgIcn2TxwX6C5AQMtpIFAgjPehNgxJErWkwA3ko= github.com/StackExchange/wmi v0.0.0-20180725035823-b12b22c5341f h1:5ZfJxyXo8KyX8DgGXC5B7ILL8y51fci/qYz2B4j8iLY= github.com/StackExchange/wmi v0.0.0-20180725035823-b12b22c5341f/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= @@ -55,6 +56,8 @@ github.com/shirou/gopsutil v2.18.10+incompatible h1:cy84jW6EVRPa5g9HAHrlbxMSIjBh github.com/shirou/gopsutil v2.18.10+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 h1:udFKJ0aHUL60LboW/A+DfgoHVedieIzIXE8uylPue0U= github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= +github.com/sparrc/go-ping v0.0.0-20181106165434-ef3ab45e41b0 h1:mu7brOsdaH5Dqf93vdch+mr/0To8Sgc+yInt/jE/RJM= +github.com/sparrc/go-ping v0.0.0-20181106165434-ef3ab45e41b0/go.mod h1:eMyUVp6f/5jnzM+3zahzl7q6UXLbgSc3MKg/+ow9QW0= github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= diff --git a/helpers.go b/helpers.go index 8e884d36..9d02ad3c 100644 --- a/helpers.go +++ b/helpers.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "net" "net/http" "net/url" @@ -19,26 +18,6 @@ import ( "github.com/joomcode/errorx" ) -// ---------------------------------- -// helper functions for working with files -// ---------------------------------- - -// Writes data first to a temporary file and then renames it to what's specified in path -func safeWriteFile(path string, data []byte) error { - dir := filepath.Dir(path) - err := os.MkdirAll(dir, 0755) - if err != nil { - return err - } - - tmpPath := path + ".tmp" - err = ioutil.WriteFile(tmpPath, data, 0644) - if err != nil { - return err - } - return os.Rename(tmpPath, path) -} - // ---------------------------------- // helper functions for HTTP handlers // ---------------------------------- @@ -48,6 +27,12 @@ func ensure(method string, handler func(http.ResponseWriter, *http.Request)) fun http.Error(w, "This request must be "+method, http.StatusMethodNotAllowed) return } + + if method == "POST" || method == "PUT" || method == "DELETE" { + controlLock.Lock() + defer controlLock.Unlock() + } + handler(w, r) } } diff --git a/upgrade.go b/upgrade.go index 0c9aeca7..e730d34b 100644 --- a/upgrade.go +++ b/upgrade.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" + "github.com/AdguardTeam/golibs/file" "github.com/AdguardTeam/golibs/log" yaml "gopkg.in/yaml.v2" ) @@ -86,7 +87,7 @@ func upgradeConfigSchema(oldVersion int, diskConfig *map[string]interface{}) err return err } - err = safeWriteFile(configFile, body) + err = file.SafeWrite(configFile, body) if err != nil { log.Printf("Couldn't save YAML config: %s", err) return err