package dhcpd import ( "bytes" "fmt" "net" "sync" "time" "github.com/hmage/golibs/log" "github.com/krolaw/dhcp4" ) const defaultDiscoverTime = time.Second * 3 // field ordering is important -- yaml fields will mirror ordering from here type Lease struct { HWAddr net.HardwareAddr `json:"mac" yaml:"hwaddr"` IP net.IP `json:"ip"` Hostname string `json:"hostname"` Expiry time.Time `json:"expires"` } // 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 uint `json:"lease_duration" yaml:"lease_duration"` // in seconds } type Server struct { conn *filterConn // listening UDP socket ipnet *net.IPNet // if interface name changes, this needs to be reset // leases leases []*Lease 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 ServerConfig sync.RWMutex } // 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 { if config != nil { s.ServerConfig = *config } iface, err := net.InterfaceByName(s.InterfaceName) if err != nil { s.closeConn() // in case it was already started return wrapErrPrint(err, "Couldn't find interface by name %s", s.InterfaceName) } // get ipv4 address of an interface s.ipnet = getIfaceIPv4(iface) if s.ipnet == nil { s.closeConn() // in case it was already started return wrapErrPrint(err, "Couldn't find IPv4 address of interface %s %+v", s.InterfaceName, iface) } if s.LeaseDuration == 0 { s.leaseTime = time.Hour * 2 s.LeaseDuration = uint(s.leaseTime.Seconds()) } else { s.leaseTime = time.Second * time.Duration(s.LeaseDuration) } s.leaseStart, err = parseIPv4(s.RangeStart) if err != nil { s.closeConn() // in case it was already started return wrapErrPrint(err, "Failed to parse range start address %s", s.RangeStart) } s.leaseStop, err = parseIPv4(s.RangeEnd) if err != nil { s.closeConn() // in case it was already started return wrapErrPrint(err, "Failed to parse range end address %s", s.RangeEnd) } subnet, err := parseIPv4(s.SubnetMask) if err != nil { s.closeConn() // in case it was already started return wrapErrPrint(err, "Failed to parse subnet mask %s", s.SubnetMask) } // if !bytes.Equal(subnet, s.ipnet.Mask) { // s.closeConn() // in case it was already started // 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(s.GatewayIP) if err != nil { s.closeConn() // in case it was already started return wrapErrPrint(err, "Failed to parse gateway IP %s", s.GatewayIP) } s.leaseOptions = dhcp4.Options{ dhcp4.OptionSubnetMask: subnet, dhcp4.OptionRouter: router, dhcp4.OptionDomainNameServer: s.ipnet.IP, } // TODO: don't close if interface and addresses are the same if s.conn != nil { s.closeConn() } c, err := newFilterConn(*iface, ":67") // it has to be bound to 0.0.0.0:67, otherwise it won't see DHCP discover/request packets if err != nil { return wrapErrPrint(err, "Couldn't start listening socket on 0.0.0.0:67") } s.conn = c go func() { // operate on c instead of c.conn because c.conn can change over time err := dhcp4.Serve(c, s) if err != nil { log.Printf("dhcp4.Serve() returned with error: %s", err) } c.Close() // in case Serve() exits for other reason than listening socket closure }() return nil } func (s *Server) Stop() error { if s.conn == nil { // nothing to do, return silently return nil } err := s.closeConn() if err != nil { return wrapErrPrint(err, "Couldn't close UDP listening socket") } 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 } 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) 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 log.Tracef("Lease not found for %s: creating new one", hwaddr) ip, err := s.findFreeIP(p, hwaddr) if err != nil { return nil, wrapErrPrint(err, "Couldn't find free IP for the lease %s", hwaddr.String()) } 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)} s.Lock() s.leases = append(s.leases, lease) s.Unlock() return lease, nil } func (s *Server) locateLease(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 } func (s *Server) findFreeIP(p dhcp4.Packet, hwaddr net.HardwareAddr) (net.IP, error) { // if IP pool is nil, lazy initialize it if s.IPpool == nil { s.IPpool = make(map[[4]byte]net.HardwareAddr) } // 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.getIPpool(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 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) getIPpool(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) } 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) } // spew.Dump(s.leases, s.IPpool) // log.Printf("Called with msgType = %v, options = %+v", msgType, options) // spew.Dump(p) // log.Printf("%14s %v", "p.Broadcast", p.Broadcast()) // false // log.Printf("%14s %v", "p.CHAddr", p.CHAddr()) // 2c:f0:a2:f2:31:00 // log.Printf("%14s %v", "p.CIAddr", p.CIAddr()) // 0.0.0.0 // log.Printf("%14s %v", "p.Cookie", p.Cookie()) // [99 130 83 99] // log.Printf("%14s %v", "p.File", p.File()) // [] // log.Printf("%14s %v", "p.Flags", p.Flags()) // [0 0] // log.Printf("%14s %v", "p.GIAddr", p.GIAddr()) // 0.0.0.0 // log.Printf("%14s %v", "p.HLen", p.HLen()) // 6 // log.Printf("%14s %v", "p.HType", p.HType()) // 1 // log.Printf("%14s %v", "p.Hops", p.Hops()) // 0 // log.Printf("%14s %v", "p.OpCode", p.OpCode()) // BootRequest // log.Printf("%14s %v", "p.Options", p.Options()) // [53 1 1 55 10 1 121 3 6 15 119 252 95 44 46 57 2 5 220 61 7 1 44 240 162 242 49 0 51 4 0 118 167 0 12 4 119 104 109 100 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] // log.Printf("%14s %v", "p.ParseOptions", p.ParseOptions()) // map[OptionParameterRequestList:[1 121 3 6 15 119 252 95 44 46] OptionDHCPMessageType:[1] OptionMaximumDHCPMessageSize:[5 220] OptionClientIdentifier:[1 44 240 162 242 49 0] OptionIPAddressLeaseTime:[0 118 167 0] OptionHostName:[119 104 109 100]] // log.Printf("%14s %v", "p.SIAddr", p.SIAddr()) // 0.0.0.0 // log.Printf("%14s %v", "p.SName", p.SName()) // [] // log.Printf("%14s %v", "p.Secs", p.Secs()) // [0 8] // log.Printf("%14s %v", "p.XId", p.XId()) // [211 184 20 44] // log.Printf("%14s %v", "p.YIAddr", p.YIAddr()) // 0.0.0.0 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 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 log.Tracef("Got from client: Request") if server, ok := options[dhcp4.OptionServerIdentifier]; ok && !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 = net.IP(p.CIAddr()) } if reqIP.To4() == nil { log.Tracef("Replying with NAK: request IP isn't 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") 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()) 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) case dhcp4.Decline: // Broadcast From Client - Sorry I can't use that IP log.Tracef("Got from client: Decline") case dhcp4.Release: // From Client, I don't need that IP anymore log.Tracef("Got from client: Release") 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 // 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") case dhcp4.ACK: // From Server, Yes you can have that IP log.Printf("SHOULD NOT HAPPEN -- FROM ANOTHER DHCP SERVER: ACK") case dhcp4.NAK: // From Server, No you cannot have that IP log.Printf("SHOULD NOT HAPPEN -- FROM ANOTHER DHCP SERVER: NAK") default: log.Printf("Unknown DHCP packet detected, ignoring: %v", msgType) return nil } return nil } func (s *Server) Leases() []*Lease { s.RLock() result := s.leases s.RUnlock() return result }