working on adding lease handouts
This commit is contained in:
parent
c475287fb1
commit
a53d392ed8
|
@ -5,6 +5,7 @@ go_library(
|
||||||
srcs = [
|
srcs = [
|
||||||
"options.go",
|
"options.go",
|
||||||
"packet.go",
|
"packet.go",
|
||||||
|
"read_options.go",
|
||||||
],
|
],
|
||||||
importpath = "git.wntrmute.dev/kyle/kdhcp/dhcp",
|
importpath = "git.wntrmute.dev/kyle/kdhcp/dhcp",
|
||||||
visibility = ["//visibility:public"],
|
visibility = ["//visibility:public"],
|
||||||
|
|
|
@ -1,57 +1,72 @@
|
||||||
package dhcp
|
package dhcp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
type OptionTag uint8
|
type OptionTag uint8
|
||||||
|
|
||||||
|
func (opt OptionTag) String() string {
|
||||||
|
s, ok := optionStrings[opt]
|
||||||
|
if !ok {
|
||||||
|
panic(fmt.Sprintf("no string for option %d", opt))
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
type Option func(req *BootRequest, r io.Reader) error
|
type Option func(req *BootRequest, r io.Reader) error
|
||||||
|
|
||||||
const (
|
const (
|
||||||
OptionTagPadding OptionTag = 0
|
OptionTagPadding OptionTag = 0
|
||||||
|
OptionTagSubnetMask OptionTag = 1
|
||||||
|
OptionTagTimeOffset OptionTag = 2
|
||||||
|
OptionTagRouter OptionTag = 3
|
||||||
|
OptionTagDomainNameServer OptionTag = 6
|
||||||
OptionTagHostName OptionTag = 12
|
OptionTagHostName OptionTag = 12
|
||||||
|
OptionTagDomainName OptionTag = 15
|
||||||
|
OptionTagInterfaceMTU OptionTag = 26
|
||||||
|
OptionTagBroadcastAddress OptionTag = 28
|
||||||
|
OptionTagNTPServers OptionTag = 42
|
||||||
|
OptionTagNBNS OptionTag = 44
|
||||||
|
OptionTagNBScope OptionTag = 47
|
||||||
OptionTagMessageType OptionTag = 53
|
OptionTagMessageType OptionTag = 53
|
||||||
OptionTagParameterRequestList OptionTag = 55
|
OptionTagParameterRequestList OptionTag = 55
|
||||||
|
OptionTagDomainSearch OptionTag = 119
|
||||||
|
OptionTagClasslessStaticRoute OptionTag = 121
|
||||||
OptionTagEnd OptionTag = 255
|
OptionTagEnd OptionTag = 255
|
||||||
)
|
)
|
||||||
|
|
||||||
var optionRegistry = map[OptionTag]Option{
|
var optionStrings = map[OptionTag]string{
|
||||||
OptionTagPadding: OptionPad,
|
OptionTagPadding: "pad",
|
||||||
OptionTagHostName: OptionHostName,
|
OptionTagSubnetMask: "subnet mask",
|
||||||
OptionTagMessageType: OptionMessageType,
|
OptionTagTimeOffset: "time offset from UTC",
|
||||||
OptionTagParameterRequestList: OptionParameterRequestList,
|
OptionTagRouter: "routers",
|
||||||
OptionTagEnd: OptionEnd,
|
OptionTagDomainNameServer: "domain name servers",
|
||||||
|
OptionTagHostName: "host name",
|
||||||
|
OptionTagDomainName: "domain name",
|
||||||
|
OptionTagInterfaceMTU: "interface MTU",
|
||||||
|
OptionTagBroadcastAddress: "broadcast address",
|
||||||
|
OptionTagNTPServers: "NTP servers",
|
||||||
|
OptionTagNBNS: "NetBIOS name servers",
|
||||||
|
OptionTagNBScope: "NetBIOS scope",
|
||||||
|
OptionTagMessageType: "message type",
|
||||||
|
OptionTagParameterRequestList: "parameter request list",
|
||||||
|
OptionTagDomainSearch: "search domain",
|
||||||
|
OptionTagClasslessStaticRoute: "classless static route",
|
||||||
|
OptionTagEnd: "end",
|
||||||
}
|
}
|
||||||
|
|
||||||
func OptionPad(req *BootRequest, r io.Reader) error {
|
type DHCPMessageType uint8
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func OptionHostName(req *BootRequest, r io.Reader) error {
|
const (
|
||||||
return errors.New("dhcp: option not implemented yet")
|
DHCPMessageTypeDiscover DHCPMessageType = 1
|
||||||
}
|
DHCPMessageTypeOffer DHCPMessageType = 2
|
||||||
|
DHCPMessageTypeRequest DHCPMessageType = 3
|
||||||
func OptionMessageType(req *BootRequest, r io.Reader) error {
|
DHCPMessageTypeDecline DHCPMessageType = 4
|
||||||
return errors.New("dhcp: option not implemented yet")
|
DHCPMessageTypeAck DHCPMessageType = 5
|
||||||
}
|
DHCPMessageTypeNAK DHCPMessageType = 6
|
||||||
|
DHCPMessageTypeRelease DHCPMessageType = 7
|
||||||
func OptionParameterRequestList(req *BootRequest, r io.Reader) error {
|
DHCPMessageTypeInform DHCPMessageType = 8
|
||||||
return errors.New("dhcp: option not implemented yet")
|
)
|
||||||
}
|
|
||||||
|
|
||||||
func OptionEnd(req *BootRequest, r io.Reader) error {
|
|
||||||
return errors.New("dhcp: option not implemented yet")
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadOption(req *BootRequest, tag byte, r io.Reader) error {
|
|
||||||
opt := OptionTag(tag)
|
|
||||||
if f, ok := optionRegistry[opt]; ok {
|
|
||||||
return f(req, r)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("dhcp: unknown/unhandled option %d", opt)
|
|
||||||
}
|
|
||||||
|
|
|
@ -44,16 +44,21 @@ type BootRequest struct {
|
||||||
NextIP net.IP
|
NextIP net.IP
|
||||||
RelayIP net.IP
|
RelayIP net.IP
|
||||||
|
|
||||||
DHCPType uint8 // option 53
|
DHCPType DHCPMessageType // option 53
|
||||||
HostName string // option 12
|
HostName string // option 12
|
||||||
ParameterRequests []string
|
ParameterRequests []OptionTag
|
||||||
|
endOptions bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPacketReaderFunc(r io.Reader) func(v any) error {
|
||||||
|
return func(v any) error {
|
||||||
|
return binary.Read(r, binary.BigEndian, v)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (req *BootRequest) Read(packet []byte) error {
|
func (req *BootRequest) Read(packet []byte) error {
|
||||||
buf := bytes.NewBuffer(packet)
|
buf := bytes.NewBuffer(packet)
|
||||||
read := func(v any) error {
|
read := newPacketReaderFunc(buf)
|
||||||
return binary.Read(buf, binary.BigEndian, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := read(&req.MessageType); err != nil {
|
if err := read(&req.MessageType); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -113,7 +118,6 @@ func (req *BootRequest) Read(packet []byte) error {
|
||||||
if _, err := buf.Read(hwaPad); err != nil {
|
if _, err := buf.Read(hwaPad); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Debugf("padding: %x", hwaPad)
|
|
||||||
|
|
||||||
tempBuf := make([]byte, maxServerName)
|
tempBuf := make([]byte, maxServerName)
|
||||||
if _, err := buf.Read(tempBuf); err != nil {
|
if _, err := buf.Read(tempBuf); err != nil {
|
||||||
|
@ -127,7 +131,15 @@ func (req *BootRequest) Read(packet []byte) error {
|
||||||
}
|
}
|
||||||
req.FileName = string(bytes.Trim(tempBuf, "\x00"))
|
req.FileName = string(bytes.Trim(tempBuf, "\x00"))
|
||||||
|
|
||||||
|
if err := ReadMagicCookie(buf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
if req.endOptions {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
tag, err := buf.ReadByte()
|
tag, err := buf.ReadByte()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
|
@ -138,6 +150,8 @@ func (req *BootRequest) Read(packet []byte) error {
|
||||||
|
|
||||||
err = ReadOption(req, tag, buf)
|
err = ReadOption(req, tag, buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Spew(*req)
|
||||||
|
log.Spew(packet)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,133 @@
|
||||||
|
package dhcp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
log "git.wntrmute.dev/kyle/goutils/syslog"
|
||||||
|
)
|
||||||
|
|
||||||
|
var optionRegistry = map[OptionTag]Option{
|
||||||
|
OptionTagPadding: OptionPad,
|
||||||
|
OptionTagHostName: OptionHostName,
|
||||||
|
OptionTagMessageType: OptionMessageType,
|
||||||
|
OptionTagParameterRequestList: OptionParameterRequestList,
|
||||||
|
OptionTagEnd: OptionEnd,
|
||||||
|
}
|
||||||
|
|
||||||
|
func OptionPad(req *BootRequest, r io.Reader) error {
|
||||||
|
// The padding option is a single 0 byte octet.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptionHostName reads a DHCP host name option.
|
||||||
|
//
|
||||||
|
// 3.14. Host Name Option
|
||||||
|
//
|
||||||
|
// This option specifies the name of the client. The name may or may
|
||||||
|
// not be qualified with the local domain name (see section 3.17 for the
|
||||||
|
// preferred way to retrieve the domain name). See RFC 1035 for
|
||||||
|
// character set restrictions.
|
||||||
|
|
||||||
|
// The code for this option is 12, and its minimum length is 1.
|
||||||
|
func OptionHostName(req *BootRequest, r io.Reader) error {
|
||||||
|
read := newPacketReaderFunc(r)
|
||||||
|
|
||||||
|
var length uint8
|
||||||
|
if err := read(&length); err != nil {
|
||||||
|
return fmt.Errorf("dhcp: reading option length for DHCP Message Type")
|
||||||
|
} else if length == 0 {
|
||||||
|
return errors.New("dhcp: read option length 0, but expected option length for DHCP Host Name is >= 1")
|
||||||
|
}
|
||||||
|
|
||||||
|
hostName := make([]byte, int(length))
|
||||||
|
if n, err := r.Read(hostName); err != nil {
|
||||||
|
return fmt.Errorf("dhcp: while reading hostname: %w", err)
|
||||||
|
} else if n != int(length) {
|
||||||
|
return fmt.Errorf("dhcp: only read %d bytes of hostname, expected %d bytes", n, length)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.HostName = string(hostName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func OptionMessageType(req *BootRequest, r io.Reader) error {
|
||||||
|
read := newPacketReaderFunc(r)
|
||||||
|
|
||||||
|
var length uint8
|
||||||
|
if err := read(&length); err != nil {
|
||||||
|
return fmt.Errorf("dhcp: reading option length for DHCP Message Type")
|
||||||
|
} else if length != 1 {
|
||||||
|
return fmt.Errorf("dhcp: read option length %d, but expected option length for DHCP Message Type is 1", length)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := read(&req.DHCPType); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func OptionParameterRequestList(req *BootRequest, r io.Reader) error {
|
||||||
|
read := newPacketReaderFunc(r)
|
||||||
|
|
||||||
|
var length uint8
|
||||||
|
if err := read(&length); err != nil {
|
||||||
|
return fmt.Errorf("dhcp: reading option length for DHCP Message Type")
|
||||||
|
} else if length == 0 {
|
||||||
|
return fmt.Errorf("dhcp: read option length %d, but expected option length for DHCP Parameter Request is >= 1", length)
|
||||||
|
}
|
||||||
|
|
||||||
|
var parameters = make([]byte, int(length))
|
||||||
|
if n, err := r.Read(parameters); err != nil {
|
||||||
|
return fmt.Errorf("dhcp: while reading parameters: %w", err)
|
||||||
|
} else if n != int(length) {
|
||||||
|
return fmt.Errorf("dhcp: only read %d octets of requested parameters, expected %d octets", n, length)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, parameter := range parameters {
|
||||||
|
opt := OptionTag(parameter)
|
||||||
|
log.Debugf("client is requesting %s", opt)
|
||||||
|
req.ParameterRequests = append(req.ParameterRequests, opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func OptionEnd(req *BootRequest, r io.Reader) error {
|
||||||
|
req.endOptions = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadOption(req *BootRequest, tag byte, r io.Reader) error {
|
||||||
|
opt := OptionTag(tag)
|
||||||
|
if f, ok := optionRegistry[opt]; ok {
|
||||||
|
log.Debugf("dhcp: reading option=%s", opt)
|
||||||
|
return f(req, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("dhcp: unknown/unhandled option %d", opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
const magicCookieLength = 4
|
||||||
|
|
||||||
|
var magicCookie = []byte{99, 130, 83, 99}
|
||||||
|
|
||||||
|
func ReadMagicCookie(r io.Reader) error {
|
||||||
|
var cookie = make([]byte, magicCookieLength)
|
||||||
|
n, err := r.Read(cookie)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("dhcp: failed to read magic cookie: %w", err)
|
||||||
|
} else if n != magicCookieLength {
|
||||||
|
return fmt.Errorf("dhcp: read %d bytes, expected to read %d bytes for the magic cookie",
|
||||||
|
n, magicCookieLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(cookie, magicCookie) {
|
||||||
|
return fmt.Errorf("dhcp: read magic cookie %x, expected %x", cookie, magicCookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -15,7 +15,7 @@ type LeaseInfo struct {
|
||||||
Expires time.Time `yaml:"expires"`
|
Expires time.Time `yaml:"expires"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type sortableLease []LeaseInfo
|
type sortableLease []*LeaseInfo
|
||||||
|
|
||||||
func (a sortableLease) Len() int { return len(a) }
|
func (a sortableLease) Len() int { return len(a) }
|
||||||
func (a sortableLease) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
func (a sortableLease) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
|
@ -33,14 +33,14 @@ func (li *LeaseInfo) Expire() {
|
||||||
li.Expires = time.Time{}
|
li.Expires = time.Time{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func SortLeases(leases []LeaseInfo) []LeaseInfo {
|
func SortLeases(leases []*LeaseInfo) []*LeaseInfo {
|
||||||
sortable := sortableLease(leases)
|
sortable := sortableLease(leases)
|
||||||
sort.Sort(sortable)
|
sort.Sort(sortable)
|
||||||
|
|
||||||
return []LeaseInfo(sortable)
|
return []*LeaseInfo(sortable)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (lease LeaseInfo) Reset() LeaseInfo {
|
func (lease *LeaseInfo) Reset() *LeaseInfo {
|
||||||
lease.Expires = time.Time{}
|
lease.Expires = time.Time{}
|
||||||
lease.HardwareAddress = nil
|
lease.HardwareAddress = nil
|
||||||
return lease
|
return lease
|
||||||
|
|
101
iptools/pool.go
101
iptools/pool.go
|
@ -1,58 +1,141 @@
|
||||||
package iptools
|
package iptools
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.wntrmute.dev/kyle/goutils/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const DefaultExpiry = 168 * time.Hour
|
||||||
|
|
||||||
type Pool struct {
|
type Pool struct {
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
||||||
Range *Range `yaml:"range"`
|
Range *Range `yaml:"range"`
|
||||||
Expiry time.Duration `yaml:"expiry"`
|
Expiry time.Duration `yaml:"expiry"`
|
||||||
Available []LeaseInfo `yaml:"available"`
|
Available []*LeaseInfo `yaml:"available"`
|
||||||
Active map[netip.Addr]LeaseInfo `yaml:"active"`
|
Active map[netip.Addr]*LeaseInfo `yaml:"active"`
|
||||||
Limbo map[netip.Addr]LeaseInfo `yaml:"limbo"` // leases that are currently being offered
|
Limbo map[netip.Addr]*LeaseInfo `yaml:"limbo"` // leases that are currently being offered
|
||||||
|
NoHostName bool `yaml:"no_hostname"` // don't set the hostname
|
||||||
|
mtx *sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pool) lock() {
|
||||||
|
if p.lock == nil {
|
||||||
|
p.mtx = &sync.Mutex{}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.mtx.Lock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pool) unlock() {
|
||||||
|
if p.mtx == nil {
|
||||||
|
panic("pool: attempted to unlock uninitialized mutex")
|
||||||
|
}
|
||||||
|
|
||||||
|
p.mtx.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPool(name string, r *Range) (*Pool, error) {
|
||||||
|
if r.Expiry == 0 {
|
||||||
|
r.Expiry = DefaultExpiry
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPool(name string, exp time.Duration, r *Range) (*Pool, error) {
|
|
||||||
p := &Pool{
|
p := &Pool{
|
||||||
Name: name,
|
Name: name,
|
||||||
Expiry: exp,
|
Expiry: r.Expiry,
|
||||||
|
NoHostName: r.NoHostName,
|
||||||
Available: enumerateRange(name, r, true),
|
Available: enumerateRange(name, r, true),
|
||||||
}
|
}
|
||||||
|
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Pool) Sort() {
|
func (p *Pool) sort() {
|
||||||
p.Available = SortLeases(p.Available)
|
p.Available = SortLeases(p.Available)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Pool) Sort() {
|
||||||
|
p.lock()
|
||||||
|
defer p.unlock()
|
||||||
|
|
||||||
|
p.sort()
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Pool) IsAddressAvailable() bool {
|
func (p *Pool) IsAddressAvailable() bool {
|
||||||
return len(p.Available) > 0
|
return len(p.Available) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Pool) Peek(t time.Time, waitFor time.Duration) netip.Addr {
|
// Peek returns the first available address from the pool and moves it
|
||||||
|
// from available to limbo. When the client is given the address, Accept
|
||||||
|
// should be called on the address to move it from limbo to active.
|
||||||
|
func (p *Pool) Peek(t time.Time, waitFor time.Duration) *LeaseInfo {
|
||||||
|
p.lock()
|
||||||
|
defer p.unlock()
|
||||||
|
|
||||||
|
li := p.peek(t, waitFor)
|
||||||
|
return li
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pool) peek(t time.Time, waitFor time.Duration) *LeaseInfo {
|
||||||
|
if len(p.Available) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
lease := p.Available[0]
|
lease := p.Available[0]
|
||||||
p.Available = p.Available[1:]
|
p.Available = p.Available[1:]
|
||||||
lease.ResetExpiry(t, waitFor)
|
lease.ResetExpiry(t, waitFor)
|
||||||
|
|
||||||
p.Limbo[lease.Addr] = lease
|
p.Limbo[lease.Addr] = lease
|
||||||
|
|
||||||
return lease.Addr
|
return lease
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pool) accept(addr netip.Addr) error {
|
||||||
|
assert.Bool(p.Active[addr] == nil, fmt.Sprintf("limbo address %s is already active: %#v", addr, *p.Active[addr]))
|
||||||
|
|
||||||
|
p.Active[addr] = p.Limbo[addr]
|
||||||
|
delete(p.Limbo, addr)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accept will move an address out limbo and mark it as active.
|
||||||
|
func (p *Pool) Accept(addr netip.Addr, t time.Time) error {
|
||||||
|
p.lock()
|
||||||
|
defer p.unlock()
|
||||||
|
|
||||||
|
p.update(t)
|
||||||
|
p.accept(addr)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update checks expirations on leases, moving any expired leases back
|
// Update checks expirations on leases, moving any expired leases back
|
||||||
// to the available pool and removing any limbo leases that are expired.
|
// to the available pool and removing any limbo leases that are expired.
|
||||||
func (p *Pool) Update(t time.Time) {
|
func (p *Pool) Update(t time.Time) {
|
||||||
|
p.lock()
|
||||||
|
defer p.unlock()
|
||||||
|
|
||||||
|
p.update(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pool) update(t time.Time) {
|
||||||
|
evictedHosts := []netip.Addr{}
|
||||||
|
|
||||||
for k, v := range p.Active {
|
for k, v := range p.Active {
|
||||||
if v.IsExpired(t) {
|
if v.IsExpired(t) {
|
||||||
delete(p.Active, k)
|
evictedHosts = append(evictedHosts, k)
|
||||||
v = v.Reset()
|
v = v.Reset()
|
||||||
p.Available = append(p.Available, v)
|
p.Available = append(p.Available, v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, ip := range evictedHosts {
|
||||||
|
delete(p.Active, ip)
|
||||||
|
}
|
||||||
|
|
||||||
for k, v := range p.Limbo {
|
for k, v := range p.Limbo {
|
||||||
if v.IsExpired(t) {
|
if v.IsExpired(t) {
|
||||||
delete(p.Limbo, k)
|
delete(p.Limbo, k)
|
||||||
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -18,7 +17,7 @@ func TestBasicPool(t *testing.T) {
|
||||||
End: poolTestIP2,
|
End: poolTestIP2,
|
||||||
}
|
}
|
||||||
|
|
||||||
p, err := NewPool("cluster", 24*time.Hour, r)
|
p, err := NewPool("cluster", r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package iptools
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -13,6 +14,8 @@ type Range struct {
|
||||||
Start netip.Addr `yaml:"start"`
|
Start netip.Addr `yaml:"start"`
|
||||||
End netip.Addr `yaml:"end"`
|
End netip.Addr `yaml:"end"`
|
||||||
Network netip.Prefix `yaml:"network"`
|
Network netip.Prefix `yaml:"network"`
|
||||||
|
Expiry time.Duration `yaml:"expiry"`
|
||||||
|
NoHostName bool `yaml:"no_hostname"` // don't set the hostname
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Range) Validate() error {
|
func (r *Range) Validate() error {
|
||||||
|
@ -54,7 +57,7 @@ func (r *Range) numHosts() int {
|
||||||
hosts := 0
|
hosts := 0
|
||||||
for cur.Compare(r.End) < 1 {
|
for cur.Compare(r.End) < 1 {
|
||||||
hosts++
|
hosts++
|
||||||
cur.Next()
|
cur = cur.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
return hosts
|
return hosts
|
||||||
|
|
|
@ -47,3 +47,16 @@ func TestBasicValidation(t *testing.T) {
|
||||||
t.Fatal("range 4 should be invalid")
|
t.Fatal("range 4 should be invalid")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNumHosts(t *testing.T) {
|
||||||
|
r := &Range{
|
||||||
|
Start: rangeTestIP1,
|
||||||
|
End: rangeTestIP2,
|
||||||
|
}
|
||||||
|
expected := 15
|
||||||
|
|
||||||
|
numHosts := r.numHosts()
|
||||||
|
if numHosts != expected {
|
||||||
|
t.Fatalf("have %d hosts, expected %d", numHosts, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
package iptools
|
package iptools
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
func enumerateRange(name string, r *Range, startFromOne bool) []LeaseInfo {
|
func enumerateRange(name string, r *Range, startFromOne bool) []*LeaseInfo {
|
||||||
start := r.Start
|
start := r.Start
|
||||||
cur := start
|
cur := start
|
||||||
lenfmt := fmt.Sprintf("%%s%%0%dd", len(fmt.Sprintf("%d", r.numHosts())))
|
lenfmt := fmt.Sprintf("%%s%%0%dd", len(fmt.Sprintf("%d", r.numHosts())))
|
||||||
|
@ -10,11 +12,12 @@ func enumerateRange(name string, r *Range, startFromOne bool) []LeaseInfo {
|
||||||
if startFromOne {
|
if startFromOne {
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
leases := []LeaseInfo{}
|
leases := []*LeaseInfo{}
|
||||||
|
|
||||||
for r.End.Compare(cur) >= 0 {
|
for r.End.Compare(cur) >= 0 {
|
||||||
leases = append(leases, LeaseInfo{
|
hostName := fmt.Sprintf(lenfmt, name, i)
|
||||||
HostName: fmt.Sprintf(lenfmt, name, i),
|
leases = append(leases, &LeaseInfo{
|
||||||
|
HostName: hostName,
|
||||||
Addr: cur,
|
Addr: cur,
|
||||||
})
|
})
|
||||||
i++
|
i++
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
kdhcp:
|
kdhcp:
|
||||||
version: 1
|
version: 1
|
||||||
lease_file: /tmp/kdhcp_lease.yaml
|
lease_file: /tmp/kdhcp_lease.yaml
|
||||||
interface: deveth0
|
interface: enp89s0
|
||||||
port: 67
|
port: 67
|
||||||
network:
|
network:
|
||||||
address: 192.168.4.250
|
address: 192.168.4.250
|
||||||
|
@ -22,6 +22,7 @@ kdhcp:
|
||||||
default:
|
default:
|
||||||
start: 192.168.4.128
|
start: 192.168.4.128
|
||||||
end: 192.168.4.164
|
end: 192.168.4.164
|
||||||
|
no_hostname: True
|
||||||
statics:
|
statics:
|
||||||
controller: 192.168.4.254
|
controller: 192.168.4.254
|
||||||
haven: 192.168.4.253
|
haven: 192.168.4.253
|
||||||
|
|
|
@ -5,6 +5,8 @@ go_library(
|
||||||
srcs = [
|
srcs = [
|
||||||
"ifi.go",
|
"ifi.go",
|
||||||
"ifi_linux.go",
|
"ifi_linux.go",
|
||||||
|
"lease_state.go",
|
||||||
|
"pools.go",
|
||||||
"server.go",
|
"server.go",
|
||||||
],
|
],
|
||||||
importpath = "git.wntrmute.dev/kyle/kdhcp/server",
|
importpath = "git.wntrmute.dev/kyle/kdhcp/server",
|
||||||
|
@ -12,6 +14,8 @@ go_library(
|
||||||
deps = [
|
deps = [
|
||||||
"//config",
|
"//config",
|
||||||
"//dhcp",
|
"//dhcp",
|
||||||
|
"//iptools",
|
||||||
"@dev_wntrmute_git_kyle_goutils//syslog",
|
"@dev_wntrmute_git_kyle_goutils//syslog",
|
||||||
|
"@in_gopkg_yaml_v2//:yaml_v2",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
log "git.wntrmute.dev/kyle/goutils/syslog"
|
||||||
|
"git.wntrmute.dev/kyle/kdhcp/iptools"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LeaseState struct {
|
||||||
|
Pools map[string]*iptools.Pool `yaml:"pools"`
|
||||||
|
Static map[string]*iptools.LeaseInfo `yaml:"static"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *Server) SaveLeases() error {
|
||||||
|
leaseFile, err := os.Create(srv.Config.LeaseFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("server: while saving leases: %w", err)
|
||||||
|
}
|
||||||
|
defer leaseFile.Close()
|
||||||
|
encoder := yaml.NewEncoder(leaseFile)
|
||||||
|
|
||||||
|
state := &LeaseState{
|
||||||
|
Pools: srv.Pools,
|
||||||
|
Static: srv.Static,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = encoder.Encode(state); err != nil {
|
||||||
|
return fmt.Errorf("server: while saving leases: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = encoder.Close(); err != nil {
|
||||||
|
return fmt.Errorf("server: while saving leases: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *Server) LoadLeases() error {
|
||||||
|
leaseState := &LeaseState{}
|
||||||
|
leaseFile, err := os.Open(srv.Config.LeaseFile)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
log.Warningf("server: not loading leases from %s: lease file not found", srv.Config.LeaseFile)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("server: while reading leases: %w", err)
|
||||||
|
}
|
||||||
|
defer leaseFile.Close()
|
||||||
|
decoder := yaml.NewDecoder(leaseFile)
|
||||||
|
|
||||||
|
if err = decoder.Decode(leaseState); err != nil {
|
||||||
|
return fmt.Errorf("server: while reading leases: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
srv.Pools = leaseState.Pools
|
||||||
|
srv.Static = leaseState.Static
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
log "git.wntrmute.dev/kyle/goutils/syslog"
|
||||||
|
"git.wntrmute.dev/kyle/kdhcp/iptools"
|
||||||
|
)
|
||||||
|
|
||||||
|
// pools.go adds pool functionality to the server.
|
||||||
|
|
||||||
|
func (srv *Server) loadPoolsFromConfig() error {
|
||||||
|
for host, ip := range srv.Config.Statics {
|
||||||
|
addr, ok := netip.AddrFromSlice(ip.To4())
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("server: while instantiating pools, could not load IP %s", ip)
|
||||||
|
}
|
||||||
|
log.Debugf("server: added static host entry %s -> %s", host, addr)
|
||||||
|
srv.Static[host] = &iptools.LeaseInfo{
|
||||||
|
HostName: host,
|
||||||
|
Addr: addr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, ipRange := range srv.Config.Pools {
|
||||||
|
pool, err := iptools.NewPool(name, ipRange)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("server: couldn't load pool %s: %w", name, err)
|
||||||
|
}
|
||||||
|
log.Debugf("server: added pool %s: %s -> %s", name, ipRange.Start, ipRange.End)
|
||||||
|
|
||||||
|
srv.Pools[name] = pool
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -3,19 +3,25 @@ package server
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
log "git.wntrmute.dev/kyle/goutils/syslog"
|
log "git.wntrmute.dev/kyle/goutils/syslog"
|
||||||
"git.wntrmute.dev/kyle/kdhcp/config"
|
"git.wntrmute.dev/kyle/kdhcp/config"
|
||||||
"git.wntrmute.dev/kyle/kdhcp/dhcp"
|
"git.wntrmute.dev/kyle/kdhcp/dhcp"
|
||||||
|
"git.wntrmute.dev/kyle/kdhcp/iptools"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
DefaultPool = "default"
|
||||||
MaxPacketSize = 512
|
MaxPacketSize = 512
|
||||||
|
MaxResponseWait = 5 * time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
Conn net.PacketConn
|
Conn net.PacketConn
|
||||||
Config *config.Config
|
Config *config.Config
|
||||||
|
Pools map[string]*iptools.Pool
|
||||||
|
Static map[string]*iptools.LeaseInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Close() error {
|
func (s *Server) Close() error {
|
||||||
|
@ -24,6 +30,8 @@ func (s *Server) Close() error {
|
||||||
|
|
||||||
func (s *Server) Bind() (err error) {
|
func (s *Server) Bind() (err error) {
|
||||||
// In order to read DHCP packets, we'll need to listen on all addresses.
|
// In order to read DHCP packets, we'll need to listen on all addresses.
|
||||||
|
// That being said, we also want to limit our listening to the DHCP
|
||||||
|
// network device.
|
||||||
ip := net.IP([]byte{0, 0, 0, 0})
|
ip := net.IP([]byte{0, 0, 0, 0})
|
||||||
s.Conn, err = BindInterface(ip, s.Config.Port, s.Config.Interface)
|
s.Conn, err = BindInterface(ip, s.Config.Port, s.Config.Interface)
|
||||||
return err
|
return err
|
||||||
|
@ -46,7 +54,7 @@ func (s *Server) ReadDHCPRequest() (*dhcp.BootRequest, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("server: read %db packet from %s", len(pkt), addr)
|
log.Debugf("server: read packet from %s", addr)
|
||||||
return dhcp.ReadRequest(pkt)
|
return dhcp.ReadRequest(pkt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,10 +85,19 @@ func (s *Server) Listen() {
|
||||||
func New(cfg *config.Config) (*Server, error) {
|
func New(cfg *config.Config) (*Server, error) {
|
||||||
srv := &Server{
|
srv := &Server{
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
|
Pools: map[string]*iptools.Pool{},
|
||||||
|
Static: map[string]*iptools.LeaseInfo{},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := srv.Bind()
|
if err := srv.loadPoolsFromConfig(); err != nil {
|
||||||
if err != nil {
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := srv.LoadLeases(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := srv.Bind(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,3 +105,19 @@ func New(cfg *config.Config) (*Server, error) {
|
||||||
|
|
||||||
return srv, nil
|
return srv, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (srv *Server) SelectLease(req *dhcp.BootRequest, t time.Time) *iptools.LeaseInfo {
|
||||||
|
if li, ok := srv.Static[req.HostName]; ok {
|
||||||
|
return li
|
||||||
|
}
|
||||||
|
|
||||||
|
if pool, ok := srv.Pools[req.HostName]; ok {
|
||||||
|
return pool.Peek(t, MaxResponseWait)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pool, ok := srv.Pools[DefaultPool]; ok {
|
||||||
|
return pool.Peek(t, MaxResponseWait)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue