engine: resources: Add a dhcp range resource
This adds the ability to offer a dhcp lease to someone when we don't know their mac address in advance. This also uses the extended autogrouping API to keep the internal API simpler.
This commit is contained in:
@@ -31,10 +31,15 @@ package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -43,14 +48,17 @@ import (
|
||||
"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{} })
|
||||
engine.RegisterResource("dhcp:range", func() engine.Res { return &DHCPRangeRes{} })
|
||||
|
||||
if _, err := time.ParseDuration(DHCPDefaultLeaseTime); err != nil {
|
||||
panic("invalid duration for DHCPDefaultLeaseTime constant")
|
||||
@@ -143,6 +151,10 @@ type DHCPServerRes struct {
|
||||
|
||||
//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?
|
||||
}
|
||||
|
||||
@@ -399,6 +411,32 @@ func (obj *DHCPServerRes) Init(init *engine.Init) error {
|
||||
//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
|
||||
}
|
||||
|
||||
@@ -717,17 +755,17 @@ func (obj *DHCPServerRes) GroupCmp(r engine.GroupableRes) error {
|
||||
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")
|
||||
// }
|
||||
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 nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("resource is not the right kind")
|
||||
}
|
||||
@@ -897,12 +935,24 @@ func (obj *DHCPServerRes) handler4() func(net.PacketConn, net.Addr, *dhcpv4.DHCP
|
||||
// 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:
|
||||
// h := res.handler4()
|
||||
// rangeHandlers = append(rangeHandlers, 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
|
||||
@@ -1004,7 +1054,7 @@ type DHCPHostRes struct {
|
||||
// slash. This is sometimes desirable for legacy tftp setups.
|
||||
NBPPath string `lang:"nbp_path" yaml:"nbp_path"`
|
||||
|
||||
ipv4Addr net.IP
|
||||
ipv4Addr net.IP // XXX: port to netip.Addr
|
||||
ipv4Mask net.IPMask
|
||||
opt66 *dhcpv4.Option
|
||||
opt67 *dhcpv4.Option
|
||||
@@ -1195,6 +1245,9 @@ func (obj *DHCPHostRes) handler4(data *HostData) (func(*dhcpv4.DHCPv4, *dhcpv4.D
|
||||
|
||||
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.
|
||||
@@ -1216,6 +1269,751 @@ func (obj *DHCPHostRes) handler4(data *HostData) (func(*dhcpv4.DHCPv4, *dhcpv4.D
|
||||
}, 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", obj.mask) // TODO: print as cidr or dotted quad
|
||||
|
||||
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.
|
||||
// XXX: Should we do this? Does it matter? Does the lib do it?
|
||||
//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.
|
||||
@@ -1236,3 +2034,18 @@ func (obj *overEngineeredLogger) Printf(format string, v ...interface{}) {
|
||||
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
|
||||
}
|
||||
|
||||
31
examples/lang/dhcp1.mcl
Normal file
31
examples/lang/dhcp1.mcl
Normal file
@@ -0,0 +1,31 @@
|
||||
#$iface = "lo" # replace with your desired interface like eth0
|
||||
$iface = "enp0s31f6"
|
||||
|
||||
net $iface {
|
||||
state => "up",
|
||||
addrs => ["192.168.42.1/24",],
|
||||
}
|
||||
|
||||
dhcp:server ":67" {
|
||||
interface => $iface, # required for now
|
||||
leasetime => "60s", # increase this for normal production
|
||||
dns => ["8.8.8.8", "1.1.1.1",], # pick your own better ones!
|
||||
routers => ["192.168.42.1",],
|
||||
|
||||
Depend => Net[$iface], # TODO: add autoedges
|
||||
}
|
||||
|
||||
dhcp:range "192.168.42.0/24" {
|
||||
skip => ["192.168.42.1/24",], # skip this host
|
||||
}
|
||||
|
||||
dhcp:host "hostname1" {
|
||||
mac => "00:11:22:33:44:55", # replace with your own!
|
||||
ip => "192.168.42.101/24", # cidr notation is required
|
||||
}
|
||||
|
||||
dhcp:host "hostname2" {
|
||||
mac => "ba:98:76:54:32:11", # replace with your own!
|
||||
ip => "192.168.42.102/24",
|
||||
nbp => "tftp://192.168.42.1/pxelinux.0", # for bios clients
|
||||
}
|
||||
6
go.mod
6
go.mod
@@ -52,8 +52,10 @@ require (
|
||||
github.com/armon/go-metrics v0.4.1 // indirect
|
||||
github.com/benbjohnson/clock v1.3.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.13.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/chappjc/logrus-prefix v0.0.0-20180227015900-3a1d64819adb // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/coreos/go-semver v0.3.0 // indirect
|
||||
github.com/distribution/reference v0.5.0 // indirect
|
||||
@@ -96,6 +98,7 @@ require (
|
||||
github.com/mdlayher/packet v1.0.0 // indirect
|
||||
github.com/mdlayher/raw v0.1.0 // indirect
|
||||
github.com/mdlayher/socket v0.5.0 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
@@ -109,6 +112,7 @@ require (
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 // indirect
|
||||
github.com/sergi/go-diff v1.2.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/soheilhy/cmux v0.1.5 // indirect
|
||||
@@ -137,10 +141,12 @@ require (
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.23.0 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect
|
||||
golang.org/x/mod v0.16.0 // indirect
|
||||
golang.org/x/net v0.22.0 // indirect
|
||||
golang.org/x/sync v0.6.0 // indirect
|
||||
golang.org/x/term v0.18.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 // indirect
|
||||
|
||||
15
go.sum
15
go.sum
@@ -54,6 +54,8 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
|
||||
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
@@ -62,6 +64,8 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chappjc/logrus-prefix v0.0.0-20180227015900-3a1d64819adb h1:aZTKxMminKeQWHtzJBbV8TttfTxzdJ+7iEJFE6FmUzg=
|
||||
github.com/chappjc/logrus-prefix v0.0.0-20180227015900-3a1d64819adb/go.mod h1:xzXc1S/L+64uglB3pw54o8kqyM6KFYpTeC9Q6+qZIu8=
|
||||
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
||||
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
@@ -131,6 +135,7 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
@@ -275,6 +280,7 @@ github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2C
|
||||
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
|
||||
@@ -286,6 +292,7 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
@@ -341,6 +348,8 @@ github.com/mdlayher/raw v0.1.0/go.mod h1:yXnxvs6c0XoF/aK52/H5PjsVHmWBCFfZUfoh/Y5
|
||||
github.com/mdlayher/socket v0.2.1/go.mod h1:QLlNPkFR88mRUNQIzRBMfXxwKal8H7u1h3bL1CV+f0E=
|
||||
github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI=
|
||||
github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
||||
github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY=
|
||||
@@ -371,6 +380,7 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
@@ -421,6 +431,8 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 h1:mZHayPoR0lNmnHyvtYjDeq0zlVHn9K/ZXoy17ylucdo=
|
||||
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5/go.mod h1:GEXHk5HgEKCvEIIrSpFI3ozzG5xOKA2DVlEX/gGnewM=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
@@ -546,6 +558,8 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY=
|
||||
go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
@@ -688,6 +702,7 @@ golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
|
||||
Reference in New Issue
Block a user