Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better DHCP Server implementation #32

Open
soypat opened this issue Jul 21, 2024 · 0 comments
Open

Better DHCP Server implementation #32

soypat opened this issue Jul 21, 2024 · 0 comments

Comments

@soypat
Copy link
Owner

soypat commented Jul 21, 2024

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)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant