seqs icon indicating copy to clipboard operation
seqs copied to clipboard

Better DHCP Server implementation

Open soypat opened this issue 1 year ago • 0 comments

Must add a reliable DHCP Server implementation. Below is work so far not present on any branch.

package stacks

import (
	"encoding/binary"
	"errors"
	"io"
	"net/netip"
	"time"

	"github.com/soypat/seqs/eth"
	"github.com/soypat/seqs/eth/dhcp"
)

type dhcpclientv2 struct {
	mac         [6]byte
	lastMsg     time.Time
	addr        [4]byte
	state       uint8
	port        uint16
	requestlist [10]byte
}

type clientcache struct {
	sli []dhcpclientv2
}

func (cc *clientcache) get(mac [6]byte) *dhcpclientv2 {
	for i := range cc.sli {
		if cc.sli[i].mac == mac {
			return &cc.sli[i]
		}
	}
	return nil
}

func (cc *clientcache) put(newClient dhcpclientv2) error {
	for i := range cc.sli {
		if cc.sli[i].mac == ([6]byte{}) {
			cc.sli[i] = newClient
			break
		}
	}
	return errors.New("no more dhcp space")
}

func (cc *clientcache) delete(mac [6]byte) error {
	for i := range cc.sli {
		if cc.sli[i].mac == mac {
			cc.sli[i] = dhcpclientv2{}
			break
		}
	}
	return errors.New("no more dhcp space")
}

type DHCPServerV2 struct {
	stack      *PortStack
	nextAddr   netip.Addr
	siaddr     netip.Addr
	port       uint16
	clients    clientcache
	aborted    bool
	lastPacket UDPPacket
	hasPacket  bool

	// Aux variables used to store intermediate client options.

	auxMsgType dhcp.MessageType
	auxreqlist [10]byte
	auxreqip   [4]byte
}

type DHCPServerConfigV2 struct {
	Address    netip.AddrPort
	MaxClients int
}

func NewDHCPServerV2(ps *PortStack, cfg DHCPServerConfigV2) (*DHCPServerV2, error) {
	if ps == nil || cfg.Address.Port() == 0 {
		panic("nil portstack or local port")
	}
	return &DHCPServerV2{
		stack:   ps,
		clients: clientcache{sli: make([]dhcpclientv2, cfg.MaxClients)},
		port:    cfg.Address.Port(),
		siaddr:  cfg.Address.Addr(),
	}, nil
}

func (d *DHCPServerV2) Start() (err error) {

	err = d.stack.OpenUDP(d.port, d)
	if d.aborted && err == nil {
		d.aborted = false // Clear abort on succesful open.
	}
	return err
}

func (d *DHCPServerV2) recv(pkt *UDPPacket) (err error) {
	if d.isAborted() {
		return io.EOF // Signal to close socket.
	}
	payload := pkt.Payload()
	if len(payload) <= dhcp.OptionsOffset {
		return errors.New("small dhcp packet")
	}
	rcvHdr := dhcp.DecodeHeaderV4(payload)
	if rcvHdr.SIAddr != d.stack.ip {
		return errors.New("dhcp addr mismatch")
	}
	d.auxMsgType = 0
	d.auxreqip = [4]byte{}
	d.auxreqlist = [10]byte{}
	err = dhcp.ForEachOption(payload, d.parseopts)
	if err != nil {
		return err
	}

	client := d.clients.get(pkt.Eth.Source)
	switch d.auxMsgType {
	case dhcp.MsgDiscover:
		if client == nil {
			client = d.clients.get([6]byte{}) // Get next free client.
			if client == nil {
				return errors.New("dhcp server: no free client spaces")
			}
		}
		client.addr = pkt.IP.Source
		client.lastMsg = pkt.Rx
		client.state = dhcpStateGotOffer

	default:
		return errors.New("unexpected or no dhcp msgtype")
	}

	d.hasPacket = true
	d.lastPacket = *pkt
	return nil
}

func (d *DHCPServerV2) parseopts(opt dhcp.Option) error {
	switch opt.Num {
	case dhcp.OptMessageType:
		if len(opt.Data) == 1 {
			d.auxMsgType = dhcp.MessageType(opt.Data[0])
		}
	case dhcp.OptParameterRequestList:
		d.auxreqlist = [10]byte{}
		copy(d.auxreqlist[:], opt.Data)
	case dhcp.OptRequestedIPaddress:
		if len(opt.Data) == 4 {
			d.auxreqip = [4]byte(opt.Data)
		}
	}
	return nil
}

func (d *DHCPServerV2) send(dst []byte) (int, error) {
	if d.isAborted() {
		return 0, io.EOF // Signal to close socket.
	}
	if !d.hasPacket {
		return 0, nil
	}
	n, err := d.handleUDP(dst, &d.lastPacket)
	d.hasPacket = false
	return n, err
}

func (d *DHCPServerV2) isPendingHandling() bool {
	return d.port != 0 && d.hasPacket
}

func (d *DHCPServerV2) isAborted() bool { return d.aborted }

func (d *DHCPServerV2) abort() {
	d.stack.trace("dhcpserver:abort")
	for k := range d.hosts {
		delete(d.hosts, k)
	}
	*d = DHCPServerV2{
		hosts:   d.hosts,
		stack:   d.stack,
		siaddr:  d.siaddr,
		port:    d.port,
		aborted: true,
	}
}

