working offers

This commit is contained in:
2023-05-15 13:09:12 +00:00
parent f4fca2ffd0
commit cec46e68e8
29 changed files with 745 additions and 180 deletions

View File

@@ -1,13 +1,25 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "dhcp",
srcs = [
"offer.go",
"options.go",
"packet.go",
"parameters.go",
"read_options.go",
],
importpath = "git.wntrmute.dev/kyle/kdhcp/dhcp",
visibility = ["//visibility:public"],
deps = ["@dev_wntrmute_git_kyle_goutils//log"],
deps = [
"//leases",
"@dev_wntrmute_git_kyle_goutils//assert",
"@dev_wntrmute_git_kyle_goutils//log",
],
)
go_test(
name = "dhcp_test",
srcs = ["parameters_test.go"],
embed = [":dhcp"],
)

36
dhcp/offer.go Normal file
View File

@@ -0,0 +1,36 @@
package dhcp
import (
"net/netip"
"git.wntrmute.dev/kyle/kdhcp/leases"
)
func NewOffer(request *Packet, server *leases.Server, lease *leases.Info) (offer *Packet, err error) {
packet := &Packet{
MessageType: MessageTypeBootResponse,
HardwareType: request.HardwareType,
HardwareAddress: request.HardwareAddress,
Hops: 0,
TransactionID: request.TransactionID,
SecondsElapsed: 0,
Flags: request.Flags,
ServerName: "",
FileName: "",
ClientIP: request.ClientIP,
YourIP: lease.Addr,
NextIP: netip.IPv4Unspecified(),
RelayIP: netip.IPv4Unspecified(),
DHCPType: DHCPMessageTypeOffer,
HostName: "",
ClientID: request.ClientID,
Parameters: []Parameter{},
}
packet.AddParameter(ParameterDNS(server.Network.DNS))
packet.AddParameter(ParameterHostName(lease.HostName))
packet.AddParameter(ParameterRouter(server.Network.Gateway))
packet.AddParameter(ParameterSubnetMask(server.Network.Mask))
return packet, nil
}

View File

