2019-06-26 02:07:24 -07:00

591 lines
16 KiB
Go

package dhcp4server
import (
"bytes"
"errors"
"log"
"net"
"sync/atomic"
"time"
"github.com/d2g/dhcp4"
"github.com/d2g/dhcp4server/leasepool"
"golang.org/x/net/ipv4"
)
/*
* The DHCP Server Structure
*/
type Server struct {
//Configuration Options
ip net.IP //The IP Address We Tell Clients The Server Is On.
defaultGateway net.IP //The Default Gateway Address
dnsServers []net.IP //DNS Servers
subnetMask net.IP //ie. 255.255.255.0
leaseDuration time.Duration //Number of Seconds
ignoreIPs []net.IP //Slice of IP's that should be ignored by the Server.
ignoreHardwareAddress []net.HardwareAddr //Slice of Hardware Addresses we should ignore.
//Local Address
laddr net.UDPAddr
//Remote address
raddr net.UDPAddr
//LeasePool
leasePool leasepool.LeasePool //Lease Pool Manager
//Used to Gracefully Close the Server
shutdown uint32
//Listeners & Response Connection.
connection *ipv4.PacketConn
}
// Create A New Server
func New(ip net.IP, l leasepool.LeasePool, options ...func(*Server) error) (*Server, error) {
s := Server{
ip: ip,
defaultGateway: ip,
dnsServers: []net.IP{net.IPv4(208, 67, 222, 222), net.IPv4(208, 67, 220, 220)}, //OPENDNS
subnetMask: net.IPv4(255, 255, 255, 0),
leaseDuration: 24 * time.Hour,
leasePool: l,
laddr: net.UDPAddr{IP: net.IPv4(0, 0, 0, 0), Port: 67},
raddr: net.UDPAddr{IP: net.IPv4bcast, Port: 68},
}
err := s.setOptions(options...)
if err != nil {
return &s, err
}
return &s, err
}
func (s *Server) setOptions(options ...func(*Server) error) error {
for _, opt := range options {
if err := opt(s); err != nil {
return err
}
}
return nil
}
// Set the Server IP
func IP(i net.IP) func(*Server) error {
return func(s *Server) error {
s.ip = i
return nil
}
return nil
}
// Set the Default Gateway Address.
func DefaultGateway(r net.IP) func(*Server) error {
return func(s *Server) error {
s.defaultGateway = r
return nil
}
}
// Set the DNS servers.
func DNSServers(dnss []net.IP) func(*Server) error {
return func(s *Server) error {
s.dnsServers = dnss
return nil
}
}
// Set the Subnet Mask
func SubnetMask(m net.IP) func(*Server) error {
return func(s *Server) error {
s.subnetMask = m
return nil
}
}
// Set Lease Duration
func LeaseDuration(d time.Duration) func(*Server) error {
return func(s *Server) error {
s.leaseDuration = d
return nil
}
}
// Set Ignore IPs
func IgnoreIPs(ips []net.IP) func(*Server) error {
return func(s *Server) error {
s.ignoreIPs = ips
return nil
}
}
// Set Ignore Hardware Addresses
func IgnoreHardwareAddresses(h []net.HardwareAddr) func(*Server) error {
return func(s *Server) error {
s.ignoreHardwareAddress = h
return nil
}
}
// Set LeasePool
func LeasePool(p leasepool.LeasePool) func(*Server) error {
return func(s *Server) error {
s.leasePool = p
return nil
}
}
// Set The Local Address
func SetLocalAddr(a net.UDPAddr) func(*Server) error {
return func(s *Server) error {
s.laddr = a
return nil
}
}
// Set The Remote Address
func SetRemoteAddr(a net.UDPAddr) func(*Server) error {
return func(s *Server) error {
s.raddr = a
return nil
}
}
/*
* Start The DHCP Server
*/
func (s *Server) ListenAndServe() error {
var err error
connection, err := net.ListenPacket("udp4", s.laddr.String())
if err != nil {
log.Printf("Debug: Error Returned From ListenPacket On \"%s\" Because of \"%s\"\n", s.laddr.String(), err.Error())
return err
}
s.connection = ipv4.NewPacketConn(connection)
defer s.connection.Close()
//We Currently Don't Use this Feature Which is the only bit that is Linux Only.
//if err := s.connection.SetControlMessage(ipv4.FlagInterface, true); err != nil {
// return err
//}
log.Println("Trace: DHCP Server Listening.")
for {
ListenForDHCPPackets:
if s.shouldShutdown() {
return nil
}
//Make Our Buffer (Max Buffer is 574) "I believe this 576 size comes from RFC 791" - Random Mailing list quote of the day.
buffer := make([]byte, 576)
//Set Read Deadline
s.connection.SetReadDeadline(time.Now().Add(time.Second))
// Read Packet
n, control_message, source, err := s.connection.ReadFrom(buffer)
if err != nil {
switch v := err.(type) {
case *net.OpError:
// If we've been signaled to shut down, ignore
// the "use of closed network connection" error
// since the connection was closed by the
// shutdown request
if s.shouldShutdown() {
return nil
}
if v.Timeout() {
goto ListenForDHCPPackets
}
case *net.AddrError:
if v.Timeout() {
goto ListenForDHCPPackets
}
case *net.UnknownNetworkError:
if v.Timeout() {
goto ListenForDHCPPackets
}
}
log.Printf("Debug: Unexpect Error from Connection Read From: %v\n", err)
return err
}
//We seem to have an issue with undersized packets?
if n < 240 {
log.Printf("Error: Invalid Packet Size \"%d\" Received:%v\n", n, buffer[:n])
continue
}
//We should ignore some requests
//It shouldn't be possible to ignore IP's because they shouldn't have them as we're the DHCP server.
//However, they can have i.e. if you're the client & server :S.
for _, ipToIgnore := range s.ignoreIPs {
if ipToIgnore.Equal(source.(*net.UDPAddr).IP) {
log.Println("Debug: Ignoring DHCP Request From IP:" + ipToIgnore.String())
continue
}
}
packet := dhcp4.Packet(buffer[:n])
//We can ignore hardware addresses.
//Usefull for ignoring a range of hardware addresses
for _, hardwareAddressToIgnore := range s.ignoreHardwareAddress {
if bytes.Equal(hardwareAddressToIgnore, packet.CHAddr()) {
log.Println("Debug: Ignoring DHCP Request From Hardware Address:" + hardwareAddressToIgnore.String())
continue
}
}
log.Printf("Trace: Packet Received ID:%v\n", packet.XId())
log.Printf("Trace: Packet Options:%v\n", packet.ParseOptions())
log.Printf("Trace: Packet Client IP : %v\n", packet.CIAddr().String())
log.Printf("Trace: Packet Your IP : %v\n", packet.YIAddr().String())
log.Printf("Trace: Packet Server IP : %v\n", packet.SIAddr().String())
log.Printf("Trace: Packet Gateway IP: %v\n", packet.GIAddr().String())
log.Printf("Trace: Packet Client Mac: %v\n", packet.CHAddr().String())
//We need to stop butting in with other servers.
if packet.SIAddr().Equal(net.IPv4(0, 0, 0, 0)) || packet.SIAddr().Equal(net.IP{}) || packet.SIAddr().Equal(s.ip) {
returnPacket, err := s.ServeDHCP(packet)
if err != nil {
log.Println("Debug: Error Serving DHCP:" + err.Error())
return err
}
if len(returnPacket) > 0 {
log.Printf("Trace: Packet Returned ID:%v\n", returnPacket.XId())
log.Printf("Trace: Packet Options:%v\n", returnPacket.ParseOptions())
log.Printf("Trace: Packet Client IP : %v\n", returnPacket.CIAddr().String())
log.Printf("Trace: Packet Your IP : %v\n", returnPacket.YIAddr().String())
log.Printf("Trace: Packet Server IP : %v\n", returnPacket.SIAddr().String())
log.Printf("Trace: Packet Gateway IP: %v\n", returnPacket.GIAddr().String())
log.Printf("Trace: Packet Client Mac: %v\n", returnPacket.CHAddr().String())
_, err = s.connection.WriteTo(returnPacket, control_message, &s.raddr)
if err != nil {
log.Println("Debug: Error Writing:" + err.Error())
return err
}
}
}
}
}
func getClientID(packetOptions dhcp4.Options) []byte {
if clientID, ok := packetOptions[dhcp4.OptionClientIdentifier]; ok {
return clientID
}
return nil
}
func (s *Server) ServeDHCP(packet dhcp4.Packet) (dhcp4.Packet, error) {
packetOptions := packet.ParseOptions()
switch dhcp4.MessageType(packetOptions[dhcp4.OptionDHCPMessageType][0]) {
case dhcp4.Discover:
//Discover Received from client
//Lets get the lease we're going to send them
found, lease, err := s.GetLease(packet)
if err != nil {
return dhcp4.Packet{}, err
}
if !found {
log.Println("Warning: It Looks Like Our Leases Are Depleted...")
return dhcp4.Packet{}, nil
}
offerPacket := s.OfferPacket(packet)
offerPacket.SetYIAddr(lease.IP)
//Sort out the packet options
offerPacket.PadToMinSize()
lease.Status = leasepool.Reserved
lease.MACAddress = packet.CHAddr()
lease.ClientID = getClientID(packetOptions)
//If the lease expires within the next 5 Mins increase the lease expiary (Giving the Client 5 mins to complete)
if lease.Expiry.Before(time.Now().Add(time.Minute * 5)) {
lease.Expiry = time.Now().Add(time.Minute * 5)
}
if packetOptions[dhcp4.OptionHostName] != nil && string(packetOptions[dhcp4.OptionHostName]) != "" {
lease.Hostname = string(packetOptions[dhcp4.OptionHostName])
}
updated, err := s.leasePool.UpdateLease(lease)
if err != nil {
return dhcp4.Packet{}, err
}
if !updated {
//Unable to reserve lease (It's now active else where maybe?)
return dhcp4.Packet{}, errors.New("Unable to Reserve Lease:" + lease.IP.String())
}
return offerPacket, nil
case dhcp4.Request:
//Request Received from client
//Lets get the lease we're going to send them
found, lease, err := s.GetLease(packet)
if err != nil {
return dhcp4.Packet{}, err
}
if !found {
log.Println("Warning: It Looks Like Our Leases Are Depleted...")
return dhcp4.Packet{}, nil
}
//If the lease is not the one requested We should send a NAK..
if len(packetOptions) > 0 && !net.IP(packetOptions[dhcp4.OptionRequestedIPAddress]).Equal(lease.IP) {
//NAK
declinePacket := s.DeclinePacket(packet)
declinePacket.PadToMinSize()
return declinePacket, nil
} else {
lease.Status = leasepool.Active
lease.MACAddress = packet.CHAddr()
lease.ClientID = getClientID(packetOptions)
lease.Expiry = time.Now().Add(s.leaseDuration)
if packetOptions[dhcp4.OptionHostName] != nil && string(packetOptions[dhcp4.OptionHostName]) != "" {
lease.Hostname = string(packetOptions[dhcp4.OptionHostName])
}
updated, err := s.leasePool.UpdateLease(lease)
if err != nil {
return dhcp4.Packet{}, err
}
if updated {
//ACK
acknowledgementPacket := s.AcknowledgementPacket(packet)
acknowledgementPacket.SetYIAddr(lease.IP)
//Lease time.
acknowledgementPacket.AddOption(dhcp4.OptionIPAddressLeaseTime, dhcp4.OptionsLeaseTime(lease.Expiry.Sub(time.Now())))
acknowledgementPacket.PadToMinSize()
return acknowledgementPacket, nil
} else {
//NAK
declinePacket := s.DeclinePacket(packet)
declinePacket.PadToMinSize()
return declinePacket, nil
}
}
case dhcp4.Decline:
//Decline from the client:
log.Printf("Debug: Decline Message:%v\n", packet)
case dhcp4.Release:
//Decline from the client:
log.Printf("Debug: Release Message:%v\n", packet)
default:
log.Printf("Debug: Unexpected Packet Type:%v\n", dhcp4.MessageType(packetOptions[dhcp4.OptionDHCPMessageType][0]))
}
return dhcp4.Packet{}, nil
}
/*
* Create DHCP Offer Packet
*/
func (s *Server) OfferPacket(discoverPacket dhcp4.Packet) dhcp4.Packet {
offerPacket := dhcp4.NewPacket(dhcp4.BootReply)
offerPacket.SetXId(discoverPacket.XId())
offerPacket.SetFlags(discoverPacket.Flags())
offerPacket.SetCHAddr(discoverPacket.CHAddr())
offerPacket.SetGIAddr(discoverPacket.GIAddr())
offerPacket.SetSecs(discoverPacket.Secs())
//53
offerPacket.AddOption(dhcp4.OptionDHCPMessageType, []byte{byte(dhcp4.Offer)})
//54
offerPacket.AddOption(dhcp4.OptionServerIdentifier, s.ip.To4())
//51
offerPacket.AddOption(dhcp4.OptionIPAddressLeaseTime, dhcp4.OptionsLeaseTime(s.leaseDuration))
//Other options go in requested order...
discoverPacketOptions := discoverPacket.ParseOptions()
ourOptions := make(dhcp4.Options)
//1
ourOptions[dhcp4.OptionSubnetMask] = s.subnetMask.To4()
//3
ourOptions[dhcp4.OptionRouter] = s.defaultGateway.To4()
//6
ourOptions[dhcp4.OptionDomainNameServer] = dhcp4.JoinIPs(s.dnsServers)
if discoverPacketOptions[dhcp4.OptionParameterRequestList] != nil {
//Loop through the requested options and if we have them add them.
for _, optionCode := range discoverPacketOptions[dhcp4.OptionParameterRequestList] {
if !bytes.Equal(ourOptions[dhcp4.OptionCode(optionCode)], []byte{}) {
offerPacket.AddOption(dhcp4.OptionCode(optionCode), ourOptions[dhcp4.OptionCode(optionCode)])
delete(ourOptions, dhcp4.OptionCode(optionCode))
}
}
}
//Add all the options not requested.
for optionCode, optionValue := range ourOptions {
offerPacket.AddOption(optionCode, optionValue)
}
return offerPacket
}
/*
* Create DHCP Acknowledgement
*/
func (s *Server) AcknowledgementPacket(requestPacket dhcp4.Packet) dhcp4.Packet {
acknowledgementPacket := dhcp4.NewPacket(dhcp4.BootReply)
acknowledgementPacket.SetXId(requestPacket.XId())
acknowledgementPacket.SetFlags(requestPacket.Flags())
acknowledgementPacket.SetGIAddr(requestPacket.GIAddr())
acknowledgementPacket.SetCHAddr(requestPacket.CHAddr())
acknowledgementPacket.SetSecs(requestPacket.Secs())
acknowledgementPacket.AddOption(dhcp4.OptionDHCPMessageType, []byte{byte(dhcp4.ACK)})
acknowledgementPacket.AddOption(dhcp4.OptionSubnetMask, s.subnetMask.To4())
acknowledgementPacket.AddOption(dhcp4.OptionRouter, s.defaultGateway.To4())
acknowledgementPacket.AddOption(dhcp4.OptionDomainNameServer, dhcp4.JoinIPs(s.dnsServers))
acknowledgementPacket.AddOption(dhcp4.OptionServerIdentifier, s.ip.To4())
return acknowledgementPacket
}
/*
* Create DHCP Decline
*/
func (s *Server) DeclinePacket(requestPacket dhcp4.Packet) dhcp4.Packet {
declinePacket := dhcp4.NewPacket(dhcp4.BootReply)
declinePacket.SetXId(requestPacket.XId())
declinePacket.SetFlags(requestPacket.Flags())
declinePacket.SetGIAddr(requestPacket.GIAddr())
declinePacket.SetCHAddr(requestPacket.CHAddr())
declinePacket.SetSecs(requestPacket.Secs())
declinePacket.AddOption(dhcp4.OptionDHCPMessageType, []byte{byte(dhcp4.NAK)})
declinePacket.AddOption(dhcp4.OptionSubnetMask, s.subnetMask.To4())
declinePacket.AddOption(dhcp4.OptionRouter, s.defaultGateway.To4())
declinePacket.AddOption(dhcp4.OptionDomainNameServer, dhcp4.JoinIPs(s.dnsServers))
declinePacket.AddOption(dhcp4.OptionServerIdentifier, s.ip.To4())
return declinePacket
}
/*
* Get Lease tries to work out the best lease for the packet supplied.
* Taking into account all Requested IP, Exisitng MACAddresses and Free leases.
*/
func (s *Server) GetLease(packet dhcp4.Packet) (found bool, lease leasepool.Lease, err error) {
packetOptions := packet.ParseOptions()
clientID := getClientID(packetOptions)
//Requested an IP
if (len(packetOptions) > 0) &&
packetOptions[dhcp4.OptionRequestedIPAddress] != nil &&
!net.IP(packetOptions[dhcp4.OptionRequestedIPAddress]).Equal(net.IP{}) {
//An IP Has Been Requested Let's Try and Get that One.
found, lease, err = s.leasePool.GetLease(net.IP(packetOptions[dhcp4.OptionRequestedIPAddress]))
if err != nil {
return
}
if found {
//If lease is free, return it to client. If it is not
//free match against the MAC address and client
//identifier.
if lease.Status == leasepool.Free {
//Lease Is Free you Can Have it.
return
}
if bytes.Equal(lease.MACAddress, packet.CHAddr()) &&
bytes.Equal(lease.ClientID, clientID) {
//Lease isn't free but it's yours
return
}
}
}
//Ok Even if you requested an IP you can't have it.
found, lease, err = s.leasePool.GetLeaseForClient(packet.CHAddr(), clientID)
if found || err != nil {
return
}
//Just get the next free lease if you can.
found, lease, err = s.leasePool.GetNextFreeLease()
return
}
/*
* Shutdown The Server Gracefully
*/
func (s *Server) Shutdown() {
atomic.StoreUint32(&s.shutdown, 1)
s.connection.Close()
}
func (s *Server) shouldShutdown() bool {
return atomic.LoadUint32(&s.shutdown) == 1
}
/*
* Garbage Collection
* Run Garbage Collection On Your Leases To Free Expired Leases.
*/
func (s *Server) GC() error {
leases, err := s.leasePool.GetLeases()
if err != nil {
return err
}
for i := range leases {
if leases[i].Status != leasepool.Free {
//Lease Is Not Free
if time.Now().After(leases[i].Expiry) {
//Lease has expired.
leases[i].Status = leasepool.Free
updated, err := s.leasePool.UpdateLease(leases[i])
if err != nil {
log.Printf("Warning: Error trying to Free Lease %s \"%v\"\n", leases[i].IP.To4().String(), err)
}
if !updated {
log.Printf("Warning: Unable to Free Lease %s\n", leases[i].IP.To4().String())
}
continue
}
}
}
return nil
}