// handleUDP is a legacy packet handling routine. Used because it works.
func (d *DHCPServerV2) handleUDP(resp []byte, packet *UDPPacket) (_ int, err error) {
	// First action is used to send data without having received a packet
	// so hasPacket will be false.
	hasPacket := d.hasPacket
	incpayload := packet.Payload()
	switch {
	case len(resp) < dhcp.SizeHeader:
		return 0, errors.New("short payload to marshall DHCP")
	case hasPacket && len(incpayload) < eth.SizeDHCPHeader:
		return 0, errors.New("short payload to parse DHCP")
	case !hasPacket:
		return 0, nil
	}

	rcvHdr := dhcp.DecodeHeaderV4(incpayload)
	mac := packet.Eth.Source
	client := d.hosts[mac]
	var msgType dhcp.MessageType
	err = dhcp.ForEachOption(incpayload, func(opt dhcp.Option) error {
		switch opt.Num {
		case dhcp.OptMessageType:
			if len(opt.Data) == 1 {
				msgType = dhcp.MessageType(opt.Data[0])
			}
		case dhcp.OptParameterRequestList:
			client.requestlist = [10]byte{}
			copy(client.requestlist[:], opt.Data)
		case dhcp.OptRequestedIPaddress:
			if len(opt.Data) == 4 && client.state == dhcpStateNone {
				client.addr = netip.AddrFrom4([4]byte(opt.Data))
			}
		}
		return nil
	})
	if err != nil || (msgType != 1 && rcvHdr.SIAddr != d.siaddr.As4()) {
		return 0, err
	}

	var Options []dhcp.Option
	switch msgType {
	case dhcp.MsgDiscover:
		if client.state != dhcpStateNone {
			err = errors.New("DHCP Discover on initialized client")
			break
		}
		rcvHdr.YIAddr = d.next(client.addr.As4())
		Options = []dhcp.Option{
			{Num: dhcp.OptMessageType, Data: []byte{byte(dhcp.MsgOffer)}},
		}
		rcvHdr.SIAddr = d.siaddr.As4()
		client.port = packet.UDP.SourcePort
		client.state = dhcpStateWaitOffer

	case dhcp.MsgRequest:
		if client.state != dhcpStateWaitOffer {
			err = errors.New("unexpected DHCP Request")
			break
		}
		Options = []dhcp.Option{
			{Num: dhcp.OptMessageType, Data: []byte{byte(dhcp.MsgAck)}}, // DHCP Message Type: ACK
		}
	}
	if err != nil {
		return 0, nil
	}
	d.hosts[mac] = client
	const dhcpOffset = eth.SizeEthernetHeader + eth.SizeIPv4Header + eth.SizeUDPHeader
	for i := dhcpOffset + 14; i < len(resp); i++ {
		resp[i] = 0 // Zero out BOOTP and options fields.
	}
	rcvHdr.Put(resp[dhcpOffset:])
	// Encode DHCP header + options.
	const magicCookie = 0x63825363
	ptr := dhcpOffset + dhcp.MagicCookieOffset
	binary.BigEndian.PutUint32(resp[ptr:], magicCookie)
	ptr = dhcpOffset + dhcp.OptionsOffset
	for _, opt := range Options {
		n, err := opt.Encode(resp[ptr:])
		if err != nil {
			return n, err
		}
		ptr += n
	}
	resp[ptr] = 0xff // endmark
	ptr++
	// Set Ethernet+IP+UDP headers.
	payload := resp[dhcpOffset:ptr]
	d.setResponseUDP(client.port, packet, payload)
	packet.PutHeaders(resp)
	return ptr, nil
}

func (d *DHCPServer) next(requested [4]byte) [4]byte {
	if requested != [4]byte{} {
		return requested
	}
	return [4]byte{192, 168, 1, 2}
}

func (d *DHCPServer) setResponseUDP(clientport uint16, packet *UDPPacket, payload []byte) {
	const ipLenInWords = 5
	// Ethernet frame.
	packet.Eth.Destination = eth.BroadcastHW6()
	packet.Eth.Source = d.stack.HardwareAddr6()

	packet.Eth.SizeOrEtherType = uint16(eth.EtherTypeIPv4)

	// IPv4 frame.
	packet.IP.Destination = [4]byte{}
	packet.IP.Source = d.siaddr.As4() // Source IP is always zeroed when client sends.
	packet.IP.Protocol = 17           // UDP
	packet.IP.TTL = 64
	packet.IP.ID = prand16(packet.IP.ID)
	packet.IP.VersionAndIHL = ipLenInWords // Sets IHL: No IP options. Version set automatically.
	packet.IP.TotalLength = 4*ipLenInWords + eth.SizeUDPHeader + uint16(len(payload))
	packet.IP.Checksum = packet.IP.CalculateChecksum()
	// TODO(soypat): Document why disabling ToS used by DHCP server may cause Request to fail.
	// Apparently server sets ToS=192. Uncommenting this line causes DHCP to fail on my setup.
	// If left fixed at 192, DHCP does not work.
	// If left fixed at 0, DHCP does not work.
	// Apparently ToS is a function of which state of DHCP one is in. Not sure why code below works.
	packet.IP.ToS = 192
	packet.IP.Flags = 0

	// UDP frame.
	packet.UDP.DestinationPort = clientport
	packet.UDP.SourcePort = d.port
	packet.UDP.Length = packet.IP.TotalLength - 4*ipLenInWords
	packet.UDP.Checksum = packet.UDP.CalculateChecksumIPv4(&packet.IP, payload)
}

soypat avatar Jul 21 '24 13:07 soypat