@@ -10,13 +10,13 @@ type OptionTag uint8
func (opt OptionTag) String() string {
s, ok := optionStrings[opt]
if !ok {
panic(fmt.Sprintf("no string for option %d", opt))
return fmt.Sprintf("<unknown DHCP option [%d]>", opt)
}
return s
}
type Option func(req *BootRequest, r io.Reader) error
type Option func(req *Packet, r io.Reader) error
const (
OptionTagPadding OptionTag = 0

View File

@@ -5,10 +5,12 @@ import (
"encoding/binary"
"fmt"
"io"
"net"
"net/netip"
"strings"
"git.wntrmute.dev/kyle/goutils/assert"
log "git.wntrmute.dev/kyle/goutils/log"
"git.wntrmute.dev/kyle/kdhcp/leases"
)
const (
@@ -17,7 +19,10 @@ const (
maxFileName = 128
)
var anyAddr = net.IP([]byte{0, 0, 0, 0})
const (
MessageTypeBootRequest = 1
MessageTypeBootResponse = 2
)
func formatMAC(mac []byte) string {
s := []string{}
@@ -28,27 +33,16 @@ func formatMAC(mac []byte) string {
return strings.Join(s, ":")
}
type BootRequest struct {
MessageType uint8
HardwareType uint8
HardwareAddress []byte
Hops uint8
TransactionID uint32
SecondsElapsed uint16
Flags uint16
ServerName string
FileName string
func readIPv4(r io.Reader) (netip.Addr, error) {
var buf [4]byte
ClientIP net.IP
YourIP net.IP
NextIP net.IP
RelayIP net.IP
n, err := r.Read(buf[:])
if err != nil {
return netip.Addr{}, fmt.Errorf("dhcp: while reading IPv4 address from reader: %w", err)
}
DHCPType DHCPMessageType // option 53
HostName string // option 12
ClientID string // option 61
ParameterRequests []OptionTag
endOptions bool
assert.Bool(n == 4, fmt.Sprintf("read %d bytes, but expected to read 4 bytes", n))
return netip.AddrFrom4(buf), nil
}
func newPacketReaderFunc(r io.Reader) func(v any) error {
@@ -57,87 +51,113 @@ func newPacketReaderFunc(r io.Reader) func(v any) error {
}
}
func (req *BootRequest) Read(packet []byte) error {
func newPacketWriterFunc(w io.Writer) func(v any) error {
return func(v any) error {
return binary.Write(w, binary.BigEndian, v)
}
}
type Packet struct {
MessageType uint8
HardwareType uint8
HardwareAddress leases.HardwareAddress
Hops uint8
TransactionID uint32
SecondsElapsed uint16
Flags uint16
ServerName string
FileName string
ClientIP netip.Addr
YourIP netip.Addr
NextIP netip.Addr
RelayIP netip.Addr
DHCPType DHCPMessageType // option 53
HostName string // option 12
ClientID string // option 61
ParameterRequests []OptionTag
Parameters []Parameter
endOptions bool
}
func (pkt *Packet) Read(packet []byte) (err error) {
buf := bytes.NewBuffer(packet)
read := newPacketReaderFunc(buf)
if err := read(&req.MessageType); err != nil {
if err = read(&pkt.MessageType); err != nil {
return err
}
if err := read(&req.HardwareType); err != nil {
if err = read(&pkt.HardwareType); err != nil {
return err
}
var hwaLength uint8
if err := read(&hwaLength); err != nil {
if err = read(&hwaLength); err != nil {
return err
}
if err := read(&req.Hops); err != nil {
if err = read(&pkt.Hops); err != nil {
return err
}
if err := read(&req.TransactionID); err != nil {
if err = read(&pkt.TransactionID); err != nil {
return err
}
if err := read(&req.SecondsElapsed); err != nil {
if err = read(&pkt.SecondsElapsed); err != nil {
return err
}
if err := read(&req.Flags); err != nil {
if err = read(&pkt.Flags); err != nil {
return err
}
req.ClientIP = anyAddr[:]
if _, err := buf.Read(req.ClientIP); err != nil {
if pkt.ClientIP, err = readIPv4(buf); err != nil {
return err
}
req.YourIP = anyAddr[:]
if _, err := buf.Read(req.YourIP); err != nil {
if pkt.YourIP, err = readIPv4(buf); err != nil {
return err
}
req.NextIP = anyAddr[:]
if _, err := buf.Read(req.NextIP); err != nil {
if pkt.NextIP, err = readIPv4(buf); err != nil {
return err
}
req.RelayIP = anyAddr[:]
if _, err := buf.Read(req.RelayIP); err != nil {
if pkt.RelayIP, err = readIPv4(buf); err != nil {
return err
}
req.HardwareAddress = make([]byte, int(hwaLength))
if _, err := buf.Read(req.HardwareAddress); err != nil {
pkt.HardwareAddress = make([]byte, int(hwaLength))
if _, err = buf.Read(pkt.HardwareAddress); err != nil {
return err
}
hwaPad := make([]byte, maxHardwareAddrLen-hwaLength)
if _, err := buf.Read(hwaPad); err != nil {
if _, err = buf.Read(hwaPad); err != nil {
return err
}
tempBuf := make([]byte, maxServerName)
if _, err := buf.Read(tempBuf); err != nil {
if _, err = buf.Read(tempBuf); err != nil {
return err
}
req.ServerName = string(bytes.Trim(tempBuf, "\x00"))
pkt.ServerName = string(bytes.Trim(tempBuf, "\x00"))
tempBuf = make([]byte, maxFileName)
if _, err := buf.Read(tempBuf); err != nil {
if _, err = buf.Read(tempBuf); err != nil {
return err
}
req.FileName = string(bytes.Trim(tempBuf, "\x00"))
pkt.FileName = string(bytes.Trim(tempBuf, "\x00"))
if err := ReadMagicCookie(buf); err != nil {
if err = ReadMagicCookie(buf); err != nil {
return err
}
for {
if req.endOptions {
if pkt.endOptions {
break
}
@@ -149,9 +169,9 @@ func (req *BootRequest) Read(packet []byte) error {
return err
}
err = ReadOption(req, tag, buf)
err = ReadOption(pkt, tag, buf)
if err != nil {
log.Spew(*req)
log.Spew(*pkt)
log.Spew(packet)
return err
}
@@ -159,13 +179,119 @@ func (req *BootRequest) Read(packet []byte) error {
return nil
}
func ReadRequest(pkt []byte) (*BootRequest, error) {
req := &BootRequest{}
err := req.Read(pkt)
func ReadPacket(pkt []byte) (*Packet, error) {
packet := &Packet{}
err := packet.Read(pkt)
if err != nil {
return nil, err
}
log.Debugf("dhcp: BOOTP request with txid %d for %s", req.TransactionID, formatMAC(req.HardwareAddress))
return req, nil
log.Debugf("dhcp: BOOTP packet with txid %d for %s", packet.TransactionID, formatMAC(packet.HardwareAddress))
return packet, nil
}
func (pkt *Packet) Write(w io.Writer) error {
write := newPacketWriterFunc(w)
if err := write(pkt.MessageType); err != nil {
return err
}
if err := write(pkt.HardwareType); err != nil {
return err
}
if err := write(uint8(len(pkt.HardwareAddress))); err != nil {
return err
}
if err := write(pkt.Hops); err != nil {
return err
}
if err := write(pkt.TransactionID); err != nil {
return err
}
if err := write(pkt.SecondsElapsed); err != nil {
return err
}
if err := write(pkt.Flags); err != nil {
return err
}
if err := write(pkt.ClientIP.AsSlice()); err != nil {
return err
}
if err := write(pkt.YourIP.AsSlice()); err != nil {
return err
}
if err := write(pkt.NextIP.AsSlice()); err != nil {
return err
}
if err := write(pkt.RelayIP.AsSlice()); err != nil {
return err
}
padding := make([]byte, maxHardwareAddrLen-len(pkt.HardwareAddress))
if err := write(pkt.HardwareAddress); err != nil {
return err
}
if err := write(padding); err != nil {
return err
}
padding = make([]byte, maxServerName-len(pkt.ServerName))
if err := write([]byte(pkt.ServerName)); err != nil {
return err
}
if err := write(padding); err != nil {
return err
}
padding = make([]byte, maxFileName-len(pkt.FileName))
if err := write([]byte(pkt.FileName)); err != nil {
return err
}
if err := write(padding); err != nil {
return err
}
if err := write(magicCookie); err != nil {
return err
}
// TODO: write parameter request write
for _, p := range pkt.Parameters {
if !p.Valid() {
continue
}
if err := write(p.Bytes()); err != nil {
return err
}
}
if err := write(OptionTagEnd); err != nil {
return err
}
return nil
}
func WritePacket(pkt *Packet) ([]byte, error) {
buf := &bytes.Buffer{}
if err := pkt.Write(buf); err != nil {
return nil, err
}
return buf.Bytes(), nil
}

124
dhcp/parameters.go Normal file
View File

@@ -0,0 +1,124 @@
package dhcp
import "net/netip"
type Parameter interface {
Code() OptionTag
Len() int
Bytes() []byte // the serialized parameter
Valid() bool
}
func (packet *Packet) AddParameter(p Parameter) {
packet.Parameters = append(packet.Parameters, p)
}
type parameterWithAddr struct {
code OptionTag
addr netip.Addr
}
func (p parameterWithAddr) Code() OptionTag {
return p.code
}
func (p parameterWithAddr) Len() int {
return len(p.addr.AsSlice())
}
func (p parameterWithAddr) Bytes() []byte {
return append([]byte{byte(p.code), uint8(p.Len())},
p.addr.AsSlice()...)
}
func (p parameterWithAddr) Valid() bool {
return p.addr.Is4() && p.addr.IsValid() && !p.addr.IsUnspecified()
}
type parameterWithAddrs struct {
code OptionTag
addrs []netip.Addr
}
func (p parameterWithAddrs) Code() OptionTag {
return p.code
}
func (p parameterWithAddrs) Len() int {
plen := 0
for _, addr := range p.addrs {
plen += len(addr.AsSlice())
}
return plen
}
func (p parameterWithAddrs) Bytes() []byte {
out := []byte{byte(p.code), byte(p.Len())}
for _, addr := range p.addrs {
out = append(out, addr.AsSlice()...)
}
return out
}
func (p parameterWithAddrs) Valid() bool {
for _, addr := range p.addrs {
if !(addr.Is4() && addr.IsValid() && !addr.IsUnspecified()) {
return false
}
}
return true
}
type parameterWithString struct {
code OptionTag
data string
}
func (p parameterWithString) Code() OptionTag {
return p.code
}
func (p parameterWithString) Len() int {
return len(p.data)
}
func (p parameterWithString) Bytes() []byte {
out := []byte{byte(p.code), byte(p.Len())}
return append(out, []byte(p.data)...)
}
func (p parameterWithString) Valid() bool {
return len(p.data) > 0
}
func ParameterRouter(router netip.Addr) Parameter {
return &parameterWithAddr{
code: OptionTagRouter,
addr: router,
}
}
func ParameterSubnetMask(mask netip.Addr) Parameter {
return &parameterWithAddr{
code: OptionTagSubnetMask,
addr: mask,
}
}
func ParameterDNS(dns []netip.Addr) Parameter {
return &parameterWithAddrs{
code: OptionTagDomainNameServer,
addrs: dns,
}
}
func ParameterHostName(name string) Parameter {
return &parameterWithString{
code: OptionTagHostName,
data: name,
}
}

35
dhcp/parameters_test.go Normal file
View File

@@ -0,0 +1,35 @@
package dhcp
import (
"bytes"
"net/netip"
"testing"
)
func Test_parameterSubnetMask(t *testing.T) {
expected := []byte{
byte(OptionTagSubnetMask), 4, 255, 255, 255, 0,
}
prefix := netip.MustParseAddr("255.255.255.0")
param := ParameterSubnetMask(prefix)
out := param.Bytes()
if !bytes.Equal(expected, out) {
t.Fatalf("invalid parameter subnet mask: have %x, want %x", out, expected)
}
}
func Test_parameterRouter(t *testing.T) {
expected := []byte{
byte(OptionTagRouter), 4, 192, 168, 1, 1,
}
router := netip.MustParseAddr("192.168.1.1")
param := ParameterRouter(router)
out := param.Bytes()
if !bytes.Equal(expected, out) {
t.Fatalf("invalid parameter router: have %x, want %x", out, expected)
}
}

View File

@@ -18,7 +18,7 @@ var optionRegistry = map[OptionTag]Option{
OptionTagEnd: OptionEnd,
}
func OptionPad(req *BootRequest, r io.Reader) error {
func OptionPad(req *Packet, r io.Reader) error {
// The padding option is a single 0 byte octet.
return nil
}
@@ -46,7 +46,7 @@ func getOptionLength(r io.Reader) (int, error) {
// character set restrictions.
// The code for this option is 12, and its minimum length is 1.
func OptionHostName(req *BootRequest, r io.Reader) error {
func OptionHostName(req *Packet, r io.Reader) error {
length, err := getOptionLength(r)
if err != nil {
return err
@@ -63,7 +63,7 @@ func OptionHostName(req *BootRequest, r io.Reader) error {
return nil
}
func OptionMessageType(req *BootRequest, r io.Reader) error {
func OptionMessageType(req *Packet, r io.Reader) error {
read := newPacketReaderFunc(r)
if length, err := getOptionLength(r); err != nil {
@@ -79,7 +79,7 @@ func OptionMessageType(req *BootRequest, r io.Reader) error {
return nil
}
func OptionParameterRequestList(req *BootRequest, r io.Reader) error {
func OptionParameterRequestList(req *Packet, r io.Reader) error {
length, err := getOptionLength(r)
if err != nil {
return err
@@ -103,7 +103,7 @@ func OptionParameterRequestList(req *BootRequest, r io.Reader) error {
return nil
}
func OptionClientID(req *BootRequest, r io.Reader) error {
func OptionClientID(req *Packet, r io.Reader) error {
length, err := getOptionLength(r)
if err != nil {
return err
@@ -122,12 +122,12 @@ func OptionClientID(req *BootRequest, r io.Reader) error {
return nil
}
func OptionEnd(req *BootRequest, r io.Reader) error {
func OptionEnd(req *Packet, r io.Reader) error {
req.endOptions = true
return nil
}
func ReadOption(req *BootRequest, tag byte, r io.Reader) error {
func ReadOption(req *Packet, tag byte, r io.Reader) error {
opt := OptionTag(tag)
if f, ok := optionRegistry[opt]; ok {
log.Debugf("dhcp: reading option=%s", opt)
@@ -137,7 +137,7 @@ func ReadOption(req *BootRequest, tag byte, r io.Reader) error {
return readUnknownOption(req, tag, r)
}
func readUnknownOption(req *BootRequest, tag byte, r io.Reader) error {
func readUnknownOption(req *Packet, tag byte, r io.Reader) error {
length, err := getOptionLength(r)
if err != nil {
return err