2063 lines
66 KiB
Go
2063 lines
66 KiB
Go
// Mgmt
|
|
// Copyright (C) 2013-2024+ James Shubin and the project contributors
|
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
//
|
|
// Additional permission under GNU GPL version 3 section 7
|
|
//
|
|
// If you modify this program, or any covered work, by linking or combining it
|
|
// with embedded mcl code and modules (and that the embedded mcl code and
|
|
// modules which link with this program, contain a copy of their source code in
|
|
// the authoritative form) containing parts covered by the terms of any other
|
|
// license, the licensors of this program grant you additional permission to
|
|
// convey the resulting work. Furthermore, the licensors of this program grant
|
|
// the original author, James Shubin, additional permission to update this
|
|
// additional permission if he deems it necessary to achieve the goals of this
|
|
// additional permission.
|
|
|
|
package resources
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"net/url"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/purpleidea/mgmt/engine"
|
|
"github.com/purpleidea/mgmt/engine/traits"
|
|
"github.com/purpleidea/mgmt/util/errwrap"
|
|
|
|
"github.com/coredhcp/coredhcp/handler"
|
|
"github.com/coredhcp/coredhcp/plugins/allocators"
|
|
"github.com/coredhcp/coredhcp/plugins/allocators/bitmap"
|
|
"github.com/insomniacslk/dhcp/dhcpv4"
|
|
"github.com/insomniacslk/dhcp/dhcpv4/server4"
|
|
"go4.org/netipx" // extension of unmerged parts from net/netip
|
|
)
|
|
|
|
func init() {
|
|
engine.RegisterResource("dhcp:server", func() engine.Res { return &DHCPServerRes{} })
|
|
engine.RegisterResource("dhcp:host", func() engine.Res { return &DHCPHostRes{} })
|
|
engine.RegisterResource("dhcp:range", func() engine.Res { return &DHCPRangeRes{} })
|
|
|
|
if _, err := time.ParseDuration(DHCPDefaultLeaseTime); err != nil {
|
|
panic("invalid duration for DHCPDefaultLeaseTime constant")
|
|
}
|
|
}
|
|
|
|
const (
|
|
// DHCPDefaultLeaseTime is the default lease time used when one was not
|
|
// specified explicitly.
|
|
DHCPDefaultLeaseTime = "10m" // common default from dhcpd
|
|
)
|
|
|
|
// DHCPServerRes is a simple dhcp server resource. It responds to dhcp client
|
|
// requests, but does not actually apply any state. The name is used as the
|
|
// address to listen on, unless the Address field is specified, and in that case
|
|
// it is used instead. The resource can offer up dhcp client leases from any
|
|
// number of dhcp:host resources which will get autogrouped into this resource
|
|
// at runtime.
|
|
//
|
|
// This server is not meant as a featureful replacement for the venerable dhcpd,
|
|
// but rather as a simple, dynamic, integrated alternative for bootstrapping new
|
|
// machines and clusters in an elegant way.
|
|
//
|
|
// TODO: Add autoedges between the Interface and any identically named NetRes.
|
|
type DHCPServerRes struct {
|
|
traits.Base // add the base methods without re-implementation
|
|
traits.Edgeable // TODO: add autoedge support
|
|
traits.Groupable // can have DHCPHostRes and more, grouped into it
|
|
|
|
init *engine.Init
|
|
|
|
// Address is the listen address to use for the dhcp server. It is
|
|
// common to use `:67` (the standard) to listen on UDP port 67 on all
|
|
// addresses.
|
|
Address string `lang:"address" yaml:"address"`
|
|
|
|
// Interface is interface to bind to. For example `eth0` for the common
|
|
// case. You may leave this field blank to not run any specific binding.
|
|
// XXX: You need to actually specify an interface here at the moment. :(
|
|
// BUG: https://github.com/insomniacslk/dhcp/issues/372
|
|
Interface string `lang:"interface" yaml:"interface"`
|
|
|
|
// ServerID is a unique IPv4 identifier for this server as specified in
|
|
// the DHCPv4 protocol. It is almost always the IP address of the DHCP
|
|
// server. If you don't specify this, then we will attempt to determine
|
|
// it from the specified interface. If it is set to the empty string,
|
|
// then this won't be set in the DHCP protocol, and your DHCP server
|
|
// might not work as you intend. Otherwise, if a valid value is
|
|
// specified, then this will be used as long as it validates correctly.
|
|
// Please note that if you attempt to automatically determine this from
|
|
// the specified interface, then this only happens at runtime when the
|
|
// first DHCP request needs this or during CheckApply, either of which
|
|
// could fail if for some reason it is not available.
|
|
ServerID *string `lang:"serverid" yaml:"serverid"`
|
|
|
|
// LeaseTime is the default lease duration in a format that is parseable
|
|
// by the golang time.ParseDuration function, for example "60s" or "10m"
|
|
// or "1h42m13s". If it is unspecified, then a default will be used. If
|
|
// the empty string is specified, then no lease time will be set in the
|
|
// DHCP protocol, and your DHCP server might not work as you intend.
|
|
LeaseTime *string `lang:"leasetime" yaml:"leasetime"`
|
|
|
|
// DNS represents a list of DNS servers to offer to the DHCP client.
|
|
// XXX: Is it mandatory? https://github.com/insomniacslk/dhcp/issues/359
|
|
DNS []string `lang:"dns" yaml:"dns"`
|
|
|
|
// Routers represents a list of routers to offer to the DHCP client. It
|
|
// is most common to only specify one unless you know what you're doing.
|
|
Routers []string `lang:"routers" yaml:"routers"`
|
|
|
|
// NBP is the network boot program URL. This is used for the tftp server
|
|
// name and the boot file name. For example, you might use:
|
|
// tftp://192.0.2.13/pxelinux.0 for a common bios, pxe boot setup. Note
|
|
// that the "scheme" prefix is required, and that it's impossible to
|
|
// specify a file that doesn't begin with a leading slash. If you wish
|
|
// to specify a "root less" file (common for legacy tftp setups) then
|
|
// you can use this feature in conjunction with the NBPPath parameter.
|
|
// For DHCPv4, the scheme must be "tftp". This values is used as the
|
|
// default for all dhcp:host resources. You can specify this here, and
|
|
// the NBPPath per-resource and they will successfully combine.
|
|
NBP string `lang:"nbp" yaml:"nbp"`
|
|
|
|
// These private fields are ordered in the handler order, the above
|
|
// public fields are ordered in the human logical order.
|
|
leaseTime time.Duration
|
|
sidMutex *sync.Mutex // guards the serverID field
|
|
serverID net.IP // can be nil
|
|
dnsServers4 []net.IP
|
|
routers4 []net.IP
|
|
|
|
//mutex *sync.RWMutex
|
|
|
|
// Global pool where allocated resources are tracked.
|
|
//reservedMacs map[string]engine.Res // net.HardwareAddr is not comparable :(
|
|
reservedIPs map[netip.Addr]engine.Res // track which res reserved
|
|
|
|
// TODO: add in ipv6 support here or in a separate resource?
|
|
}
|
|
|
|
// Default returns some sensible defaults for this resource.
|
|
func (obj *DHCPServerRes) Default() engine.Res {
|
|
return &DHCPServerRes{}
|
|
}
|
|
|
|
// getAddress returns the actual address to use. When Address is not specified,
|
|
// we use the Name.
|
|
func (obj *DHCPServerRes) getAddress() string {
|
|
if obj.Address != "" {
|
|
return obj.Address
|
|
}
|
|
return obj.Name()
|
|
}
|
|
|
|
// getServerID returns the expected server identifier that we should use, based
|
|
// on our obj.ServerID, obj.Interface and Address value. This function should
|
|
// only return (nil, nil) if the user requested we skip the server id process.
|
|
func (obj *DHCPServerRes) getServerID() (net.IP, error) {
|
|
// First see if the server id was specified explicitly...
|
|
if obj.ServerID != nil {
|
|
id := *obj.ServerID
|
|
if id == "" {
|
|
return nil, nil // skip!
|
|
}
|
|
ip := net.ParseIP(id)
|
|
if ip == nil {
|
|
// We're allowed to fail here, because you can either
|
|
// specify a valid server id, or omit it, but you can
|
|
// never pass in invalid data for no reason at all...
|
|
return nil, fmt.Errorf("the ServerID is not a valid IP: %s", id)
|
|
}
|
|
if ip.To4() == nil {
|
|
return nil, fmt.Errorf("the ServerID is not a valid v4 address: %s", id)
|
|
}
|
|
return ip, nil
|
|
}
|
|
|
|
// Next use the host part of the address if it was specified...
|
|
if host, _, err := net.SplitHostPort(obj.getAddress()); err == nil && host != "" {
|
|
ip := net.ParseIP(host)
|
|
if ip == nil {
|
|
// We're allowed to fail here, because you can either
|
|
// specify a valid server id, or omit it, but you can
|
|
// never pass in invalid data for no reason at all...
|
|
return nil, fmt.Errorf("the Address is not a valid IP: %s", host)
|
|
}
|
|
if ip.To4() == nil {
|
|
return nil, fmt.Errorf("the Address is not a valid v4 address: %s", host)
|
|
}
|
|
return ip, nil
|
|
}
|
|
|
|
// Try and lookup the ServerID automatically if we can, otherwise fail.
|
|
|
|
// TODO: is there a way to determine this without Interface being set?
|
|
// NOTE: we could look on the first interface if there is only one, but
|
|
// what if an earlier graph operation created that interface in our run?
|
|
if obj.Interface == "" {
|
|
return nil, fmt.Errorf("can't get ServerID with an empty Interface")
|
|
}
|
|
|
|
// From `man dhcpd.conf`:
|
|
// The default value is the first IP address associated with the
|
|
// physical network interface on which the request arrived. The usual
|
|
// case where the server-identifier statement needs to be sent is when a
|
|
// physical interface has more than one IP address...
|
|
|
|
iface, err := net.InterfaceByName(obj.Interface) // *net.Interface
|
|
if err != nil {
|
|
return nil, errwrap.Wrapf(err, "error finding interface: %s", obj.Interface)
|
|
}
|
|
if iface == nil {
|
|
return nil, fmt.Errorf("unexpected nil iface")
|
|
}
|
|
|
|
a, err := iface.Addrs()
|
|
if err != nil {
|
|
return nil, errwrap.Wrapf(err, "error getting addrs from interface: %s", iface.Name)
|
|
}
|
|
if len(a) == 0 { // add a better error message in this scenario
|
|
return nil, fmt.Errorf("no addrs were found on %s", iface.Name)
|
|
}
|
|
if obj.init.Debug {
|
|
obj.init.Logf("got %d addrs from %s", len(a), iface.Name)
|
|
for _, addr := range a {
|
|
obj.init.Logf("addr: %s", addr.String())
|
|
}
|
|
}
|
|
|
|
var ip net.IP
|
|
for _, addr := range a {
|
|
// we're only interested in the strings (not the network)
|
|
// NOTE: it's weird you can't get net.IP directly :/ Probably
|
|
// because "the exact form and meaning of the strings is up to
|
|
// the implementation".
|
|
s := addr.String()
|
|
|
|
// Parse by two different methods, since we don't know if it
|
|
// contains a CIDR suffix or not...
|
|
|
|
ip = net.ParseIP(s)
|
|
if ip == nil {
|
|
var err error
|
|
ip, _, err = net.ParseCIDR(s)
|
|
if err != nil || ip == nil {
|
|
continue // nothing found
|
|
}
|
|
}
|
|
|
|
// If we reached here, we have a potential IP...
|
|
if ip.To4() == nil {
|
|
ip = nil
|
|
continue // take the first ipv4 address, or nothing...
|
|
}
|
|
|
|
break // we only care about the first one
|
|
}
|
|
if ip == nil {
|
|
return nil, fmt.Errorf("no valid IPv4 addrs were found")
|
|
}
|
|
|
|
return ip, nil
|
|
}
|
|
|
|
// Validate checks if the resource data structure was populated correctly.
|
|
func (obj *DHCPServerRes) Validate() error {
|
|
// FIXME: https://github.com/insomniacslk/dhcp/issues/372
|
|
if obj.Interface == "" {
|
|
return fmt.Errorf("the Interface is empty")
|
|
}
|
|
|
|
if obj.getAddress() == "" {
|
|
return fmt.Errorf("the Address is empty")
|
|
}
|
|
|
|
// Ensure this format is valid for when we parse it later.
|
|
host, _, err := net.SplitHostPort(obj.getAddress())
|
|
if err != nil {
|
|
return errwrap.Wrapf(err, "the Address is in an invalid format: %s", obj.getAddress())
|
|
}
|
|
if host != "" {
|
|
ip := net.ParseIP(host)
|
|
if ip == nil {
|
|
return fmt.Errorf("the Address is not a valid IP: %s", host)
|
|
}
|
|
if ip.To4() == nil {
|
|
return fmt.Errorf("the Address is not a valid v4 address: %s", host)
|
|
}
|
|
}
|
|
|
|
// TODO: is there a way to determine this without Interface being set?
|
|
// NOTE: we could look on the first interface if there is only one, but
|
|
// what if an earlier graph operation created that interface in our run?
|
|
if obj.ServerID == nil && (obj.Interface == "" && host == "") {
|
|
return fmt.Errorf("can't determine ServerID automatically without Interface or host Address")
|
|
}
|
|
|
|
// We only validate the ServerID if it's specified and not empty.
|
|
// We can't call getServerID because it does run-time dependent checks.
|
|
if obj.ServerID != nil && *obj.ServerID != "" {
|
|
// modified from: https://github.com/coredhcp/coredhcp/blob/b4aa45e6f7268cc4c52f863b130bd8eb388647b2/plugins/serverid/plugin.go#L101
|
|
serverID := net.ParseIP(*obj.ServerID)
|
|
if serverID == nil {
|
|
return fmt.Errorf("%s is an invalid or empty IP address", *obj.ServerID)
|
|
}
|
|
if serverID.To4() == nil {
|
|
return fmt.Errorf("%s is not a valid IPv4 address", *obj.ServerID)
|
|
}
|
|
}
|
|
|
|
// We only validate the LeaseTime if it's specified and not empty.
|
|
if obj.LeaseTime != nil && *obj.LeaseTime != "" {
|
|
// modified from: https://github.com/coredhcp/coredhcp/blob/b4aa45e6f7268cc4c52f863b130bd8eb388647b2/plugins/leasetime/plugin.go#L49
|
|
_, err := time.ParseDuration(*obj.LeaseTime)
|
|
if err != nil {
|
|
return errwrap.Wrapf(err, "invalid duration: %s", *obj.LeaseTime)
|
|
}
|
|
}
|
|
|
|
// Validate the DNS servers.
|
|
// modified from: https://github.com/coredhcp/coredhcp/blob/b4aa45e6f7268cc4c52f863b130bd8eb388647b2/plugins/dns/plugin.go#L52
|
|
for _, ip := range obj.DNS {
|
|
if dns := net.ParseIP(ip); dns.To4() == nil {
|
|
return fmt.Errorf("expected a DNS server address, got: %s", ip)
|
|
}
|
|
}
|
|
|
|
// Validate the routers.
|
|
// modified from: https://github.com/coredhcp/coredhcp/blob/b4aa45e6f7268cc4c52f863b130bd8eb388647b2/plugins/router/plugin.go#L42
|
|
for _, ip := range obj.Routers {
|
|
if router := net.ParseIP(ip); router.To4() == nil {
|
|
return fmt.Errorf("expected a router address, got: %s", ip)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Init runs some startup code for this resource.
|
|
func (obj *DHCPServerRes) Init(init *engine.Init) error {
|
|
obj.init = init // save for later
|
|
|
|
// NOTE: If we don't Init anything that's autogrouped, then it won't
|
|
// even get an Init call on it.
|
|
// TODO: should we do this in the engine? Do we want to decide it here?
|
|
for _, res := range obj.GetGroup() { // grouped elements
|
|
if err := res.Init(init); err != nil {
|
|
return errwrap.Wrapf(err, "autogrouped Init failed")
|
|
}
|
|
}
|
|
|
|
// Ensure the lease time is valid before we try and use it.
|
|
if obj.LeaseTime == nil || *obj.LeaseTime != "" {
|
|
leaseTime := DHCPDefaultLeaseTime
|
|
if obj.LeaseTime != nil {
|
|
leaseTime = *obj.LeaseTime
|
|
}
|
|
|
|
var err error
|
|
if obj.leaseTime, err = time.ParseDuration(leaseTime); err != nil {
|
|
return errwrap.Wrapf(err, "unexpected invalid duration: %s", leaseTime)
|
|
}
|
|
}
|
|
|
|
obj.sidMutex = &sync.Mutex{}
|
|
// We can't do this here, because our network might not be up yet, and
|
|
// if this happens in Init, that's before a Net resource might do it!
|
|
//var err error
|
|
//if obj.serverID, err = obj.getServerID(); err != nil {
|
|
// return errwrap.Wrapf(err, "could not determine the ServerID")
|
|
//}
|
|
|
|
obj.dnsServers4 = []net.IP{}
|
|
for _, ip := range obj.DNS {
|
|
dns := net.ParseIP(ip).To4()
|
|
if dns == nil {
|
|
return fmt.Errorf("unexpected invalid DNS server address, got: %s", ip)
|
|
}
|
|
obj.dnsServers4 = append(obj.dnsServers4, dns)
|
|
}
|
|
|
|
obj.routers4 = []net.IP{}
|
|
for _, ip := range obj.Routers {
|
|
router := net.ParseIP(ip).To4()
|
|
if router == nil {
|
|
return fmt.Errorf("unexpected invalid router address, got: %s", ip)
|
|
}
|
|
obj.routers4 = append(obj.routers4, router)
|
|
}
|
|
|
|
//obj.mutex = &sync.RWMutex{}
|
|
//obj.mutex.RLock()
|
|
|
|
//obj.reservedMacs = make(map[string]engine.Res)
|
|
obj.reservedIPs = make(map[netip.Addr]engine.Res)
|
|
|
|
for _, x := range obj.GetGroup() { // grouped elements
|
|
switch res := x.(type) { // convert from Res
|
|
case *DHCPHostRes:
|
|
|
|
// TODO: reserve res.Mac as well
|
|
|
|
addr, ok := netip.AddrFromSlice(res.ipv4Addr) // net.IP -> netip.Addr
|
|
if !ok {
|
|
// programming error
|
|
return fmt.Errorf("could not convert ip: %s", res.ipv4Addr)
|
|
}
|
|
//addr := res.ipv4Addr // TODO: once ported to netip.Addr
|
|
if r, exists := obj.reservedIPs[addr]; exists {
|
|
// TODO: Could we do all of this in Validate() ?
|
|
return fmt.Errorf("res %s already reserved ip: %s", r, addr)
|
|
}
|
|
|
|
obj.reservedIPs[addr] = res // reserve!
|
|
|
|
case *DHCPRangeRes:
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Cleanup is run by the engine to clean up after the resource is done.
|
|
func (obj *DHCPServerRes) Cleanup() error {
|
|
// NOTE: if this ever panics, it might mean the engine is running Close
|
|
// before Watch finishes exiting, which is an engine bug in that code...
|
|
//obj.mutex.RUnlock()
|
|
return nil
|
|
}
|
|
|
|
// Watch is the primary listener for this resource and it outputs events.
|
|
func (obj *DHCPServerRes) Watch(ctx context.Context) error {
|
|
addr, err := net.ResolveUDPAddr("udp", obj.getAddress()) // *net.UDPAddr
|
|
if err != nil {
|
|
return errwrap.Wrapf(err, "could not resolve address")
|
|
}
|
|
|
|
//conn, err := net.ListenUDP("udp", addr)
|
|
//if err != nil {
|
|
// return errwrap.Wrapf(err, "could not start listener")
|
|
//}
|
|
//defer conn.Close()
|
|
|
|
opts := []server4.ServerOpt{}
|
|
|
|
// This is the variant for the simple interface as seen in:
|
|
// https://github.com/insomniacslk/dhcp/pull/373
|
|
//logf := func(format string, v ...interface{}) {
|
|
// obj.init.Logf("dhcpv4: "+format, v...)
|
|
//}
|
|
//logfOpt := server4.WithLogf(logf) // wrap the server logging...
|
|
//opts = append(opts, logfOpt)
|
|
|
|
newLogger := &overEngineeredLogger{
|
|
logf: func(format string, v ...interface{}) {
|
|
obj.init.Logf(format, v...)
|
|
},
|
|
}
|
|
logOpt := server4.WithLogger(newLogger)
|
|
opts = append(opts, logOpt)
|
|
|
|
server, err := server4.NewServer(obj.Interface, addr, obj.handler4(), opts...)
|
|
if err != nil {
|
|
return errwrap.Wrapf(err, "could not start listener") // it's inside
|
|
}
|
|
|
|
obj.init.Running() // when started, notify engine that we're running
|
|
//defer obj.mutex.RLock()
|
|
//obj.mutex.RUnlock() // it's safe to let CheckApply proceed
|
|
|
|
var closeError error
|
|
closeSignal := make(chan struct{})
|
|
|
|
wg := &sync.WaitGroup{}
|
|
defer wg.Wait()
|
|
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
defer close(closeSignal)
|
|
|
|
err := server.Serve() // blocks until Close() is called I hope!
|
|
// TODO: getting this error is probably a bug, please see:
|
|
// https://github.com/insomniacslk/dhcp/issues/376
|
|
isClosing := errors.Is(err, net.ErrClosed)
|
|
if err == nil || isClosing {
|
|
return
|
|
}
|
|
// if this returned on its own, then closeSignal can be used...
|
|
closeError = errwrap.Wrapf(err, "the server errored")
|
|
}()
|
|
defer server.Close()
|
|
|
|
startupChan := make(chan struct{})
|
|
close(startupChan) // send one initial signal
|
|
|
|
var send = false // send event?
|
|
for {
|
|
if obj.init.Debug {
|
|
obj.init.Logf("Looping...")
|
|
}
|
|
|
|
select {
|
|
case <-startupChan:
|
|
startupChan = nil
|
|
send = true
|
|
|
|
case <-closeSignal: // something shut us down early
|
|
return closeError
|
|
|
|
case <-ctx.Done(): // closed by the engine to signal shutdown
|
|
return nil
|
|
}
|
|
|
|
// do all our event sending all together to avoid duplicate msgs
|
|
if send {
|
|
send = false
|
|
obj.init.Event() // notify engine of an event (this can block)
|
|
}
|
|
}
|
|
}
|
|
|
|
// sidCheckApply runs the server ID cache operation in CheckApply, which can
|
|
// help CheckApply fail before the handler runs, so at least we see an error.
|
|
func (obj *DHCPServerRes) sidCheckApply(ctx context.Context, apply bool) (bool, error) {
|
|
// Mutex guards the cached obj.serverID value.
|
|
defer obj.sidMutex.Unlock()
|
|
obj.sidMutex.Lock()
|
|
|
|
// We've been explicitly asked to skip this handler.
|
|
if obj.ServerID != nil && *obj.ServerID == "" {
|
|
return true, nil
|
|
}
|
|
|
|
if obj.serverID == nil { // lookup the server ID and cache it here...
|
|
var err error
|
|
if obj.serverID, err = obj.getServerID(); err != nil {
|
|
obj.init.Logf("could not determine the ServerID during CheckApply")
|
|
return false, err
|
|
}
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// CheckApply never has anything to do for this resource, so it always succeeds.
|
|
// It does however check that certain runtime requirements (such as the Root dir
|
|
// existing if one was specified) are fulfilled.
|
|
func (obj *DHCPServerRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
|
|
if obj.init.Debug {
|
|
obj.init.Logf("CheckApply")
|
|
}
|
|
|
|
//// We don't want the initial CheckApply to return true until the Watch
|
|
//// has started up, so we must block here until that's the case...
|
|
//ch := make(chan struct{})
|
|
//go func() {
|
|
// // XXX: this goroutine leaks if CheckApply runs right before the
|
|
// // Watch method exits on error. And this could deadlock it too!
|
|
// defer close(ch)
|
|
// defer obj.mutex.Unlock()
|
|
// obj.mutex.Lock() // can't acquire this lock until Watch startup RUnlock
|
|
//
|
|
//}()
|
|
//select {
|
|
//case <-ch:
|
|
////case <-obj.interruptChan: // TODO: if we ever support InterruptableRes
|
|
//case <-obj.init.DoneCtx.Done(): // closed by the engine to signal shutdown
|
|
//}
|
|
|
|
// Cheap runtime validation!
|
|
checkOK := true
|
|
if c, err := obj.sidCheckApply(ctx, apply); err != nil {
|
|
return false, err
|
|
} else if !c {
|
|
checkOK = false
|
|
}
|
|
|
|
return checkOK, nil // almost always succeeds, with nothing to do!
|
|
}
|
|
|
|
// Cmp compares two resources and returns an error if they are not equivalent.
|
|
func (obj *DHCPServerRes) Cmp(r engine.Res) error {
|
|
// we can only compare DHCPServerRes to others of the same resource kind
|
|
res, ok := r.(*DHCPServerRes)
|
|
if !ok {
|
|
return fmt.Errorf("res is not the same kind")
|
|
}
|
|
|
|
if obj.Address != res.Address {
|
|
return fmt.Errorf("the Address differs")
|
|
}
|
|
if obj.Interface != res.Interface {
|
|
return fmt.Errorf("the Interface differs")
|
|
}
|
|
|
|
if (obj.ServerID == nil) != (res.ServerID == nil) { // xor
|
|
return fmt.Errorf("the ServerID differs")
|
|
}
|
|
if obj.ServerID != nil && res.ServerID != nil {
|
|
if *obj.ServerID != *res.ServerID { // compare the strings
|
|
return fmt.Errorf("the contents of ServerID differ")
|
|
}
|
|
}
|
|
|
|
// Be sneaky and compare the actual values. Eg: 60s vs 1m are the same!
|
|
objIsEmptyLeaseTime := obj.LeaseTime != nil && *obj.LeaseTime == ""
|
|
resIsEmptyLeaseTime := res.LeaseTime != nil && *res.LeaseTime == ""
|
|
|
|
// if objIsEmptyLeaseTime && !resIsEmptyLeaseTime : FAIL
|
|
// if !objIsEmptyLeaseTime && resIsEmptyLeaseTime : FAIL
|
|
if objIsEmptyLeaseTime != resIsEmptyLeaseTime {
|
|
return fmt.Errorf("the LeaseTime differs")
|
|
}
|
|
|
|
// if objIsEmptyLeaseTime && resIsEmptyLeaseTime : SKIP
|
|
// if !objIsEmptyLeaseTime && !resIsEmptyLeaseTime : COMPARE
|
|
if !objIsEmptyLeaseTime && !resIsEmptyLeaseTime {
|
|
objLeaseTime := DHCPDefaultLeaseTime
|
|
resLeaseTime := DHCPDefaultLeaseTime
|
|
if obj.LeaseTime != nil {
|
|
objLeaseTime = *obj.LeaseTime
|
|
}
|
|
if res.LeaseTime != nil {
|
|
resLeaseTime = *res.LeaseTime
|
|
}
|
|
d1, err1 := time.ParseDuration(objLeaseTime)
|
|
d2, err2 := time.ParseDuration(resLeaseTime)
|
|
// we can only compare this way if they both parse correctly...
|
|
if err1 == nil && err2 == nil {
|
|
// TODO: should we use Nanoseconds or Seconds instead?
|
|
// NOTE: Seconds are the sane unit for DHCP, and
|
|
// Nanoseconds are the internal representation of the
|
|
// Duration value. LeaseTime option precision is in sec.
|
|
//if d1.Milliseconds() != d2.Milliseconds() {
|
|
if d1.Seconds() != d2.Seconds() {
|
|
return fmt.Errorf("the duration of LeaseTime differs")
|
|
}
|
|
|
|
} else if objLeaseTime != resLeaseTime { // plain string cmp
|
|
return fmt.Errorf("the contents of LeaseTime differs")
|
|
}
|
|
}
|
|
|
|
if len(obj.DNS) != len(res.DNS) {
|
|
return fmt.Errorf("the number of DNS servers differs")
|
|
}
|
|
for i, x := range obj.DNS {
|
|
if dns := res.DNS[i]; x != dns {
|
|
return fmt.Errorf("the DNS server at index %d differs", i)
|
|
}
|
|
}
|
|
|
|
if len(obj.Routers) != len(res.Routers) {
|
|
return fmt.Errorf("the number of routers differs")
|
|
}
|
|
for i, x := range obj.Routers {
|
|
if router := res.Routers[i]; x != router {
|
|
return fmt.Errorf("the router at index %d differs", i)
|
|
}
|
|
}
|
|
|
|
if len(obj.NBP) != len(res.NBP) {
|
|
return fmt.Errorf("the NBP field differs")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Copy copies the resource. Don't call it directly, use engine.ResCopy instead.
|
|
// TODO: should this copy internal state?
|
|
func (obj *DHCPServerRes) Copy() engine.CopyableRes {
|
|
var serverID *string
|
|
if obj.ServerID != nil {
|
|
s := *obj.ServerID // copy
|
|
serverID = &s
|
|
}
|
|
var leaseTime *string
|
|
if obj.LeaseTime != nil {
|
|
x := *obj.LeaseTime // copy
|
|
leaseTime = &x
|
|
}
|
|
dns := []string{}
|
|
for _, x := range obj.DNS {
|
|
dns = append(dns, x)
|
|
}
|
|
routers := []string{}
|
|
for _, x := range obj.Routers {
|
|
routers = append(routers, x)
|
|
}
|
|
return &DHCPServerRes{
|
|
Address: obj.Address,
|
|
Interface: obj.Interface,
|
|
ServerID: serverID,
|
|
LeaseTime: leaseTime,
|
|
DNS: dns,
|
|
Routers: routers,
|
|
NBP: obj.NBP,
|
|
}
|
|
}
|
|
|
|
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
|
// primarily useful for setting the defaults.
|
|
func (obj *DHCPServerRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
type rawRes DHCPServerRes // indirection to avoid infinite recursion
|
|
|
|
def := obj.Default() // get the default
|
|
res, ok := def.(*DHCPServerRes) // put in the right format
|
|
if !ok {
|
|
return fmt.Errorf("could not convert to DHCPServerRes")
|
|
}
|
|
raw := rawRes(*res) // convert; the defaults go here
|
|
|
|
if err := unmarshal(&raw); err != nil {
|
|
return err
|
|
}
|
|
|
|
*obj = DHCPServerRes(raw) // restore from indirection with type conversion!
|
|
return nil
|
|
}
|
|
|
|
// GroupCmp returns whether two resources can be grouped together or not. Can
|
|
// these two resources be merged, aka, does this resource support doing so? Will
|
|
// resource allow itself to be grouped _into_ this obj?
|
|
func (obj *DHCPServerRes) GroupCmp(r engine.GroupableRes) error {
|
|
res1, ok1 := r.(*DHCPHostRes) // different from what we usually do!
|
|
if ok1 {
|
|
// If the dhcp host resource has the Server field specified,
|
|
// then it must match against our name field if we want it to
|
|
// group with us.
|
|
if res1.Server != "" && res1.Server != obj.Name() {
|
|
return fmt.Errorf("resource groups with a different server name")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
res2, ok2 := r.(*DHCPRangeRes) // different from what we usually do!
|
|
if ok2 {
|
|
// If the dhcp range resource has the Server field specified,
|
|
// then it must match against our name field if we want it to
|
|
// group with us.
|
|
if res2.Server != "" && res2.Server != obj.Name() {
|
|
return fmt.Errorf("resource groups with a different server name")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("resource is not the right kind")
|
|
}
|
|
|
|
// leasetimeHandler4 handles DHCPv4 packets for the leasetime component.
|
|
// Modified from: https://github.com/coredhcp/coredhcp/blob/b4aa45e6f7268cc4c52f863b130bd8eb388647b2/plugins/leasetime/plugin.go#L32
|
|
func (obj *DHCPServerRes) leasetimeHandler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
|
|
// We've been explicitly asked to skip this handler.
|
|
if obj.LeaseTime != nil && *obj.LeaseTime == "" {
|
|
return resp, false
|
|
}
|
|
|
|
if req.OpCode != dhcpv4.OpcodeBootRequest {
|
|
return resp, false
|
|
}
|
|
// Set lease time unless it has already been set.
|
|
if !resp.Options.Has(dhcpv4.OptionIPAddressLeaseTime) {
|
|
resp.Options.Update(dhcpv4.OptIPAddressLeaseTime(obj.leaseTime))
|
|
}
|
|
|
|
return resp, false
|
|
}
|
|
|
|
// serverIDHandler4 handles DHCPv4 packets for the serverid component. Modified
|
|
// from: https://github.com/coredhcp/coredhcp/blob/b4aa45e6f7268cc4c52f863b130bd8eb388647b2/plugins/serverid/plugin.go#L78
|
|
func (obj *DHCPServerRes) serverIDHandler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
|
|
// We've been explicitly asked to skip this handler.
|
|
if obj.ServerID != nil && *obj.ServerID == "" {
|
|
return resp, false
|
|
}
|
|
|
|
// Mutex guards the cached obj.serverID value.
|
|
defer obj.sidMutex.Unlock()
|
|
obj.sidMutex.Lock()
|
|
if obj.serverID == nil { // lookup the server ID and cache it here...
|
|
var err error
|
|
if obj.serverID, err = obj.getServerID(); err != nil {
|
|
obj.init.Logf("could not determine the ServerID during runtime")
|
|
return resp, false
|
|
}
|
|
}
|
|
|
|
v4ServerID := obj.serverID.To4()
|
|
if v4ServerID == nil { // We already checked this in Validate!
|
|
panic("unexpected nil serverID")
|
|
}
|
|
|
|
if req.OpCode != dhcpv4.OpcodeBootRequest {
|
|
if obj.init.Debug {
|
|
obj.init.Logf("not a BootRequest, ignoring")
|
|
}
|
|
return resp, false
|
|
}
|
|
if req.ServerIPAddr != nil &&
|
|
!req.ServerIPAddr.Equal(net.IPv4zero) &&
|
|
!req.ServerIPAddr.Equal(v4ServerID) {
|
|
// This request is not for us, drop it.
|
|
if obj.init.Debug {
|
|
obj.init.Logf("requested server ID does not match this server's ID. Got %v, want %v", req.ServerIPAddr, v4ServerID)
|
|
}
|
|
return nil, true
|
|
}
|
|
|
|
resp.ServerIPAddr = make(net.IP, net.IPv4len)
|
|
copy(resp.ServerIPAddr[:], v4ServerID)
|
|
resp.UpdateOption(dhcpv4.OptServerIdentifier(v4ServerID))
|
|
|
|
return resp, false
|
|
}
|
|
|
|
// dnsHandler4 handles DHCPv4 packets for the DNS component. Modified from:
|
|
// https://github.com/coredhcp/coredhcp/blob/b4aa45e6f7268cc4c52f863b130bd8eb388647b2/plugins/dns/plugin.go#L79
|
|
// XXX: Is it mandatory? https://github.com/insomniacslk/dhcp/issues/359
|
|
func (obj *DHCPServerRes) dnsHandler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
|
|
if len(obj.dnsServers4) == 0 {
|
|
return resp, false // skip it
|
|
}
|
|
|
|
if req.IsOptionRequested(dhcpv4.OptionDomainNameServer) {
|
|
resp.Options.Update(dhcpv4.OptDNS(obj.dnsServers4...))
|
|
}
|
|
return resp, false
|
|
}
|
|
|
|
// routerHandler4 handles DHCPv4 packets for the router component. Modified
|
|
// from: https://github.com/coredhcp/coredhcp/blob/b4aa45e6f7268cc4c52f863b130bd8eb388647b2/plugins/router/plugin.go#L61
|
|
func (obj *DHCPServerRes) routerHandler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
|
|
if len(obj.routers4) == 0 {
|
|
return resp, false // skip it
|
|
}
|
|
|
|
resp.Options.Update(dhcpv4.OptRouter(obj.routers4...))
|
|
return resp, false
|
|
}
|
|
|
|
// handler4 handles all the incoming requests from IPv4 clients.
|
|
func (obj *DHCPServerRes) handler4() func(net.PacketConn, net.Addr, *dhcpv4.DHCPv4) {
|
|
|
|
// NOTE: this is similar to MainHandler4 in coredhcp. Keep it in sync...
|
|
return func(conn net.PacketConn, peer net.Addr, req *dhcpv4.DHCPv4) {
|
|
// req is the incoming message from the dhcp client
|
|
// peer is who we're replying to (often a broadcast address)
|
|
obj.init.Logf("received a DHCPv4 packet from: %s", req.ClientHWAddr.String())
|
|
if obj.init.Debug {
|
|
obj.init.Logf("received from DHCPv4 peer: %s", peer)
|
|
obj.init.Logf("received a DHCPv4 packet: %s", req.Summary())
|
|
}
|
|
|
|
var (
|
|
resp, tmp *dhcpv4.DHCPv4
|
|
err error
|
|
stop bool
|
|
)
|
|
if req.OpCode != dhcpv4.OpcodeBootRequest {
|
|
obj.init.Logf("handler4: unsupported opcode %d. Only BootRequest (%d) is supported", req.OpCode, dhcpv4.OpcodeBootRequest)
|
|
return
|
|
}
|
|
tmp, err = dhcpv4.NewReplyFromRequest(req)
|
|
if err != nil {
|
|
obj.init.Logf("handler4: failed to build reply: %v", err)
|
|
return
|
|
}
|
|
// D-O-R-A
|
|
switch mt := req.MessageType(); mt {
|
|
case dhcpv4.MessageTypeDiscover:
|
|
tmp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer))
|
|
case dhcpv4.MessageTypeRequest:
|
|
tmp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck))
|
|
case dhcpv4.MessageTypeDecline:
|
|
// If mask is not set, some DHCP clients will DECLINE.
|
|
obj.init.Logf("handler4: Unhandled decline message: %+v", req)
|
|
return
|
|
default:
|
|
obj.init.Logf("handler4: Unhandled message type: %v", mt)
|
|
return
|
|
}
|
|
|
|
handlers := []handler.Handler4{}
|
|
|
|
// These are the core handlers from our own server struct. The
|
|
// order of these matters in theory, and possibly in practice.
|
|
handlers = append(handlers, obj.leasetimeHandler4)
|
|
handlers = append(handlers, obj.serverIDHandler4)
|
|
handlers = append(handlers, obj.dnsHandler4)
|
|
handlers = append(handlers, obj.routerHandler4)
|
|
//handlers = append(handlers, obj.netmaskHandler4) // in host
|
|
|
|
// These handlers arrive from autogrouping other resources in.
|
|
hostHandlers := []handler.Handler4{}
|
|
rangeHandlers := []handler.Handler4{}
|
|
|
|
// Look through the autogrouped resources!
|
|
// TODO: can we improve performance by only searching here once?
|
|
for _, x := range obj.GetGroup() { // grouped elements
|
|
if obj.init.Debug {
|
|
obj.init.Logf("Got grouped resource: %s", x.String())
|
|
}
|
|
|
|
// TODO: any kind of filtering could go here...
|
|
|
|
switch res := x.(type) { // convert from Res
|
|
case *DHCPHostRes:
|
|
// NOTE: this is not changed if we do send/recv.
|
|
// If we want to support that, we need to patch!
|
|
data := &HostData{
|
|
NBP: obj.NBP,
|
|
}
|
|
h, err := res.handler4(data)
|
|
if err != nil {
|
|
// This should rarely error, so a log
|
|
// message and skipping the handler is
|
|
// good enough here.
|
|
obj.init.Logf("%s: invalid handler: %v", x.String(), err)
|
|
continue
|
|
}
|
|
hostHandlers = append(hostHandlers, h)
|
|
|
|
case *DHCPRangeRes:
|
|
// TODO: should all handlers have the same signature?
|
|
data := &HostData{
|
|
NBP: obj.NBP,
|
|
}
|
|
h, err := res.handler4(data)
|
|
if err != nil {
|
|
// This should rarely error, so a log
|
|
// message and skipping the handler is
|
|
// good enough here.
|
|
obj.init.Logf("%s: invalid handler: %v", x.String(), err)
|
|
continue
|
|
}
|
|
rangeHandlers = append(rangeHandlers, h)
|
|
|
|
default:
|
|
continue
|
|
}
|
|
}
|
|
|
|
// NOTE: the order of these plugins matter, but we'll just
|
|
// hardcode something for now. It could be configurable later.
|
|
|
|
for _, h := range hostHandlers {
|
|
handlers = append(handlers, h)
|
|
}
|
|
for _, h := range rangeHandlers {
|
|
handlers = append(handlers, h)
|
|
}
|
|
|
|
resp = tmp
|
|
for _, handler := range handlers {
|
|
resp, stop = handler(req, resp)
|
|
if stop {
|
|
break
|
|
}
|
|
}
|
|
|
|
if resp != nil {
|
|
if obj.init.Debug {
|
|
// NOTE: This is very useful for debugging!
|
|
obj.init.Logf("sending a DHCPv4 packet: %s", resp.Summary())
|
|
}
|
|
var peer net.Addr
|
|
if !req.GatewayIPAddr.IsUnspecified() {
|
|
// TODO: make RFC8357 compliant
|
|
peer = &net.UDPAddr{IP: req.GatewayIPAddr, Port: dhcpv4.ServerPort}
|
|
} else if resp.MessageType() == dhcpv4.MessageTypeNak {
|
|
peer = &net.UDPAddr{IP: net.IPv4bcast, Port: dhcpv4.ClientPort}
|
|
} else if !req.ClientIPAddr.IsUnspecified() {
|
|
peer = &net.UDPAddr{IP: req.ClientIPAddr, Port: dhcpv4.ClientPort}
|
|
} else if req.IsBroadcast() {
|
|
peer = &net.UDPAddr{IP: net.IPv4bcast, Port: dhcpv4.ClientPort}
|
|
} else {
|
|
// FIXME: we're supposed to unicast to a specific *L2* address, and an L3
|
|
// address that's not yet assigned.
|
|
// I don't know how to do that with this API...
|
|
//peer = &net.UDPAddr{IP: resp.YourIPAddr, Port: dhcpv4.ClientPort}
|
|
obj.init.Logf("handler4: Cannot handle non-broadcast-capable unspecified peers in an RFC-compliant way. Response will be broadcast")
|
|
|
|
peer = &net.UDPAddr{IP: net.IPv4bcast, Port: dhcpv4.ClientPort}
|
|
}
|
|
|
|
if _, err := conn.WriteTo(resp.ToBytes(), peer); err != nil {
|
|
obj.init.Logf("handler4: conn.Write to %v failed: %v", peer, err)
|
|
}
|
|
|
|
} else {
|
|
obj.init.Logf("handler4: dropping request because response is nil")
|
|
}
|
|
|
|
return // department of redundancy department
|
|
}
|
|
}
|
|
|
|
// DHCPHostRes is a representation of a static host assignment in DHCP.
|
|
type DHCPHostRes struct {
|
|
traits.Base // add the base methods without re-implementation
|
|
//traits.Edgeable // XXX: add autoedge support
|
|
traits.Groupable // can be grouped into DHCPServerRes
|
|
|
|
init *engine.Init
|
|
|
|
// Server is the name of the dhcp server resource to group this into. If
|
|
// it is omitted, and there is only a single dhcp resource, then it will
|
|
// be grouped into it automatically. If there is more than one main dhcp
|
|
// resource being used, then the grouping behaviour is *undefined* when
|
|
// this is not specified, and it is not recommended to leave this blank!
|
|
Server string `lang:"server" yaml:"server"`
|
|
|
|
// Mac is the mac address of the host in lower case and separated with
|
|
// colons.
|
|
Mac string `lang:"mac" yaml:"mac"`
|
|
|
|
// IP is the IPv4 address with the CIDR suffix. The suffix is required
|
|
// because it specifies the netmask to be used in the DHCPv4 protocol.
|
|
// For example, you might specify 192.0.2.42/24 which represents a mask
|
|
// of 255.255.255.0 that will be sent.
|
|
IP string `lang:"ip" yaml:"ip"`
|
|
|
|
// NBP is the network boot program URL. This is used for the tftp server
|
|
// name and the boot file name. For example, you might use:
|
|
// tftp://192.0.2.13/pxelinux.0 for a common bios, pxe boot setup. Note
|
|
// that the "scheme" prefix is required, and that it's impossible to
|
|
// specify a file that doesn't begin with a leading slash. If you wish
|
|
// to specify a "root less" file (common for legacy tftp setups) then
|
|
// you can use this feature in conjunction with the NBPPath parameter.
|
|
// For DHCPv4, the scheme must be "tftp".
|
|
NBP string `lang:"nbp" yaml:"nbp"`
|
|
|
|
// NBPPath overrides the path that is sent for the nbp protocols. By
|
|
// default it is taken from parsing a URL in NBP, but this can override
|
|
// that. This is useful if you require a path that doesn't start with a
|
|
// slash. This is sometimes desirable for legacy tftp setups.
|
|
NBPPath string `lang:"nbp_path" yaml:"nbp_path"`
|
|
|
|
ipv4Addr net.IP // XXX: port to netip.Addr
|
|
ipv4Mask net.IPMask
|
|
opt66 *dhcpv4.Option
|
|
opt67 *dhcpv4.Option
|
|
}
|
|
|
|
// Default returns some sensible defaults for this resource.
|
|
func (obj *DHCPHostRes) Default() engine.Res {
|
|
return &DHCPHostRes{}
|
|
}
|
|
|
|
// Validate checks if the resource data structure was populated correctly.
|
|
func (obj *DHCPHostRes) Validate() error {
|
|
if hw, err := net.ParseMAC(obj.Mac); err != nil {
|
|
return errwrap.Wrapf(err, "invalid mac address")
|
|
} else if s := hw.String(); s != obj.Mac {
|
|
// should be all lowercase, for example
|
|
return fmt.Errorf("the mac address is not in the canonical format of: %s", s)
|
|
}
|
|
|
|
ipv4Addr, _, err := net.ParseCIDR(obj.IP)
|
|
if err != nil {
|
|
return errwrap.Wrapf(err, "invalid IP/CIDR address")
|
|
}
|
|
if ipv4Addr.To4() == nil {
|
|
return fmt.Errorf("only IPv4 is currently supported")
|
|
}
|
|
|
|
// If we didn't require the CIDR, then we could do this...
|
|
//if obj.IP != "" && net.ParseIP(obj.IP) == nil {
|
|
// return fmt.Errorf("the IP was not a valid address")
|
|
//}
|
|
//if obj.IP != "" && net.ParseIP(obj.IP).To4() == nil {
|
|
// return fmt.Errorf("only IPv4 is currently supported")
|
|
//}
|
|
|
|
// validate the network boot program URL
|
|
if obj.NBP != "" {
|
|
u, err := url.Parse(obj.NBP)
|
|
if err != nil {
|
|
return errwrap.Wrapf(err, "invalid nbp URL")
|
|
}
|
|
if u.Scheme == "" {
|
|
return fmt.Errorf("missing nbp scheme")
|
|
}
|
|
// TODO: remove this check when we support DHCPv6
|
|
if u.Scheme != "tftp" {
|
|
return fmt.Errorf("the scheme must be `tftp` for DHCPv4")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Init runs some startup code for this resource.
|
|
func (obj *DHCPHostRes) Init(init *engine.Init) error {
|
|
obj.init = init // save for later
|
|
|
|
ipv4Addr, ipv4Net, err := net.ParseCIDR(obj.IP)
|
|
if err != nil {
|
|
return errwrap.Wrapf(err, "unexpected invalid IP/CIDR address")
|
|
}
|
|
if ipv4Addr.To4() == nil {
|
|
return fmt.Errorf("unexpectedly missing an IPv4 address")
|
|
}
|
|
|
|
obj.ipv4Addr = ipv4Addr
|
|
obj.ipv4Mask = ipv4Net.Mask
|
|
|
|
return nil
|
|
}
|
|
|
|
// Cleanup is run by the engine to clean up after the resource is done.
|
|
func (obj *DHCPHostRes) Cleanup() error {
|
|
return nil
|
|
}
|
|
|
|
// Watch is the primary listener for this resource and it outputs events. This
|
|
// particular one does absolutely nothing but block until we've received a done
|
|
// signal.
|
|
func (obj *DHCPHostRes) Watch(ctx context.Context) error {
|
|
obj.init.Running() // when started, notify engine that we're running
|
|
|
|
select {
|
|
case <-ctx.Done(): // closed by the engine to signal shutdown
|
|
}
|
|
|
|
//obj.init.Event() // notify engine of an event (this can block)
|
|
|
|
return nil
|
|
}
|
|
|
|
// CheckApply never has anything to do for this resource, so it always succeeds.
|
|
func (obj *DHCPHostRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
|
|
if obj.init.Debug {
|
|
obj.init.Logf("CheckApply")
|
|
}
|
|
|
|
return true, nil // always succeeds, with nothing to do!
|
|
}
|
|
|
|
// Cmp compares two resources and returns an error if they are not equivalent.
|
|
func (obj *DHCPHostRes) Cmp(r engine.Res) error {
|
|
// we can only compare DHCPHostRes to others of the same resource kind
|
|
res, ok := r.(*DHCPHostRes)
|
|
if !ok {
|
|
return fmt.Errorf("res is not the same kind")
|
|
}
|
|
|
|
if obj.Server != res.Server {
|
|
return fmt.Errorf("the Server field differs")
|
|
}
|
|
if obj.Mac != res.Mac {
|
|
return fmt.Errorf("the Mac differs")
|
|
}
|
|
if obj.IP != res.IP {
|
|
return fmt.Errorf("the IP differs")
|
|
}
|
|
if obj.NBP != res.NBP {
|
|
return fmt.Errorf("the NBP differs")
|
|
}
|
|
if obj.NBPPath != res.NBPPath {
|
|
return fmt.Errorf("the NBPPath differs")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
|
// primarily useful for setting the defaults.
|
|
func (obj *DHCPHostRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
type rawRes DHCPHostRes // indirection to avoid infinite recursion
|
|
|
|
def := obj.Default() // get the default
|
|
res, ok := def.(*DHCPHostRes) // put in the right format
|
|
if !ok {
|
|
return fmt.Errorf("could not convert to DHCPHostRes")
|
|
}
|
|
raw := rawRes(*res) // convert; the defaults go here
|
|
|
|
if err := unmarshal(&raw); err != nil {
|
|
return err
|
|
}
|
|
|
|
*obj = DHCPHostRes(raw) // restore from indirection with type conversion!
|
|
return nil
|
|
}
|
|
|
|
// handler4 returns the handler for the host resource. It gets called from the
|
|
// main handler4 function in the dhcp server resource. This combines the concept
|
|
// of multiple "plugins" inside of coredhcp. It includes "file" and also "nbp"
|
|
// and others.
|
|
func (obj *DHCPHostRes) handler4(data *HostData) (func(*dhcpv4.DHCPv4, *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool), error) {
|
|
nbp := ""
|
|
if data != nil {
|
|
nbp = data.NBP // from server
|
|
}
|
|
if obj.NBP != "" { // host-specific override
|
|
nbp = obj.NBP
|
|
}
|
|
result, err := url.Parse(nbp)
|
|
if err != nil {
|
|
// this should have been checked in Validate :/
|
|
return nil, errwrap.Wrapf(err, "unexpected invalid nbp URL")
|
|
}
|
|
otsn := dhcpv4.OptTFTPServerName(result.Host)
|
|
obj.opt66 = &otsn
|
|
p := result.Path
|
|
if obj.NBPPath != "" { // override the path if this is specified
|
|
p = obj.NBPPath
|
|
}
|
|
obfn := dhcpv4.OptBootFileName(p)
|
|
if p != "" {
|
|
obj.opt67 = &obfn
|
|
}
|
|
|
|
return func(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
|
|
// Might not be *this* reservation, but another one...
|
|
if obj.init.Debug {
|
|
obj.init.Logf("comparing mac %s to %s", req.ClientHWAddr.String(), obj.Mac)
|
|
}
|
|
if req.ClientHWAddr.String() != obj.Mac {
|
|
//obj.init.Logf("MAC address %s is unknown", req.ClientHWAddr.String())
|
|
return resp, false
|
|
}
|
|
if obj.init.Debug || true { // TODO: should we silence this?
|
|
obj.init.Logf("found IP address %s for MAC %s", obj.IP, req.ClientHWAddr.String())
|
|
}
|
|
|
|
resp.YourIPAddr = obj.ipv4Addr // TODO: make a copy for this?
|
|
|
|
// XXX: This is done in the standalone leasetime handler for now
|
|
//resp.Options.Update(dhcpv4.OptIPAddressLeaseTime(obj.LeaseTime.Round(time.Second)))
|
|
|
|
// XXX: https://tools.ietf.org/html/rfc2132#section-3.3
|
|
// If both the subnet mask and the router option are specified
|
|
// in a DHCP reply, the subnet mask option MUST be first.
|
|
// If mask is not set, some DHCP clients will DECLINE.
|
|
resp.Options.Update(dhcpv4.OptSubnetMask(obj.ipv4Mask)) // net.IPMask
|
|
|
|
// nbp section
|
|
if obj.opt66 != nil && req.IsOptionRequested(dhcpv4.OptionTFTPServerName) {
|
|
resp.Options.Update(*obj.opt66)
|
|
}
|
|
if obj.opt67 != nil && req.IsOptionRequested(dhcpv4.OptionBootfileName) {
|
|
resp.Options.Update(*obj.opt67)
|
|
}
|
|
if obj.init.Debug {
|
|
obj.init.Logf("Added NBP %s / %s to request", obj.opt66, obj.opt67)
|
|
}
|
|
|
|
return resp, true
|
|
}, nil
|
|
}
|
|
|
|
// DHCPRangeRes is a representation of a range allocator in DHCP. To declare a
|
|
// range you must specify either the `network` field or the `from` and `to`
|
|
// fields as ip with cidr's, or `from` and `to` fields without cidr's but with
|
|
// the `mask` field as either a dotted netmask or a `/number` field. If you
|
|
// specify none of these, then the resource name will be interpreted the same
|
|
// way that the `network` field os. The last ip in the range (which is often
|
|
// used as a broadcast address) is never allocated.
|
|
// TODO: Add a setting to determine if we should allocate the last address.
|
|
type DHCPRangeRes struct {
|
|
traits.Base // add the base methods without re-implementation
|
|
//traits.Edgeable // XXX: add autoedge support?
|
|
traits.Groupable // can be grouped into DHCPServerRes
|
|
|
|
init *engine.Init
|
|
|
|
// Server is the name of the dhcp server resource to group this into. If
|
|
// it is omitted, and there is only a single dhcp resource, then it will
|
|
// be grouped into it automatically. If there is more than one main dhcp
|
|
// resource being used, then the grouping behaviour is *undefined* when
|
|
// this is not specified, and it is not recommended to leave this blank!
|
|
Server string `lang:"server" yaml:"server"`
|
|
|
|
// MacMatch only allocates ip addresses if the mac address matches this
|
|
// wildcard pattern. The default pattern of the empty string, means any
|
|
// mac address is permitted.
|
|
// TODO: This is not implemented at the moment.
|
|
// TODO: Consider implementing this sort of functionality.
|
|
// TODO: Can we use https://pkg.go.dev/path/filepath#Match ?
|
|
//MacMatch string `lang:"macmatch" yaml:"macmatch"`
|
|
|
|
// Network is the network number and cidr to determine the range. For
|
|
// example, the common network range of 192.168.42.1 to 192.168.42.255
|
|
// should have a network field here of 192.168.42.0/24. You can either
|
|
// specify this field or `from` and `to`, but not a different
|
|
// combination. If you don't specify any of these fields, then the
|
|
// resource name will be parsed as if it was used here.
|
|
Network string `lang:"network" yaml:"network"`
|
|
|
|
// From is the start address in the range inclusive. If it is specified
|
|
// in cidr notation, then the `mask` field must not be used. Otherwise
|
|
// it must be used. In both situations the cidr or mask must be
|
|
// consistent with the `to` field. If this field is used, you must not
|
|
// use the `network` field.
|
|
From string `lang:"from" yaml:"from"`
|
|
|
|
// To is the end address in the range inclusive. If it is specified in
|
|
// cidr notation, then the `mask` field must not be used. Otherwise it
|
|
// must be used. In both situations the cidr or mask must be consistent
|
|
// with the `from` field. If this field is used, you must not use the
|
|
// `network` field.
|
|
To string `lang:"to" yaml:"to"`
|
|
|
|
// Mask is the cidr or netmask of ip addresses in the specified range.
|
|
// This field must only be used if both `from` and `to` are specified,
|
|
// and if neither of them specify a cidr suffix. If neither do, then the
|
|
// mask here can be in either dotted format or, preferably, in cidr
|
|
// format by starting with a slash.
|
|
Mask string `lang:"mask" yaml:"mask"`
|
|
|
|
// Skip is a list ip's in either cidr or standalone representation which
|
|
// will be skipped and not allocated.
|
|
Skip []string `lang:"skip" yaml:"skip"`
|
|
|
|
// Persist should be true if you want to persist the lease information
|
|
// to disk so that a new (or changed) invocation of this resource with
|
|
// the same name, will regain that existing initial state at startup.
|
|
// TODO: Add a new param to persist the data to etcd in the world API so
|
|
// that we could have redundant dhcp servers which share the same state.
|
|
// This would require having a distributed allocator through etcd too!
|
|
// TODO: Consider adding a new param to erase the persisted record
|
|
// database if any field param changes, as opposed to just looking at
|
|
// the name field alone.
|
|
// XXX: This is currently not implemented.
|
|
Persist bool `lang:"persist" yaml:"persist"`
|
|
|
|
// NBP is the network boot program URL. This is used for the tftp server
|
|
// name and the boot file name. For example, you might use:
|
|
// tftp://192.0.2.13/pxelinux.0 for a common bios, pxe boot setup. Note
|
|
// that the "scheme" prefix is required, and that it's impossible to
|
|
// specify a file that doesn't begin with a leading slash. If you wish
|
|
// to specify a "root less" file (common for legacy tftp setups) then
|
|
// you can use this feature in conjunction with the NBPPath parameter.
|
|
// For DHCPv4, the scheme must be "tftp".
|
|
NBP string `lang:"nbp" yaml:"nbp"`
|
|
|
|
// NBPPath overrides the path that is sent for the nbp protocols. By
|
|
// default it is taken from parsing a URL in NBP, but this can override
|
|
// that. This is useful if you require a path that doesn't start with a
|
|
// slash. This is sometimes desirable for legacy tftp setups.
|
|
NBPPath string `lang:"nbp_path" yaml:"nbp_path"`
|
|
|
|
// TODO: consider persisting this to disk (with the Local API)
|
|
records map[string]*HostRecord // key is mac address
|
|
|
|
// TODO: port the allocator to use the net/netip types
|
|
// TODO: add a new allocator that can work on multiple hosts over etcd
|
|
allocator allocators.Allocator
|
|
|
|
// mutex guards access to records and allocator when running.
|
|
mutex *sync.Mutex
|
|
|
|
from netip.Addr
|
|
to netip.Addr
|
|
mask net.IPMask
|
|
skip []netip.Addr
|
|
|
|
opt66 *dhcpv4.Option
|
|
opt67 *dhcpv4.Option
|
|
}
|
|
|
|
// Default returns some sensible defaults for this resource.
|
|
func (obj *DHCPRangeRes) Default() engine.Res {
|
|
return &DHCPRangeRes{}
|
|
}
|
|
|
|
// parse handles the permutations of options to parse the fields into our data
|
|
// format that we use. This is a helper function because it is used in both
|
|
// Validate and also Init. It only stores the result on no error, and if set is
|
|
// true.
|
|
func (obj *DHCPRangeRes) parse(set bool) error {
|
|
if a, b := obj.From == "", obj.To == ""; a != b {
|
|
return fmt.Errorf("must specify both from and to or neither")
|
|
}
|
|
|
|
if obj.From != "" && obj.To != "" {
|
|
var from, to netip.Addr
|
|
var mask net.IPMask // nil unless set
|
|
|
|
if prefix, err := netip.ParsePrefix(obj.From); err == nil {
|
|
from = prefix.Addr()
|
|
|
|
ones := prefix.Bits() // set portion of the mask, -1 if invalid
|
|
bits := 128 // ipv6
|
|
if from.Is4() { // ipv4
|
|
bits = 32
|
|
}
|
|
mask = net.CIDRMask(ones, bits) // net.IPMask
|
|
|
|
} else if addr, err := netip.ParseAddr(obj.From); err == nil { // without cidr
|
|
from = addr
|
|
}
|
|
// else we error (caught below)
|
|
if prefix, err := netip.ParsePrefix(obj.To); err == nil {
|
|
to = prefix.Addr()
|
|
|
|
ones := prefix.Bits() // set portion of the mask, -1 if invalid
|
|
bits := 128 // ipv6
|
|
if to.Is4() { // ipv4
|
|
bits = 32
|
|
}
|
|
mask = net.CIDRMask(ones, bits) // net.IPMask
|
|
|
|
} else if addr, err := netip.ParseAddr(obj.To); err == nil { // without cidr
|
|
to = addr
|
|
}
|
|
// else we error (caught below)
|
|
|
|
if !from.Is4() || !to.Is4() {
|
|
// TODO: support ipv6
|
|
return fmt.Errorf("only ipv4 is supported at this time")
|
|
}
|
|
|
|
if a, b := obj.Mask == "", mask == nil; a == b {
|
|
return fmt.Errorf("mask must be specified somehow")
|
|
}
|
|
|
|
// weird "/cidr" form
|
|
// TODO: Do we want to allow this form?
|
|
if obj.Mask != "" && strings.HasPrefix(obj.Mask, "/") {
|
|
|
|
ones, err := strconv.Atoi(obj.Mask[1:])
|
|
if err != nil {
|
|
return fmt.Errorf("invalid cidr suffix: %s", obj.Mask)
|
|
}
|
|
|
|
// The range is [0,32] for IPv4 or [0,128] for IPv6.
|
|
if ones < 0 || ones > 128 {
|
|
return fmt.Errorf("invalid cidr: %s", obj.Mask)
|
|
}
|
|
|
|
bits := 128 // ipv6
|
|
if from.Is4() && to.Is4() { // ipv4
|
|
if ones > 32 {
|
|
return fmt.Errorf("invalid ipv4 cidr: %s", obj.Mask)
|
|
}
|
|
bits = 32
|
|
}
|
|
mask = net.CIDRMask(ones, bits) // net.IPMask
|
|
|
|
} else if obj.Mask != "" { // standard 255.255.255.0 form
|
|
|
|
ip := net.ParseIP(obj.Mask)
|
|
if ip == nil || ip.IsUnspecified() {
|
|
return fmt.Errorf("invalid mask: %s", obj.Mask)
|
|
}
|
|
ip4 := ip.To4()
|
|
if ip4 == nil {
|
|
return fmt.Errorf("invalid ipv4 mask: %s", obj.Mask)
|
|
}
|
|
mask = net.IPv4Mask(ip4[0], ip4[1], ip4[2], ip4[3])
|
|
}
|
|
|
|
if !checkValidNetmask(mask) { // TODO: is this needed?
|
|
return fmt.Errorf("invalid mask: %s", mask)
|
|
}
|
|
|
|
if !set {
|
|
return nil
|
|
}
|
|
|
|
r := netipx.IPRangeFrom(from, to) // netipx.IPRange
|
|
obj.from = r.From() // netip.Addr
|
|
obj.to = r.To() // netip.Addr
|
|
obj.mask = mask
|
|
|
|
return nil
|
|
}
|
|
|
|
if obj.Mask != "" {
|
|
return fmt.Errorf("mask must not be set when using network")
|
|
}
|
|
|
|
if obj.Network != "" {
|
|
return obj.parseNetwork(set, obj.Network)
|
|
}
|
|
|
|
// try to parse based on name if nothing else is possible
|
|
return obj.parseNetwork(set, obj.Name())
|
|
}
|
|
|
|
// parseNetwork handles the permutations of options to parse the network field
|
|
// into the data format that we use. This is a helper function because it is
|
|
// used in both Validate and also Init for both the network field and the name
|
|
// field. It only stores the result on no error, and if set is true.
|
|
func (obj *DHCPRangeRes) parseNetwork(set bool, network string) error {
|
|
if network == "" {
|
|
return fmt.Errorf("empty network")
|
|
}
|
|
|
|
prefix, err := netip.ParsePrefix(network)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ones := prefix.Bits() // set portion of the mask, -1 if invalid
|
|
bits := 128 // ipv6
|
|
if prefix.Addr().Is4() { // ipv4
|
|
bits = 32
|
|
}
|
|
mask := net.CIDRMask(ones, bits) // net.IPMask
|
|
|
|
if !checkValidNetmask(mask) { // TODO: is this needed?
|
|
return fmt.Errorf("invalid mask: %s", mask)
|
|
}
|
|
|
|
// XXX: Are we doing the network math here correctly? (eg with .Next())
|
|
r := netipx.RangeOfPrefix(prefix) // netipx.IPRange
|
|
from := r.From() // netip.Addr
|
|
next := from.Next() // skip the network addr
|
|
if !next.IsValid() {
|
|
return fmt.Errorf("not enough addresses") // or a bug?
|
|
}
|
|
|
|
if !set {
|
|
return nil
|
|
}
|
|
|
|
obj.from = next // netip.Addr
|
|
// TODO: should we use .Prev() for to?
|
|
obj.to = r.To() // netip.Addr
|
|
obj.mask = mask
|
|
|
|
return nil
|
|
}
|
|
|
|
// parseSkip handles the permutations of options to parse the skip field into
|
|
// the data format that we use. This is a helper function because it is used in
|
|
// both Validate and also Init. It only stores the result on no error, and if
|
|
// set is true.
|
|
func (obj *DHCPRangeRes) parseSkip(set bool) error {
|
|
ips := []netip.Addr{}
|
|
for _, s := range obj.Skip {
|
|
if prefix, err := netip.ParsePrefix(s); err == nil {
|
|
addr := prefix.Addr()
|
|
ips = append(ips, addr)
|
|
|
|
// TODO: check if mask matches mask from range?
|
|
//ones := prefix.Bits() // set portion of the mask, -1 if invalid
|
|
//bits := 128 // ipv6
|
|
//if addr.Is4() { // ipv4
|
|
// bits = 32
|
|
//}
|
|
//mask = net.CIDRMask(ones, bits) // net.IPMask
|
|
continue
|
|
|
|
} else if addr, err := netip.ParseAddr(s); err == nil { // without cidr
|
|
ips = append(ips, addr)
|
|
continue
|
|
}
|
|
|
|
return fmt.Errorf("invalid ip: %s", s)
|
|
}
|
|
|
|
if !set {
|
|
return nil
|
|
}
|
|
|
|
obj.skip = ips
|
|
|
|
return nil
|
|
}
|
|
|
|
// Validate checks if the resource data structure was populated correctly.
|
|
func (obj *DHCPRangeRes) Validate() error {
|
|
|
|
//if obj.MacMatch != "" {
|
|
// TODO: validate pattern
|
|
//}
|
|
|
|
// If input is false, this doesn't modify any private struct fields.
|
|
if err := obj.parse(false); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := obj.parseSkip(false); err != nil {
|
|
return err
|
|
}
|
|
|
|
// validate the network boot program URL
|
|
if obj.NBP != "" {
|
|
u, err := url.Parse(obj.NBP)
|
|
if err != nil {
|
|
return errwrap.Wrapf(err, "invalid nbp URL")
|
|
}
|
|
if u.Scheme == "" {
|
|
return fmt.Errorf("missing nbp scheme")
|
|
}
|
|
// TODO: remove this check when we support DHCPv6
|
|
if u.Scheme != "tftp" {
|
|
return fmt.Errorf("the scheme must be `tftp` for DHCPv4")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Init runs some startup code for this resource.
|
|
func (obj *DHCPRangeRes) Init(init *engine.Init) error {
|
|
obj.init = init // save for later
|
|
|
|
// This sets some private struct fields if it doesn't error.
|
|
if err := obj.parse(true); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := obj.parseSkip(true); err != nil {
|
|
return err
|
|
}
|
|
|
|
obj.records = make(map[string]*HostRecord)
|
|
// TODO: consider persisting this to disk
|
|
if obj.Persist {
|
|
return fmt.Errorf("persist not implemented")
|
|
//records, err := obj.load()
|
|
//if err != nil {
|
|
// return nil
|
|
//}
|
|
//obj.records = records
|
|
}
|
|
|
|
from := net.IP(obj.from.AsSlice()) // netip.Addr -> net.IP
|
|
to := net.IP(obj.to.AsSlice())
|
|
allocator, err := bitmap.NewIPv4Allocator(from, to)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
obj.allocator = allocator
|
|
obj.mutex = &sync.Mutex{}
|
|
|
|
res, ok := obj.Parent().(*DHCPServerRes)
|
|
if !ok {
|
|
// programming error
|
|
return fmt.Errorf("unexpected parent resource")
|
|
}
|
|
// TODO: res.reservedMutex?
|
|
for addr := range res.reservedIPs {
|
|
ip := net.IP(addr.AsSlice()) // netip.Addr -> net.IP
|
|
hint := net.IPNet{IP: ip}
|
|
ipNet, err := obj.allocator.Allocate(hint) // (net.IPNet, error)
|
|
if err != nil {
|
|
return fmt.Errorf("could not reserve ip %s: %v", ip, err)
|
|
}
|
|
if ip.String() != ipNet.IP.String() {
|
|
return fmt.Errorf("requested %s, allocator returned %s", ip, ipNet.IP)
|
|
}
|
|
|
|
// NOTE: We don't add these to the memory or stateful record
|
|
// store, since we have these "stored" by virtue of them being a
|
|
// resource as code. If those changed, we'd have an out-of-date
|
|
// stateful database!
|
|
}
|
|
|
|
macs := []string{}
|
|
for k := range obj.records { // deterministic for now
|
|
macs = append(macs, k)
|
|
}
|
|
sort.Strings(macs)
|
|
for _, mac := range macs {
|
|
record, ok := obj.records[mac]
|
|
if !ok {
|
|
// programming error
|
|
return fmt.Errorf("missing record")
|
|
}
|
|
if !record.IP.IsValid() || record.IP.IsUnspecified() {
|
|
// programming error
|
|
return fmt.Errorf("bad ip in record")
|
|
}
|
|
|
|
// Allocate what we already chose to statically in the database.
|
|
// NOTE: The API lets us request an ip, but it's not guaranteed.
|
|
hint := net.IPNet{IP: net.IP(record.IP.AsSlice())} // netip.Addr -> net.IP
|
|
ipNet, err := obj.allocator.Allocate(hint) // (net.IPNet, error)
|
|
if err != nil {
|
|
return fmt.Errorf("could not re-allocate leased ip %s: %v", record.IP, err)
|
|
}
|
|
if record.IP.String() != ipNet.IP.String() {
|
|
return fmt.Errorf("pre-requested %s, allocator returned %s", record.IP, ipNet.IP)
|
|
}
|
|
}
|
|
|
|
for _, ip := range obj.skip {
|
|
// Allocate what the config already chose to skip statically.
|
|
// NOTE: The API lets us request an ip, but it's not guaranteed.
|
|
hint := net.IPNet{IP: net.IP(ip.AsSlice())} // netip.Addr -> net.IP
|
|
ipNet, err := obj.allocator.Allocate(hint) // (net.IPNet, error)
|
|
if err != nil {
|
|
return fmt.Errorf("could not allocate skip ip %s: %v", ip, err)
|
|
}
|
|
if ip.String() != ipNet.IP.String() {
|
|
return fmt.Errorf("skip-requested %s, allocator returned %s", ip, ipNet.IP)
|
|
}
|
|
}
|
|
|
|
obj.init.Logf("from: %s", obj.from)
|
|
obj.init.Logf(" to: %s", obj.to)
|
|
obj.init.Logf("mask: %s", netmaskAsQuadString(obj.mask))
|
|
|
|
return nil
|
|
}
|
|
|
|
// Cleanup is run by the engine to clean up after the resource is done.
|
|
func (obj *DHCPRangeRes) Cleanup() error {
|
|
return nil
|
|
}
|
|
|
|
// Watch is the primary listener for this resource and it outputs events. This
|
|
// particular one does absolutely nothing but block until we've received a done
|
|
// signal.
|
|
func (obj *DHCPRangeRes) Watch(ctx context.Context) error {
|
|
obj.init.Running() // when started, notify engine that we're running
|
|
|
|
select {
|
|
case <-ctx.Done(): // closed by the engine to signal shutdown
|
|
}
|
|
|
|
//obj.init.Event() // notify engine of an event (this can block)
|
|
|
|
return nil
|
|
}
|
|
|
|
// CheckApply never has anything to do for this resource, so it always succeeds.
|
|
func (obj *DHCPRangeRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
|
|
if obj.init.Debug {
|
|
obj.init.Logf("CheckApply")
|
|
}
|
|
|
|
return true, nil // always succeeds, with nothing to do!
|
|
}
|
|
|
|
// Cmp compares two resources and returns an error if they are not equivalent.
|
|
func (obj *DHCPRangeRes) Cmp(r engine.Res) error {
|
|
// we can only compare DHCPRangeRes to others of the same resource kind
|
|
res, ok := r.(*DHCPRangeRes)
|
|
if !ok {
|
|
return fmt.Errorf("res is not the same kind")
|
|
}
|
|
|
|
if obj.Server != res.Server {
|
|
return fmt.Errorf("the Server field differs")
|
|
}
|
|
|
|
//if obj.MacMatch != res.MacMatch {
|
|
// return fmt.Errorf("the MacMatch field differs")
|
|
//}
|
|
|
|
if obj.Network != res.Network {
|
|
return fmt.Errorf("the Network field differs")
|
|
}
|
|
if obj.From != res.From {
|
|
return fmt.Errorf("the From field differs")
|
|
}
|
|
if obj.To != res.To {
|
|
return fmt.Errorf("the To field differs")
|
|
}
|
|
if obj.Mask != res.Mask {
|
|
return fmt.Errorf("the Mask field differs")
|
|
}
|
|
|
|
if len(obj.Skip) != len(res.Skip) {
|
|
return fmt.Errorf("the size of Skip differs")
|
|
}
|
|
for i, x := range obj.Skip {
|
|
if x != res.Skip[i] {
|
|
return fmt.Errorf("the Skip at index %d differs", i)
|
|
}
|
|
}
|
|
|
|
if obj.Persist != res.Persist {
|
|
return fmt.Errorf("the Persist field differs")
|
|
}
|
|
|
|
if obj.NBP != res.NBP {
|
|
return fmt.Errorf("the NBP differs")
|
|
}
|
|
if obj.NBPPath != res.NBPPath {
|
|
return fmt.Errorf("the NBPPath differs")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
|
// primarily useful for setting the defaults.
|
|
func (obj *DHCPRangeRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
type rawRes DHCPRangeRes // indirection to avoid infinite recursion
|
|
|
|
def := obj.Default() // get the default
|
|
res, ok := def.(*DHCPRangeRes) // put in the right format
|
|
if !ok {
|
|
return fmt.Errorf("could not convert to DHCPRangeRes")
|
|
}
|
|
raw := rawRes(*res) // convert; the defaults go here
|
|
|
|
if err := unmarshal(&raw); err != nil {
|
|
return err
|
|
}
|
|
|
|
*obj = DHCPRangeRes(raw) // restore from indirection with type conversion!
|
|
return nil
|
|
}
|
|
|
|
// handler4 returns the handler for the range resource. It gets called from the
|
|
// main handler4 function in the dhcp server resource. This combines the concept
|
|
// of multiple "plugins" inside of coredhcp. It includes "file" and also "nbp"
|
|
// and others.
|
|
func (obj *DHCPRangeRes) handler4(data *HostData) (func(*dhcpv4.DHCPv4, *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool), error) {
|
|
nbp := ""
|
|
if data != nil {
|
|
nbp = data.NBP // from server
|
|
}
|
|
if obj.NBP != "" { // host-specific override
|
|
nbp = obj.NBP
|
|
}
|
|
result, err := url.Parse(nbp)
|
|
if err != nil {
|
|
// this should have been checked in Validate :/
|
|
return nil, errwrap.Wrapf(err, "unexpected invalid nbp URL")
|
|
}
|
|
otsn := dhcpv4.OptTFTPServerName(result.Host)
|
|
obj.opt66 = &otsn
|
|
p := result.Path
|
|
if obj.NBPPath != "" { // override the path if this is specified
|
|
p = obj.NBPPath
|
|
}
|
|
obfn := dhcpv4.OptBootFileName(p)
|
|
if p != "" {
|
|
obj.opt67 = &obfn
|
|
}
|
|
|
|
res, ok := obj.Parent().(*DHCPServerRes)
|
|
if !ok {
|
|
// programming error
|
|
return nil, fmt.Errorf("unexpected parent resource")
|
|
}
|
|
|
|
leaseTime := res.leaseTime
|
|
|
|
// FIXME: Run this somewhere for now, eventually it should get scheduled
|
|
// to run in the returned duration of time. This way, it would clean old
|
|
// peristed entries when they're stale, not when a new request comes in.
|
|
if _, err := obj.leaseClean(); err != nil {
|
|
return nil, errwrap.Wrapf(err, "clean error")
|
|
}
|
|
|
|
return func(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
|
|
obj.mutex.Lock()
|
|
defer obj.mutex.Unlock()
|
|
|
|
mac := req.ClientHWAddr.String()
|
|
//hostname := req.HostName() // TODO: is it needed in the record?
|
|
|
|
// Incoming allocation request in our range.
|
|
if obj.init.Debug {
|
|
obj.init.Logf("range lookup for mac %s", mac)
|
|
}
|
|
|
|
update := false
|
|
record, ok := obj.records[mac]
|
|
if !ok {
|
|
ipNet, err := obj.allocator.Allocate(net.IPNet{}) // (net.IPNet, error)
|
|
if err != nil {
|
|
obj.init.Logf("could not allocate for mac %s: %v", mac, err)
|
|
return nil, true
|
|
}
|
|
// TODO: Do we need to use ipNet.Mask ?
|
|
|
|
// TODO: do we want this complex version instead?
|
|
//addr, ok := netipx.FromStdIP(ipNet.IP) // unmap's
|
|
addr, ok := netip.AddrFromSlice(ipNet.IP) // net.IP -> netip.Addr
|
|
if !ok {
|
|
// programming error
|
|
obj.init.Logf("could not convert ip: %s", ipNet.IP)
|
|
return nil, true
|
|
}
|
|
|
|
rec := &HostRecord{
|
|
IP: addr,
|
|
Expires: int(time.Now().Add(leaseTime).Unix()),
|
|
//Hostname: hostname,
|
|
}
|
|
|
|
obj.records[mac] = rec
|
|
record = rec // set it
|
|
update = true
|
|
} else {
|
|
// extend lease
|
|
expiry := time.Unix(int64(record.Expires), 0) // 0 is nsec
|
|
if expiry.Before(time.Now().Add(leaseTime)) {
|
|
record.Expires = int(time.Now().Add(leaseTime).Round(time.Second).Unix())
|
|
//record.Hostname = hostname
|
|
update = true
|
|
}
|
|
}
|
|
|
|
// TODO: consider persisting this to disk
|
|
if obj.Persist && update {
|
|
//if err = obj.store(req.ClientHWAddr, record); err != nil {
|
|
// obj.init.Logf("could not store mac %s: %v", mac, err)
|
|
//}
|
|
}
|
|
|
|
if obj.init.Debug || true { // TODO: should we silence this?
|
|
obj.init.Logf("allocated ip %s for MAC %s", record.IP, mac)
|
|
}
|
|
|
|
resp.YourIPAddr = net.IP(record.IP.AsSlice()) // netip.Addr -> net.IP
|
|
|
|
// XXX: This is done in the standalone leasetime handler for now
|
|
//resp.Options.Update(dhcpv4.OptIPAddressLeaseTime(record.Expires.Round(time.Second)))
|
|
|
|
// XXX: https://tools.ietf.org/html/rfc2132#section-3.3
|
|
// If both the subnet mask and the router option are specified
|
|
// in a DHCP reply, the subnet mask option MUST be first.
|
|
// If mask is not set, some DHCP clients will DECLINE.
|
|
resp.Options.Update(dhcpv4.OptSubnetMask(obj.mask)) // net.IPMask
|
|
|
|
// nbp section
|
|
if obj.opt66 != nil && req.IsOptionRequested(dhcpv4.OptionTFTPServerName) {
|
|
resp.Options.Update(*obj.opt66)
|
|
}
|
|
if obj.opt67 != nil && req.IsOptionRequested(dhcpv4.OptionBootfileName) {
|
|
resp.Options.Update(*obj.opt67)
|
|
}
|
|
if obj.init.Debug {
|
|
obj.init.Logf("Added NBP %s / %s to request", obj.opt66, obj.opt67)
|
|
}
|
|
|
|
return resp, true
|
|
}, nil
|
|
}
|
|
|
|
// leaseClean frees any expired leases. It also returns the duration till the
|
|
// next expected cleaning.
|
|
func (obj *DHCPRangeRes) leaseClean() (time.Duration, error) {
|
|
obj.mutex.Lock()
|
|
defer obj.mutex.Unlock()
|
|
|
|
min := time.Duration(-1)
|
|
now := time.Now()
|
|
expire := []string{}
|
|
for mac, record := range obj.records { // see who is expired...
|
|
expiry := time.Unix(int64(record.Expires), 0) // 0 is nsec
|
|
//if !expiry.After(now) // same
|
|
if delta := expiry.Sub(now); delta > 0 { // positive if expired
|
|
if min == -1 { // initialize
|
|
min = delta
|
|
}
|
|
min = minDuration(min, delta) // soonest time to wakeup
|
|
continue
|
|
}
|
|
|
|
// it's expired
|
|
expire = append(expire, mac)
|
|
}
|
|
|
|
for _, mac := range expire {
|
|
record, exists := obj.records[mac]
|
|
if !exists {
|
|
// programming error
|
|
return 0, fmt.Errorf("missing record")
|
|
}
|
|
free := net.IPNet{IP: net.IP(record.IP.AsSlice())} // netip.Addr -> net.IP
|
|
err := obj.allocator.Free(free)
|
|
delete(obj.records, mac)
|
|
obj.init.Logf("unallocated (free) ip %s for MAC %s", record.IP, mac)
|
|
// TODO: run the persist somewhere...
|
|
// if obj.Persist {
|
|
//
|
|
//}
|
|
if err == nil {
|
|
continue
|
|
}
|
|
if dblErr, ok := err.(*allocators.ErrDoubleFree); ok {
|
|
// programming error
|
|
obj.init.Logf("double free programming error on: %v", dblErr.Loc)
|
|
continue
|
|
}
|
|
return 0, err // actual unknown error
|
|
}
|
|
|
|
//if min >= 0 // schedule in `min * time.Second` seconds
|
|
return min, nil
|
|
}
|
|
|
|
// HostRecord is some information that we store about each reservation that we
|
|
// allocated. This struct is stored as a value which is mapped to by a mac
|
|
// address key, which is why the mac address is not stored in this record.
|
|
type HostRecord struct {
|
|
IP netip.Addr
|
|
|
|
// Expires represents the number of seconds since the epoch that this
|
|
// lease expires at.
|
|
Expires int
|
|
}
|
|
|
|
// HostData is some data that each host will get made available to its handler.
|
|
type HostData struct {
|
|
// NBP is the network boot program URL. See the resources for more docs.
|
|
NBP string
|
|
}
|
|
|
|
// overEngineeredLogger is a helper struct that fulfills the over-engineered
|
|
// logging interface that was introduced in:
|
|
// https://github.com/insomniacslk/dhcp/pull/371/
|
|
type overEngineeredLogger struct {
|
|
logf func(format string, v ...interface{})
|
|
}
|
|
|
|
func (obj *overEngineeredLogger) Printf(format string, v ...interface{}) {
|
|
obj.logf(format, v...)
|
|
}
|
|
|
|
func (obj *overEngineeredLogger) PrintMessage(prefix string, message *dhcpv4.DHCPv4) {
|
|
obj.logf("%s: %s", prefix, message)
|
|
}
|
|
|
|
func minDuration(d1, d2 time.Duration) time.Duration {
|
|
if d1 < d2 {
|
|
return d1
|
|
}
|
|
return d2
|
|
}
|
|
|
|
// from: https://github.com/coredhcp/coredhcp/blob/master/plugins/netmask/plugin.go
|
|
func checkValidNetmask(netmask net.IPMask) bool {
|
|
netmaskInt := binary.BigEndian.Uint32(netmask)
|
|
x := ^netmaskInt
|
|
y := x + 1
|
|
return (y & x) == 0
|
|
}
|
|
|
|
// netmaskAsQuadString returns a dotted-quad string giving you something like:
|
|
// 255.255.255.0 instead of ffffff00 which is what's seen when you print it now.
|
|
func netmaskAsQuadString(netmask net.IPMask) string {
|
|
return fmt.Sprintf("%d.%d.%d.%d", netmask[0], netmask[1], netmask[2], netmask[3])
|
|
}
|