591 lines
16 KiB
Go
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
|
|
}
|