package dhcp4client import ( "bytes" "fmt" "hash/fnv" "math/rand" "net" "sync" "syscall" "time" "github.com/d2g/dhcp4" ) const ( MaxDHCPLen = 576 ) type Client struct { hardwareAddr net.HardwareAddr //The HardwareAddr to send in the request. ignoreServers []net.IP //List of Servers to Ignore requests from. timeout time.Duration //Time before we timeout. broadcast bool //Set the Bcast flag in BOOTP Flags connection ConnectionInt //The Connection Method to use generateXID func([]byte) //Function Used to Generate a XID } //Abstracts the type of underlying socket used type ConnectionInt interface { Close() error Write(packet []byte) error ReadFrom() ([]byte, net.IP, error) SetReadTimeout(t time.Duration) error } func New(options ...func(*Client) error) (*Client, error) { c := Client{ timeout: time.Second * 10, broadcast: true, } err := c.SetOption(options...) if err != nil { return nil, err } if c.generateXID == nil { // https://tools.ietf.org/html/rfc2131#section-4.1 explains: // // A DHCP client MUST choose 'xid's in such a way as to minimize the chance // of using an 'xid' identical to one used by another client. // // Hence, seed a random number generator with the current time and hardware // address. h := fnv.New64() h.Write(c.hardwareAddr) seed := int64(h.Sum64()) + time.Now().Unix() rnd := rand.New(rand.NewSource(seed)) var rndMu sync.Mutex c.generateXID = func(b []byte) { rndMu.Lock() defer rndMu.Unlock() rnd.Read(b) } } //if connection hasn't been set as an option create the default. if c.connection == nil { conn, err := NewInetSock() if err != nil { return nil, err } c.connection = conn } return &c, nil } func (c *Client) SetOption(options ...func(*Client) error) error { for _, opt := range options { if err := opt(c); err != nil { return err } } return nil } func Timeout(t time.Duration) func(*Client) error { return func(c *Client) error { c.timeout = t return nil } } func IgnoreServers(s []net.IP) func(*Client) error { return func(c *Client) error { c.ignoreServers = s return nil } } func HardwareAddr(h net.HardwareAddr) func(*Client) error { return func(c *Client) error { c.hardwareAddr = h return nil } } func Broadcast(b bool) func(*Client) error { return func(c *Client) error { c.broadcast = b return nil } } func Connection(conn ConnectionInt) func(*Client) error { return func(c *Client) error { c.connection = conn return nil } } func GenerateXID(g func([]byte)) func(*Client) error { return func(c *Client) error { c.generateXID = g return nil } } //Close Connections func (c *Client) Close() error { if c.connection != nil { return c.connection.Close() } return nil } //Send the Discovery Packet to the Broadcast Channel func (c *Client) SendDiscoverPacket() (dhcp4.Packet, error) { discoveryPacket := c.DiscoverPacket() discoveryPacket.PadToMinSize() return discoveryPacket, c.SendPacket(discoveryPacket) } // TimeoutError records a timeout when waiting for a DHCP packet. type TimeoutError struct { Timeout time.Duration } func (te *TimeoutError) Error() string { return fmt.Sprintf("no DHCP packet received within %v", te.Timeout) } //Retreive Offer... //Wait for the offer for a specific Discovery Packet. func (c *Client) GetOffer(discoverPacket *dhcp4.Packet) (dhcp4.Packet, error) { start := time.Now() for { timeout := c.timeout - time.Since(start) if timeout < 0 { return dhcp4.Packet{}, &TimeoutError{Timeout: c.timeout} } c.connection.SetReadTimeout(timeout) readBuffer, source, err := c.connection.ReadFrom() if err != nil { if errno, ok := err.(syscall.Errno); ok && errno == syscall.EAGAIN { return dhcp4.Packet{}, &TimeoutError{Timeout: c.timeout} } return dhcp4.Packet{}, err } offerPacket := dhcp4.Packet(readBuffer) offerPacketOptions := offerPacket.ParseOptions() // Ignore Servers in my Ignore list for _, ignoreServer := range c.ignoreServers { if source.Equal(ignoreServer) { continue } if offerPacket.SIAddr().Equal(ignoreServer) { continue } } if len(offerPacketOptions[dhcp4.OptionDHCPMessageType]) < 1 || dhcp4.MessageType(offerPacketOptions[dhcp4.OptionDHCPMessageType][0]) != dhcp4.Offer || !bytes.Equal(discoverPacket.XId(), offerPacket.XId()) { continue } return offerPacket, nil } } //Send Request Based On the offer Received. func (c *Client) SendRequest(offerPacket *dhcp4.Packet) (dhcp4.Packet, error) { requestPacket := c.RequestPacket(offerPacket) requestPacket.PadToMinSize() return requestPacket, c.SendPacket(requestPacket) } //Retreive Acknowledgement //Wait for the offer for a specific Request Packet. func (c *Client) GetAcknowledgement(requestPacket *dhcp4.Packet) (dhcp4.Packet, error) { start := time.Now() for { timeout := c.timeout - time.Since(start) if timeout < 0 { return dhcp4.Packet{}, &TimeoutError{Timeout: c.timeout} } c.connection.SetReadTimeout(timeout) readBuffer, source, err := c.connection.ReadFrom() if err != nil { if errno, ok := err.(syscall.Errno); ok && errno == syscall.EAGAIN { return dhcp4.Packet{}, &TimeoutError{Timeout: c.timeout} } return dhcp4.Packet{}, err } acknowledgementPacket := dhcp4.Packet(readBuffer) acknowledgementPacketOptions := acknowledgementPacket.ParseOptions() // Ignore Servers in my Ignore list for _, ignoreServer := range c.ignoreServers { if source.Equal(ignoreServer) { continue } if acknowledgementPacket.SIAddr().Equal(ignoreServer) { continue } } if !bytes.Equal(requestPacket.XId(), acknowledgementPacket.XId()) || len(acknowledgementPacketOptions[dhcp4.OptionDHCPMessageType]) < 1 || (dhcp4.MessageType(acknowledgementPacketOptions[dhcp4.OptionDHCPMessageType][0]) != dhcp4.ACK && dhcp4.MessageType(acknowledgementPacketOptions[dhcp4.OptionDHCPMessageType][0]) != dhcp4.NAK) { continue } return acknowledgementPacket, nil } } //Send Decline to the received acknowledgement. func (c *Client) SendDecline(acknowledgementPacket *dhcp4.Packet) (dhcp4.Packet, error) { declinePacket := c.DeclinePacket(acknowledgementPacket) declinePacket.PadToMinSize() return declinePacket, c.SendPacket(declinePacket) } //Send a DHCP Packet. func (c *Client) SendPacket(packet dhcp4.Packet) error { return c.connection.Write(packet) } //Create Discover Packet func (c *Client) DiscoverPacket() dhcp4.Packet { messageid := make([]byte, 4) c.generateXID(messageid) packet := dhcp4.NewPacket(dhcp4.BootRequest) packet.SetCHAddr(c.hardwareAddr) packet.SetXId(messageid) packet.SetBroadcast(c.broadcast) packet.AddOption(dhcp4.OptionDHCPMessageType, []byte{byte(dhcp4.Discover)}) //packet.PadToMinSize() return packet } //Create Request Packet func (c *Client) RequestPacket(offerPacket *dhcp4.Packet) dhcp4.Packet { offerOptions := offerPacket.ParseOptions() packet := dhcp4.NewPacket(dhcp4.BootRequest) packet.SetCHAddr(c.hardwareAddr) packet.SetXId(offerPacket.XId()) packet.SetCIAddr(offerPacket.CIAddr()) packet.SetSIAddr(offerPacket.SIAddr()) packet.SetBroadcast(c.broadcast) packet.AddOption(dhcp4.OptionDHCPMessageType, []byte{byte(dhcp4.Request)}) packet.AddOption(dhcp4.OptionRequestedIPAddress, (offerPacket.YIAddr()).To4()) packet.AddOption(dhcp4.OptionServerIdentifier, offerOptions[dhcp4.OptionServerIdentifier]) return packet } //Create Request Packet For a Renew func (c *Client) RenewalRequestPacket(acknowledgement *dhcp4.Packet) dhcp4.Packet { messageid := make([]byte, 4) c.generateXID(messageid) acknowledgementOptions := acknowledgement.ParseOptions() packet := dhcp4.NewPacket(dhcp4.BootRequest) packet.SetCHAddr(acknowledgement.CHAddr()) packet.SetXId(messageid) packet.SetCIAddr(acknowledgement.YIAddr()) packet.SetSIAddr(acknowledgement.SIAddr()) packet.SetBroadcast(c.broadcast) packet.AddOption(dhcp4.OptionDHCPMessageType, []byte{byte(dhcp4.Request)}) packet.AddOption(dhcp4.OptionRequestedIPAddress, (acknowledgement.YIAddr()).To4()) packet.AddOption(dhcp4.OptionServerIdentifier, acknowledgementOptions[dhcp4.OptionServerIdentifier]) return packet } //Create Release Packet For a Release func (c *Client) ReleasePacket(acknowledgement *dhcp4.Packet) dhcp4.Packet { messageid := make([]byte, 4) c.generateXID(messageid) acknowledgementOptions := acknowledgement.ParseOptions() packet := dhcp4.NewPacket(dhcp4.BootRequest) packet.SetCHAddr(acknowledgement.CHAddr()) packet.SetXId(messageid) packet.SetCIAddr(acknowledgement.YIAddr()) packet.AddOption(dhcp4.OptionDHCPMessageType, []byte{byte(dhcp4.Release)}) packet.AddOption(dhcp4.OptionServerIdentifier, acknowledgementOptions[dhcp4.OptionServerIdentifier]) return packet } //Create Decline Packet func (c *Client) DeclinePacket(acknowledgement *dhcp4.Packet) dhcp4.Packet { messageid := make([]byte, 4) c.generateXID(messageid) acknowledgementOptions := acknowledgement.ParseOptions() packet := dhcp4.NewPacket(dhcp4.BootRequest) packet.SetCHAddr(acknowledgement.CHAddr()) packet.SetXId(messageid) packet.AddOption(dhcp4.OptionDHCPMessageType, []byte{byte(dhcp4.Decline)}) packet.AddOption(dhcp4.OptionRequestedIPAddress, (acknowledgement.YIAddr()).To4()) packet.AddOption(dhcp4.OptionServerIdentifier, acknowledgementOptions[dhcp4.OptionServerIdentifier]) return packet } //Lets do a Full DHCP Request. func (c *Client) Request() (bool, dhcp4.Packet, error) { discoveryPacket, err := c.SendDiscoverPacket() if err != nil { return false, discoveryPacket, err } offerPacket, err := c.GetOffer(&discoveryPacket) if err != nil { return false, offerPacket, err } requestPacket, err := c.SendRequest(&offerPacket) if err != nil { return false, requestPacket, err } acknowledgement, err := c.GetAcknowledgement(&requestPacket) if err != nil { return false, acknowledgement, err } acknowledgementOptions := acknowledgement.ParseOptions() if dhcp4.MessageType(acknowledgementOptions[dhcp4.OptionDHCPMessageType][0]) != dhcp4.ACK { return false, acknowledgement, nil } return true, acknowledgement, nil } //Renew a lease backed on the Acknowledgement Packet. //Returns Sucessfull, The AcknoledgementPacket, Any Errors func (c *Client) Renew(acknowledgement dhcp4.Packet) (bool, dhcp4.Packet, error) { renewRequest := c.RenewalRequestPacket(&acknowledgement) renewRequest.PadToMinSize() err := c.SendPacket(renewRequest) if err != nil { return false, renewRequest, err } newAcknowledgement, err := c.GetAcknowledgement(&renewRequest) if err != nil { return false, newAcknowledgement, err } newAcknowledgementOptions := newAcknowledgement.ParseOptions() if dhcp4.MessageType(newAcknowledgementOptions[dhcp4.OptionDHCPMessageType][0]) != dhcp4.ACK { return false, newAcknowledgement, nil } return true, newAcknowledgement, nil } //Release a lease backed on the Acknowledgement Packet. //Returns Any Errors func (c *Client) Release(acknowledgement dhcp4.Packet) error { release := c.ReleasePacket(&acknowledgement) release.PadToMinSize() return c.SendPacket(release) }