mirror of
https://github.com/soypat/lneto.git
synced 2026-07-26 10:38:47 +00:00
Compare commits
1 Commits
v0.2.1
...
dhcp-allocator
| Author | SHA1 | Date | |
|---|---|---|---|
| b46b4c7bc5 |
@@ -0,0 +1,65 @@
|
||||
package dhcp
|
||||
|
||||
import "net/netip"
|
||||
|
||||
// Request is the decoded, allocation-relevant view of an inbound client message
|
||||
// passed to an [Allocator]. All slice fields alias the caller's receive buffer
|
||||
// and are only valid for the duration of the call; an implementation that needs
|
||||
// to persist them must copy.
|
||||
type Request struct {
|
||||
// ClientID identifies the client. For DHCPv4 this is the client identifier
|
||||
// option when present, otherwise the client hardware address. For DHCPv6 it
|
||||
// is the client DUID.
|
||||
ClientID []byte
|
||||
// Requested is the address the client asked for (DHCPv4 "Requested IP
|
||||
// Address" option), or the zero value when the client expressed no
|
||||
// preference.
|
||||
Requested netip.Addr
|
||||
// Subnet is the server's configured allocation prefix. It is the zero value
|
||||
// when the server does not constrain allocation to a prefix.
|
||||
Subnet netip.Prefix
|
||||
// Hostname is the client-supplied hostname, or nil.
|
||||
Hostname []byte
|
||||
// ParamReqList is the client's parameter request list, or nil.
|
||||
ParamReqList []byte
|
||||
}
|
||||
|
||||
// Allocator owns a DHCP server's lease database: the address pool, the
|
||||
// persisted client-to-address bindings, and the lease lifetime / expiration
|
||||
// policy. A server delegates address assignment to an Allocator so that it only
|
||||
// has to drive the protocol state machine.
|
||||
//
|
||||
// Implementations that expire leases must obtain time from a caller-injected
|
||||
// clock rather than calling time.Now directly, in keeping with lneto's
|
||||
// time-independent design.
|
||||
//
|
||||
// Offer and Commit model the two phases of acquisition: Offer makes a tentative
|
||||
// reservation in response to a DHCPv4 DISCOVER (or DHCPv6 SOLICIT), and Commit
|
||||
// binds it in response to a REQUEST. Implementations may treat Offer
|
||||
// idempotently so that a repeated DISCOVER for the same client returns the same
|
||||
// reservation.
|
||||
type Allocator interface {
|
||||
// Offer tentatively reserves a binding for the requesting client and
|
||||
// returns it. It is called when the server receives a DISCOVER/SOLICIT.
|
||||
Offer(Request) (Binding, error)
|
||||
// Commit binds a previously offered reservation, finalizing the lease, and
|
||||
// returns the committed binding. It is called when the server receives a
|
||||
// REQUEST.
|
||||
Commit(Request) (Binding, error)
|
||||
// Release frees the binding held for the given client identity. It is
|
||||
// called when the server receives a RELEASE. Releasing an unknown client is
|
||||
// not an error.
|
||||
Release(clientID []byte) error
|
||||
// Decline marks the addresses in the request as unusable, typically because
|
||||
// the client detected an address conflict. It is called when the server
|
||||
// receives a DECLINE.
|
||||
Decline(Request) error
|
||||
|
||||
// AppendOptions lets the allocator customize the option bytes the server is
|
||||
// about to send. dst already contains the options the server derived from
|
||||
// its own configuration (server identifier, router, subnet mask, DNS, lease
|
||||
// times, ...) for the given client and binding. The implementation may
|
||||
// append further options, or rewrite the existing ones, and must return the
|
||||
// resulting slice. The returned slice must not exceed dst's capacity.
|
||||
AppendOptions(dst []byte, clientID []byte, b Binding) ([]byte, error)
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
package dhcpv4
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/soypat/lneto/dhcp"
|
||||
"github.com/soypat/lneto/ipv4"
|
||||
)
|
||||
|
||||
// MapAllocator is the built-in [dhcp.Allocator] used by [Server] when no
|
||||
// allocator is supplied. It keeps the lease database in a map and allocates
|
||||
// addresses sequentially from the configured subnet, preferring a client's
|
||||
// requested address when it is free and in-subnet.
|
||||
//
|
||||
// When configured with a clock (see [AllocatorConfig.Now]) it reclaims expired
|
||||
// leases back into the pool; with no clock leases never expire, matching the
|
||||
// behaviour of a server that does not track time.
|
||||
//
|
||||
// The map makes MapAllocator unsuitable for the most memory-constrained
|
||||
// targets; such deployments should provide their own [dhcp.Allocator]. This
|
||||
// mirrors the existing allowance for the DHCP server to use a map on capable
|
||||
// hardware.
|
||||
type MapAllocator struct {
|
||||
subnet ipv4.Prefix
|
||||
serverAddr [4]byte
|
||||
nextAddr [4]byte
|
||||
leaseSeconds uint32
|
||||
now func() time.Time
|
||||
leases map[[36]byte]mapLease
|
||||
declined map[[4]byte]struct{}
|
||||
}
|
||||
|
||||
type mapLease struct {
|
||||
addr [4]byte
|
||||
expiry time.Time // zero means no expiry (no clock configured).
|
||||
committed bool
|
||||
}
|
||||
|
||||
// AllocatorConfig configures a [MapAllocator].
|
||||
type AllocatorConfig struct {
|
||||
// ServerAddr is the server's own address, excluded from the pool.
|
||||
ServerAddr [4]byte
|
||||
// Subnet is the allocation prefix.
|
||||
Subnet ipv4.Prefix
|
||||
// LeaseSeconds is the lease duration handed to clients. Zero defaults to 3600.
|
||||
LeaseSeconds uint32
|
||||
// Now, when non-nil, is used to expire and reclaim leases. When nil leases
|
||||
// never expire.
|
||||
Now func() time.Time
|
||||
}
|
||||
|
||||
// NewMapAllocator returns a configured [MapAllocator].
|
||||
func NewMapAllocator(cfg AllocatorConfig) (*MapAllocator, error) {
|
||||
if !cfg.Subnet.IsValid() {
|
||||
return nil, errors.New("dhcpv4 allocator: invalid subnet")
|
||||
} else if !cfg.Subnet.Contains(cfg.ServerAddr) {
|
||||
return nil, errors.New("dhcpv4 allocator: server address outside subnet")
|
||||
}
|
||||
lease := cfg.LeaseSeconds
|
||||
if lease == 0 {
|
||||
lease = 3600
|
||||
}
|
||||
return &MapAllocator{
|
||||
subnet: cfg.Subnet,
|
||||
serverAddr: cfg.ServerAddr,
|
||||
nextAddr: cfg.Subnet.Next(cfg.ServerAddr),
|
||||
leaseSeconds: lease,
|
||||
now: cfg.Now,
|
||||
leases: make(map[[36]byte]mapLease),
|
||||
declined: make(map[[4]byte]struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
var _ dhcp.Allocator = (*MapAllocator)(nil)
|
||||
|
||||
// Offer implements [dhcp.Allocator]. It returns the binding already reserved for
|
||||
// the client if one exists, otherwise it reserves a new address.
|
||||
func (a *MapAllocator) Offer(req dhcp.Request) (dhcp.Binding, error) {
|
||||
a.sweepExpired()
|
||||
key, ok := keyOf(req.ClientID)
|
||||
if !ok {
|
||||
return dhcp.Binding{}, errors.New("dhcpv4 allocator: client id too long")
|
||||
}
|
||||
if existing, ok := a.leases[key]; ok {
|
||||
existing.expiry = a.expiry()
|
||||
a.leases[key] = existing
|
||||
return a.binding(existing.addr), nil
|
||||
}
|
||||
addr, ok := a.allocAddr(req.Requested)
|
||||
if !ok {
|
||||
return dhcp.Binding{}, errors.New("dhcpv4 allocator: address pool exhausted")
|
||||
}
|
||||
a.leases[key] = mapLease{addr: addr, expiry: a.expiry()}
|
||||
return a.binding(addr), nil
|
||||
}
|
||||
|
||||
// Commit implements [dhcp.Allocator], finalizing the client's lease.
|
||||
func (a *MapAllocator) Commit(req dhcp.Request) (dhcp.Binding, error) {
|
||||
a.sweepExpired()
|
||||
key, ok := keyOf(req.ClientID)
|
||||
if !ok {
|
||||
return dhcp.Binding{}, errors.New("dhcpv4 allocator: client id too long")
|
||||
}
|
||||
lease, ok := a.leases[key]
|
||||
if !ok {
|
||||
// Reservation expired or never made; allocate afresh.
|
||||
addr, ok := a.allocAddr(req.Requested)
|
||||
if !ok {
|
||||
return dhcp.Binding{}, errors.New("dhcpv4 allocator: address pool exhausted")
|
||||
}
|
||||
lease = mapLease{addr: addr}
|
||||
}
|
||||
lease.committed = true
|
||||
lease.expiry = a.expiry()
|
||||
a.leases[key] = lease
|
||||
return a.binding(lease.addr), nil
|
||||
}
|
||||
|
||||
// Release implements [dhcp.Allocator], freeing the client's lease.
|
||||
func (a *MapAllocator) Release(clientID []byte) error {
|
||||
key, ok := keyOf(clientID)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
delete(a.leases, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Decline implements [dhcp.Allocator], marking the client's address unusable.
|
||||
func (a *MapAllocator) Decline(req dhcp.Request) error {
|
||||
key, ok := keyOf(req.ClientID)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if lease, ok := a.leases[key]; ok {
|
||||
a.declined[lease.addr] = struct{}{}
|
||||
delete(a.leases, key)
|
||||
} else if req.Requested.Is4() {
|
||||
a.declined[req.Requested.As4()] = struct{}{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AppendOptions implements [dhcp.Allocator]. The server already wrote the
|
||||
// configuration-derived options, so the default allocator leaves them as-is.
|
||||
func (a *MapAllocator) AppendOptions(dst []byte, _ []byte, _ dhcp.Binding) ([]byte, error) {
|
||||
return dst, nil
|
||||
}
|
||||
|
||||
func (a *MapAllocator) binding(addr [4]byte) dhcp.Binding {
|
||||
t1, t2 := dhcp.DefaultT1T2(a.leaseSeconds)
|
||||
return dhcp.Binding{
|
||||
T1: t1,
|
||||
T2: t2,
|
||||
Leases: []dhcp.Lease{{
|
||||
Addr: netip.AddrFrom4(addr),
|
||||
Preferred: a.leaseSeconds,
|
||||
Valid: a.leaseSeconds,
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *MapAllocator) expiry() time.Time {
|
||||
if a.now == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return a.now().Add(time.Duration(a.leaseSeconds) * time.Second)
|
||||
}
|
||||
|
||||
func (a *MapAllocator) sweepExpired() {
|
||||
if a.now == nil {
|
||||
return
|
||||
}
|
||||
now := a.now()
|
||||
for k, v := range a.leases {
|
||||
if !v.expiry.IsZero() && now.After(v.expiry) {
|
||||
delete(a.leases, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// allocAddr allocates the next available address from the pool, preferring a
|
||||
// valid in-subnet requested address that is free.
|
||||
func (a *MapAllocator) allocAddr(requested netip.Addr) ([4]byte, bool) {
|
||||
if requested.Is4() {
|
||||
candidate := requested.As4()
|
||||
if a.subnet.Contains(candidate) && candidate != a.serverAddr && !a.isAssigned(candidate) {
|
||||
return candidate, true
|
||||
}
|
||||
}
|
||||
addr := a.nextAddr
|
||||
a.nextAddr = a.subnet.Next(a.nextAddr)
|
||||
if a.nextAddr == a.serverAddr {
|
||||
a.nextAddr = a.subnet.Next(a.nextAddr)
|
||||
}
|
||||
// Reject the broadcast address (all host bits set).
|
||||
hostBits := uint(32 - a.subnet.Bits())
|
||||
hostMask := ^uint32(0) >> (32 - hostBits)
|
||||
if binary.BigEndian.Uint32(addr[:])&hostMask == hostMask {
|
||||
return [4]byte{}, false
|
||||
}
|
||||
return addr, true
|
||||
}
|
||||
|
||||
func (a *MapAllocator) isAssigned(addr [4]byte) bool {
|
||||
if _, ok := a.declined[addr]; ok {
|
||||
return true
|
||||
}
|
||||
for _, v := range a.leases {
|
||||
if v.addr == addr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func keyOf(clientID []byte) ([36]byte, bool) {
|
||||
var key [36]byte
|
||||
if len(clientID) > len(key) {
|
||||
return key, false
|
||||
}
|
||||
copy(key[:], clientID)
|
||||
return key, true
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package dhcpv4
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/soypat/lneto/dhcp"
|
||||
"github.com/soypat/lneto/ipv4"
|
||||
)
|
||||
|
||||
// recordingAllocator is a test [dhcp.Allocator] that hands out a fixed address,
|
||||
// records whether the server-derived options reached its AppendOptions hook, and
|
||||
// appends a marker option of its own.
|
||||
type recordingAllocator struct {
|
||||
addr netip.Addr
|
||||
sawRouter bool
|
||||
appended bool
|
||||
}
|
||||
|
||||
func (a *recordingAllocator) binding() dhcp.Binding {
|
||||
return dhcp.Binding{
|
||||
T1: 30,
|
||||
T2: 52,
|
||||
Leases: []dhcp.Lease{{Addr: a.addr, Preferred: 60, Valid: 60}},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *recordingAllocator) Offer(dhcp.Request) (dhcp.Binding, error) { return a.binding(), nil }
|
||||
func (a *recordingAllocator) Commit(dhcp.Request) (dhcp.Binding, error) { return a.binding(), nil }
|
||||
func (a *recordingAllocator) Release([]byte) error { return nil }
|
||||
func (a *recordingAllocator) Decline(dhcp.Request) error { return nil }
|
||||
|
||||
func (a *recordingAllocator) AppendOptions(dst []byte, _ []byte, _ dhcp.Binding) ([]byte, error) {
|
||||
// dst already contains the server-derived options. Confirm the router
|
||||
// option the server was configured to send is present.
|
||||
for i := 0; i+1 < len(dst); {
|
||||
op := OptNum(dst[i])
|
||||
if op == OptEnd {
|
||||
break
|
||||
}
|
||||
if op == OptRouter {
|
||||
a.sawRouter = true
|
||||
}
|
||||
i += 2 + int(dst[i+1])
|
||||
}
|
||||
// Append our own option into the remaining capacity.
|
||||
tail := dst[len(dst):cap(dst)]
|
||||
n, err := EncodeOptionString(tail, OptDomainName, "example")
|
||||
if err != nil {
|
||||
return dst, err
|
||||
}
|
||||
a.appended = true
|
||||
return dst[:len(dst)+n], nil
|
||||
}
|
||||
|
||||
// TestServerCustomAllocator verifies the server delegates address assignment to
|
||||
// an injected allocator, that the allocator's AppendOptions hook sees the
|
||||
// server-derived options, and that options it appends reach the client.
|
||||
func TestServerCustomAllocator(t *testing.T) {
|
||||
svAddr := [4]byte{192, 168, 1, 1}
|
||||
want := netip.AddrFrom4([4]byte{10, 9, 8, 7})
|
||||
alloc := &recordingAllocator{addr: want}
|
||||
|
||||
var sv Server
|
||||
err := sv.Configure(ServerConfig{
|
||||
ServerAddr: svAddr,
|
||||
Gateway: [4]byte{192, 168, 1, 254},
|
||||
Subnet: ipv4.PrefixFrom(svAddr, 24),
|
||||
Allocator: alloc,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var cl Client
|
||||
if err := cl.BeginRequest(42, RequestConfig{ClientHardwareAddr: [6]byte{1, 2, 3, 4, 5, 6}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var buf [1024]byte
|
||||
n, err := cl.Encapsulate(buf[:], -1, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := sv.Demux(buf[:n], 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
n, err = sv.Encapsulate(buf[:], -1, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else if n == 0 {
|
||||
t.Fatal("no offer emitted")
|
||||
}
|
||||
|
||||
frm, _ := NewFrame(buf[:n])
|
||||
if got := *frm.YIAddr(); got != want.As4() {
|
||||
t.Errorf("offered address: got %v want %v", got, want)
|
||||
}
|
||||
if !alloc.sawRouter {
|
||||
t.Error("allocator AppendOptions did not see the server-derived router option")
|
||||
}
|
||||
var foundDomain bool
|
||||
frm.ForEachOption(func(_ int, opt OptNum, data []byte) error {
|
||||
if opt == OptDomainName && string(data) == "example" {
|
||||
foundDomain = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if !foundDomain {
|
||||
t.Error("allocator-appended domain option did not reach the emitted frame")
|
||||
}
|
||||
}
|
||||
|
||||
// TestMapAllocatorExpiration verifies that the default allocator reclaims an
|
||||
// expired lease back into the pool once its valid lifetime has elapsed.
|
||||
func TestMapAllocatorExpiration(t *testing.T) {
|
||||
svAddr := [4]byte{192, 168, 1, 1}
|
||||
now := time.Unix(1_000_000, 0)
|
||||
clock := func() time.Time { return now }
|
||||
|
||||
a, err := NewMapAllocator(AllocatorConfig{
|
||||
ServerAddr: svAddr,
|
||||
Subnet: ipv4.PrefixFrom(svAddr, 24),
|
||||
LeaseSeconds: 10,
|
||||
Now: clock,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Client 1 acquires and commits an address.
|
||||
b1, err := a.Offer(dhcp.Request{ClientID: []byte{1}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
addr1, _ := b1.Addr()
|
||||
if _, err := a.Commit(dhcp.Request{ClientID: []byte{1}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// While client 1 holds the lease, another client requesting the same
|
||||
// address must not be granted it.
|
||||
b2, err := a.Offer(dhcp.Request{ClientID: []byte{2}, Requested: addr1})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if addr2, _ := b2.Addr(); addr2 == addr1 {
|
||||
t.Fatalf("addr %v handed out while still leased to client 1", addr1)
|
||||
}
|
||||
if err := a.Release([]byte{2}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Advance time past client 1's lease; its address must be reclaimable.
|
||||
now = now.Add(20 * time.Second)
|
||||
b3, err := a.Offer(dhcp.Request{ClientID: []byte{3}, Requested: addr1})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if addr3, _ := b3.Addr(); addr3 != addr1 {
|
||||
t.Errorf("expired address not reclaimed: got %v want %v", addr3, addr1)
|
||||
}
|
||||
}
|
||||
@@ -265,11 +265,11 @@ func (c *Client) getMessageType(frm Frame) MessageType {
|
||||
func (c *Client) setOptions(frm Frame) error {
|
||||
err := frm.ForEachOption(func(_ int, opt OptNum, data []byte) error {
|
||||
switch opt {
|
||||
case OptRenewTimeValue:
|
||||
case OptT1Renewal:
|
||||
c.tRenew = maybeU32(data)
|
||||
case OptIPAddressLeaseTime:
|
||||
c.tIPLease = maybeU32(data)
|
||||
case OptRebindingTimeValue:
|
||||
case OptT2Rebinding:
|
||||
c.tRebind = maybeU32(data)
|
||||
case OptServerIdentification:
|
||||
c.svip.setmaybe(data)
|
||||
|
||||
@@ -128,8 +128,8 @@ const (
|
||||
OptParameterRequestList OptNum = 55 // Parameter request list
|
||||
OptMessage OptNum = 56 // DHCP error message
|
||||
OptMaximumMessageSize OptNum = 57 // DHCP maximum message size
|
||||
OptRenewTimeValue OptNum = 58 // DHCP renewal (T1) time
|
||||
OptRebindingTimeValue OptNum = 59 // DHCP rebinding (T2) time
|
||||
OptT1Renewal OptNum = 58 // DHCP renewal (T1) time
|
||||
OptT2Rebinding OptNum = 59 // DHCP rebinding (T2) time
|
||||
OptClientIdentifier OptNum = 60 // Client identifier
|
||||
OptClientIdentifier1 OptNum = 61 // Client identifier(1)
|
||||
|
||||
|
||||
+245
-168
@@ -4,26 +4,38 @@ import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
"github.com/soypat/lneto/dhcp"
|
||||
"github.com/soypat/lneto/internal"
|
||||
"github.com/soypat/lneto/ipv4"
|
||||
)
|
||||
|
||||
var errOptionNotFit = errors.New("DHCPv4: options dont fit")
|
||||
|
||||
const defaultMaxPending = 8
|
||||
|
||||
// Server implements the DHCPv4 server state machine (RFC 2131). It drives the
|
||||
// DISCOVER->OFFER->REQUEST->ACK exchange and delegates address assignment,
|
||||
// lease lifetime and per-client option customization to a [dhcp.Allocator].
|
||||
//
|
||||
// The Server itself holds no lease database: it keeps only a bounded table of
|
||||
// in-flight transactions. The persistent client-to-address bindings live in the
|
||||
// allocator, which may be supplied through [ServerConfig.Allocator] or, when
|
||||
// omitted, defaults to a [MapAllocator] built from the configuration.
|
||||
type Server struct {
|
||||
connID uint64
|
||||
nextAddr [4]byte
|
||||
subnet ipv4.Prefix
|
||||
hosts map[[36]byte]serverEntry
|
||||
vld lneto.Validator
|
||||
pending int
|
||||
leaseSeconds uint32
|
||||
port uint16
|
||||
siaddr [4]byte
|
||||
gwaddr [4]byte
|
||||
dns [4]byte
|
||||
connID uint64
|
||||
subnet ipv4.Prefix
|
||||
alloc dhcp.Allocator
|
||||
txns []txn
|
||||
vld lneto.Validator
|
||||
maxPending int
|
||||
port uint16
|
||||
siaddr [4]byte
|
||||
gwaddr [4]byte
|
||||
dns [4]byte
|
||||
}
|
||||
|
||||
// ServerConfig contains configuration parameters for [Server.Configure].
|
||||
@@ -36,64 +48,82 @@ type ServerConfig struct {
|
||||
DNS [4]byte
|
||||
// Subnet defines the network prefix for address allocation and subnet mask responses.
|
||||
Subnet ipv4.Prefix
|
||||
// LeaseSeconds is the lease duration. Zero defaults to 3600.
|
||||
// LeaseSeconds is the lease duration. Zero defaults to 3600. Only used when
|
||||
// building the default allocator (Allocator is nil).
|
||||
LeaseSeconds uint32
|
||||
// Port is the server listening port. Zero defaults to DefaultServerPort.
|
||||
Port uint16
|
||||
// Allocator delegates address assignment and lease management. When nil a
|
||||
// [MapAllocator] is built from ServerAddr, Subnet, LeaseSeconds and Now.
|
||||
Allocator dhcp.Allocator
|
||||
// MaxPending bounds the number of simultaneous in-flight transactions the
|
||||
// server tracks. Zero defaults to a small built-in value.
|
||||
MaxPending int
|
||||
// Now, when non-nil, is passed to the default allocator to enable lease
|
||||
// expiration. Ignored when Allocator is non-nil.
|
||||
Now func() time.Time
|
||||
}
|
||||
|
||||
type serverEntry struct {
|
||||
hostname string
|
||||
xid uint32
|
||||
port uint16
|
||||
addr [4]byte
|
||||
requestlist [10]byte
|
||||
hwaddr [6]byte
|
||||
clientIdlen uint8
|
||||
// Possible states:
|
||||
// - 0: No entry/uninitialized
|
||||
// - Init: Server received discover, pending Offer sent out.
|
||||
// - Selecting: Server sent out offer, request not received.
|
||||
// - Requesting: Request received, pending Ack sent out.
|
||||
// - Bound: Ack sent out, no more pending data to be sent.
|
||||
state ClientState
|
||||
// txn is an in-flight DORA transaction awaiting a response or a follow-up
|
||||
// request. The committed lease lives in the allocator; txn only carries what
|
||||
// the server needs to emit OFFER/ACK frames.
|
||||
type txn struct {
|
||||
binding dhcp.Binding
|
||||
clientID [36]byte
|
||||
xid uint32
|
||||
port uint16
|
||||
offered [4]byte
|
||||
hwaddr [6]byte
|
||||
clientLen uint8
|
||||
state ClientState // StateSelecting (offer pending/sent) or StateRequesting (ack pending).
|
||||
respond MessageType // MsgOffer, MsgAck, or 0 when nothing is queued to send.
|
||||
}
|
||||
|
||||
// Configure resets and configures the server with the given configuration.
|
||||
// The connection ID is incremented on each call to invalidate existing connections.
|
||||
// The hosts map is reused across calls to avoid reallocation.
|
||||
func (sv *Server) Configure(cfg ServerConfig) error {
|
||||
if !cfg.Subnet.IsValid() {
|
||||
return errors.New("dhcpv4 server: invalid subnet")
|
||||
} else if !cfg.Subnet.Contains(cfg.ServerAddr) {
|
||||
return errors.New("dhcpv4 server: server address outside subnet")
|
||||
alloc := cfg.Allocator
|
||||
if alloc == nil {
|
||||
if !cfg.Subnet.IsValid() {
|
||||
return errors.New("dhcpv4 server: invalid subnet")
|
||||
} else if !cfg.Subnet.Contains(cfg.ServerAddr) {
|
||||
return errors.New("dhcpv4 server: server address outside subnet")
|
||||
}
|
||||
a, err := NewMapAllocator(AllocatorConfig{
|
||||
ServerAddr: cfg.ServerAddr,
|
||||
Subnet: cfg.Subnet,
|
||||
LeaseSeconds: cfg.LeaseSeconds,
|
||||
Now: cfg.Now,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
alloc = a
|
||||
}
|
||||
port := cfg.Port
|
||||
if port == 0 {
|
||||
port = DefaultServerPort
|
||||
}
|
||||
lease := cfg.LeaseSeconds
|
||||
if lease == 0 {
|
||||
lease = 3600
|
||||
maxPending := cfg.MaxPending
|
||||
if maxPending <= 0 {
|
||||
maxPending = defaultMaxPending
|
||||
}
|
||||
hosts := sv.hosts
|
||||
if hosts == nil {
|
||||
hosts = make(map[[36]byte]serverEntry)
|
||||
txns := sv.txns
|
||||
if cap(txns) < maxPending {
|
||||
txns = make([]txn, 0, maxPending)
|
||||
} else {
|
||||
for k := range hosts {
|
||||
delete(hosts, k)
|
||||
}
|
||||
txns = txns[:0]
|
||||
}
|
||||
*sv = Server{
|
||||
connID: sv.connID + 1,
|
||||
siaddr: cfg.ServerAddr,
|
||||
gwaddr: cfg.Gateway,
|
||||
dns: cfg.DNS,
|
||||
subnet: cfg.Subnet,
|
||||
port: port,
|
||||
leaseSeconds: lease,
|
||||
nextAddr: cfg.Subnet.Next(cfg.ServerAddr),
|
||||
hosts: hosts,
|
||||
connID: sv.connID + 1,
|
||||
siaddr: cfg.ServerAddr,
|
||||
gwaddr: cfg.Gateway,
|
||||
dns: cfg.DNS,
|
||||
subnet: cfg.Subnet,
|
||||
port: port,
|
||||
alloc: alloc,
|
||||
txns: txns,
|
||||
maxPending: maxPending,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -148,115 +178,114 @@ func (sv *Server) Demux(carrierData []byte, frameOffset int) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var clientIDRaw [36]byte
|
||||
var client serverEntry
|
||||
var clientExists bool
|
||||
|
||||
// Resolve client identity: the client identifier option when present,
|
||||
// otherwise the hardware address.
|
||||
if len(clientID) == 0 {
|
||||
client, clientIDRaw, clientExists = sv.getClientByIP(*dfrm.CIAddr())
|
||||
} else {
|
||||
copy(clientIDRaw[:], clientID)
|
||||
client, clientExists = sv.getClient(clientIDRaw)
|
||||
clientID = dfrm.CHAddrAs6()[:]
|
||||
}
|
||||
|
||||
req := dhcp.Request{
|
||||
ClientID: clientID,
|
||||
Hostname: hostname,
|
||||
ParamReqList: reqlist,
|
||||
}
|
||||
if sv.subnet.IsValid() {
|
||||
req.Subnet = sv.subnet.NetipPrefix()
|
||||
}
|
||||
if len(reqAddr) == 4 {
|
||||
req.Requested = netip.AddrFrom4([4]byte(reqAddr))
|
||||
}
|
||||
|
||||
switch msgType {
|
||||
case MsgDiscover:
|
||||
if clientExists && (client.state == StateInit || client.state == StateRequesting) {
|
||||
sv.pending-- // Cancel unfulfilled pending response.
|
||||
binding, err := sv.alloc.Offer(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dhcpv4 server offer: %w", err)
|
||||
}
|
||||
if !clientExists {
|
||||
addr, ok := sv.allocAddr(reqAddr)
|
||||
if !ok {
|
||||
return errors.New("dhcpv4 server: address pool exhausted")
|
||||
}
|
||||
client.addr = addr
|
||||
offered, ok := binding.Addr()
|
||||
if !ok || !offered.Is4() {
|
||||
return errors.New("dhcpv4 server: allocator returned no IPv4 address")
|
||||
}
|
||||
copy(client.requestlist[:], reqlist)
|
||||
client.state = StateInit
|
||||
client.hostname = string(hostname)
|
||||
client.xid = dfrm.XID()
|
||||
client.hwaddr = *dfrm.CHAddrAs6()
|
||||
t := sv.upsertTxn(clientID)
|
||||
if t == nil {
|
||||
return errors.New("dhcpv4 server: too many pending transactions")
|
||||
}
|
||||
t.binding = binding
|
||||
t.offered = offered.As4()
|
||||
t.xid = dfrm.XID()
|
||||
t.hwaddr = *dfrm.CHAddrAs6()
|
||||
if isIPLayer {
|
||||
_, client.port, _ = getSrcIPPort(carrierData)
|
||||
_, t.port, _ = getSrcIPPort(carrierData)
|
||||
}
|
||||
client.clientIdlen = uint8(len(clientID))
|
||||
sv.pending++
|
||||
t.state = StateSelecting
|
||||
t.respond = MsgOffer
|
||||
|
||||
case MsgRequest:
|
||||
if !clientExists {
|
||||
err = errors.New("request for non existing client")
|
||||
} else if dfrm.XID() != client.xid {
|
||||
err = errors.New("unexpected XID for client")
|
||||
} else if client.state != StateSelecting && client.state != StateRequesting {
|
||||
err = errors.New("DHCP request unexpected state")
|
||||
t := sv.findTxn(clientID)
|
||||
if t == nil {
|
||||
return errors.New("request for non existing client")
|
||||
} else if dfrm.XID() != t.xid {
|
||||
return errors.New("unexpected XID for client")
|
||||
} else if t.state != StateSelecting && t.state != StateRequesting {
|
||||
return errors.New("DHCP request unexpected state")
|
||||
}
|
||||
binding, err := sv.alloc.Commit(req)
|
||||
if err != nil {
|
||||
break
|
||||
return fmt.Errorf("dhcpv4 server commit: %w", err)
|
||||
}
|
||||
if client.state == StateSelecting {
|
||||
client.state = StateRequesting
|
||||
sv.pending++
|
||||
offered, ok := binding.Addr()
|
||||
if !ok || !offered.Is4() {
|
||||
return errors.New("dhcpv4 server: allocator returned no IPv4 address")
|
||||
}
|
||||
t.binding = binding
|
||||
t.offered = offered.As4()
|
||||
t.state = StateRequesting
|
||||
t.respond = MsgAck
|
||||
|
||||
case MsgRelease:
|
||||
if clientExists {
|
||||
if client.state == StateInit || client.state == StateRequesting {
|
||||
sv.pending--
|
||||
}
|
||||
delete(sv.hosts, clientIDRaw)
|
||||
return nil
|
||||
err = sv.alloc.Release(clientID)
|
||||
sv.dropTxn(clientID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dhcpv4 server release: %w", err)
|
||||
}
|
||||
|
||||
case MsgDecline:
|
||||
err = sv.alloc.Decline(req)
|
||||
sv.dropTxn(clientID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dhcpv4 server decline: %w", err)
|
||||
}
|
||||
|
||||
default:
|
||||
err = fmt.Errorf("unhandled message type %s", msgType.String())
|
||||
return fmt.Errorf("unhandled message type %s", msgType.String())
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("msgtype=%s client=%+v: %w", msgType.String(), client, err)
|
||||
}
|
||||
sv.hosts[clientIDRaw] = client
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sv *Server) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int) (int, error) {
|
||||
carrierIsIP := offsetToIP >= 0
|
||||
dfrm, err := NewFrame(carrierData[offsetToFrame:])
|
||||
optBuf := dfrm.OptionsPayload()[:]
|
||||
if err != nil {
|
||||
return 0, err
|
||||
} else if len(optBuf) < 255 {
|
||||
}
|
||||
optBuf := dfrm.OptionsPayload()
|
||||
if len(optBuf) < 255 {
|
||||
return 0, errOptionNotFit
|
||||
}
|
||||
if sv.pending == 0 {
|
||||
|
||||
t := sv.nextPending()
|
||||
if t == nil {
|
||||
return 0, nil // No pending outgoing frames.
|
||||
}
|
||||
|
||||
var client serverEntry
|
||||
var clientID [36]byte
|
||||
for k, v := range sv.hosts {
|
||||
pending := v.state == StateInit || v.state == StateRequesting
|
||||
if pending {
|
||||
client = v
|
||||
clientID = k
|
||||
break
|
||||
}
|
||||
}
|
||||
if client.state == 0 {
|
||||
return 0, nil // Nothing to do.
|
||||
}
|
||||
futureState := ClientState(0)
|
||||
var nopt int
|
||||
switch client.state {
|
||||
case StateInit:
|
||||
futureState = StateSelecting
|
||||
nopt, err = EncodeOption(optBuf[nopt:], OptMessageType, byte(MsgOffer))
|
||||
case StateRequesting:
|
||||
futureState = StateBound
|
||||
nopt, err = EncodeOption(optBuf[nopt:], OptMessageType, byte(MsgAck))
|
||||
*dfrm.CIAddr() = client.addr
|
||||
}
|
||||
n, err := EncodeOption(optBuf[nopt:], OptMessageType, byte(t.respond))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
n, _ := EncodeOption(optBuf[nopt:], OptServerIdentification, sv.siaddr[:]...)
|
||||
nopt += n
|
||||
n, _ = EncodeOption(optBuf[nopt:], OptServerIdentification, sv.siaddr[:]...)
|
||||
nopt += n
|
||||
if sv.gwaddr != [4]byte{} {
|
||||
n, _ = EncodeOption(optBuf[nopt:], OptRouter, sv.gwaddr[:]...)
|
||||
@@ -274,88 +303,136 @@ func (sv *Server) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int)
|
||||
n, _ = EncodeOption(optBuf[nopt:], OptDNSServers, sv.dns[:]...)
|
||||
nopt += n
|
||||
}
|
||||
if sv.leaseSeconds > 0 {
|
||||
n, _ = EncodeOption32(optBuf[nopt:], OptIPAddressLeaseTime, sv.leaseSeconds)
|
||||
if lease, ok := leaseOf(t.binding); ok && lease.Valid > 0 {
|
||||
n, _ = EncodeOption32(optBuf[nopt:], OptIPAddressLeaseTime, lease.Valid)
|
||||
nopt += n
|
||||
n, _ = EncodeOption32(optBuf[nopt:], OptRenewTimeValue, sv.leaseSeconds/2)
|
||||
n, _ = EncodeOption32(optBuf[nopt:], OptT1Renewal, t.binding.T1)
|
||||
nopt += n
|
||||
n, _ = EncodeOption32(optBuf[nopt:], OptRebindingTimeValue, sv.leaseSeconds*7/8)
|
||||
n, _ = EncodeOption32(optBuf[nopt:], OptT2Rebinding, t.binding.T2)
|
||||
nopt += n
|
||||
}
|
||||
|
||||
// Let the allocator append or rewrite options. dst already holds the
|
||||
// server-derived options for this client and binding.
|
||||
optBuf, err = sv.alloc.AppendOptions(optBuf[:nopt], t.clientID[:t.clientLen], t.binding)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
nopt = len(optBuf)
|
||||
optBuf = dfrm.OptionsPayload()
|
||||
if nopt >= len(optBuf) {
|
||||
return 0, errOptionNotFit
|
||||
}
|
||||
optBuf[nopt] = byte(OptEnd)
|
||||
nopt++
|
||||
|
||||
dfrm.ClearHeader()
|
||||
dfrm.SetOp(OpReply)
|
||||
dfrm.SetHardware(1, 6, 0)
|
||||
dfrm.SetXID(client.xid)
|
||||
dfrm.SetXID(t.xid)
|
||||
dfrm.SetSecs(0)
|
||||
dfrm.SetFlags(0)
|
||||
*dfrm.YIAddr() = client.addr // Offer here.
|
||||
*dfrm.YIAddr() = t.offered
|
||||
if t.respond == MsgAck {
|
||||
*dfrm.CIAddr() = t.offered
|
||||
}
|
||||
*dfrm.SIAddr() = sv.siaddr
|
||||
*dfrm.GIAddr() = sv.gwaddr
|
||||
copy(dfrm.CHAddrAs6()[:], client.hwaddr[:])
|
||||
copy(dfrm.CHAddrAs6()[:], t.hwaddr[:])
|
||||
dfrm.SetMagicCookie(MagicCookie)
|
||||
if carrierIsIP {
|
||||
err = internal.SetIPAddrs(carrierData[offsetToIP:], 0, sv.siaddr[:], client.addr[:])
|
||||
err = internal.SetIPAddrs(carrierData[offsetToIP:], 0, sv.siaddr[:], t.offered[:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
client.state = futureState
|
||||
|
||||
// Set server state.
|
||||
sv.hosts[clientID] = client
|
||||
sv.pending--
|
||||
if t.respond == MsgAck {
|
||||
// Transaction complete; the lease now lives in the allocator.
|
||||
sv.dropTxnAt(t)
|
||||
} else {
|
||||
t.respond = 0 // Offer sent, await the client's REQUEST.
|
||||
}
|
||||
return OptionsOffset + nopt, nil
|
||||
}
|
||||
|
||||
// allocAddr allocates the next available address from the pool.
|
||||
// If reqAddr is a valid 4-byte address within the subnet and not already assigned,
|
||||
// it is preferred. Returns false if the pool is exhausted.
|
||||
func (sv *Server) allocAddr(reqAddr []byte) ([4]byte, bool) {
|
||||
if len(reqAddr) == 4 {
|
||||
candidate := [4]byte(reqAddr)
|
||||
if sv.subnet.Contains(candidate) && candidate != sv.siaddr && !sv.isAddrAssigned(candidate) {
|
||||
return candidate, true
|
||||
}
|
||||
// leaseOf returns the first lease of a binding.
|
||||
func leaseOf(b dhcp.Binding) (dhcp.Lease, bool) {
|
||||
if len(b.Leases) == 0 {
|
||||
return dhcp.Lease{}, false
|
||||
}
|
||||
// Reject broadcast address (all host bits set).
|
||||
a := sv.nextAddr
|
||||
sv.nextAddr = sv.subnet.Next(sv.nextAddr)
|
||||
if sv.nextAddr == sv.siaddr {
|
||||
sv.nextAddr = sv.subnet.Next(sv.nextAddr)
|
||||
}
|
||||
hostBits := uint(32 - sv.subnet.Bits())
|
||||
hostMask := ^uint32(0) >> (32 - hostBits)
|
||||
if binary.BigEndian.Uint32(a[:])&hostMask == hostMask {
|
||||
return [4]byte{}, false
|
||||
}
|
||||
return a, true
|
||||
return b.Leases[0], true
|
||||
}
|
||||
|
||||
func (sv *Server) isAddrAssigned(addr [4]byte) bool {
|
||||
for _, v := range sv.hosts {
|
||||
if v.addr == addr {
|
||||
return true
|
||||
// findTxn returns the in-flight transaction for clientID, or nil.
|
||||
func (sv *Server) findTxn(clientID []byte) *txn {
|
||||
for i := range sv.txns {
|
||||
if sv.txns[i].matches(clientID) {
|
||||
return &sv.txns[i]
|
||||
}
|
||||
}
|
||||
return false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sv *Server) getClient(clientID [36]byte) (serverEntry, bool) {
|
||||
entry, ok := sv.hosts[clientID]
|
||||
return entry, ok
|
||||
// upsertTxn returns the existing transaction for clientID, reusing its slot, or
|
||||
// appends a new one. It returns nil if the table is full.
|
||||
func (sv *Server) upsertTxn(clientID []byte) *txn {
|
||||
if t := sv.findTxn(clientID); t != nil {
|
||||
t.binding = dhcp.Binding{}
|
||||
t.respond = 0
|
||||
return t
|
||||
}
|
||||
if len(sv.txns) >= sv.maxPending {
|
||||
return nil
|
||||
}
|
||||
var t txn
|
||||
t.clientLen = uint8(copy(t.clientID[:], clientID))
|
||||
sv.txns = append(sv.txns, t)
|
||||
return &sv.txns[len(sv.txns)-1]
|
||||
}
|
||||
|
||||
func (sv *Server) getClientByIP(ip [4]byte) (serverEntry, [36]byte, bool) {
|
||||
for k, v := range sv.hosts {
|
||||
if v.addr == ip {
|
||||
return v, k, true
|
||||
// nextPending returns the next transaction with a queued response, or nil.
|
||||
func (sv *Server) nextPending() *txn {
|
||||
for i := range sv.txns {
|
||||
if sv.txns[i].respond != 0 {
|
||||
return &sv.txns[i]
|
||||
}
|
||||
}
|
||||
return serverEntry{}, [36]byte{}, false
|
||||
return nil
|
||||
}
|
||||
|
||||
// dropTxn removes the transaction for clientID if present.
|
||||
func (sv *Server) dropTxn(clientID []byte) {
|
||||
for i := range sv.txns {
|
||||
if sv.txns[i].matches(clientID) {
|
||||
sv.removeAt(i)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dropTxnAt removes the transaction pointed to by t.
|
||||
func (sv *Server) dropTxnAt(t *txn) {
|
||||
for i := range sv.txns {
|
||||
if &sv.txns[i] == t {
|
||||
sv.removeAt(i)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (sv *Server) removeAt(i int) {
|
||||
last := len(sv.txns) - 1
|
||||
sv.txns[i] = sv.txns[last]
|
||||
sv.txns[last] = txn{}
|
||||
sv.txns = sv.txns[:last]
|
||||
}
|
||||
|
||||
func (t *txn) matches(clientID []byte) bool {
|
||||
if int(t.clientLen) != len(clientID) {
|
||||
return false
|
||||
}
|
||||
return string(t.clientID[:t.clientLen]) == string(clientID)
|
||||
}
|
||||
|
||||
func getSrcIPPort(ipCarrier []byte) (srcaddr []byte, port uint16, err error) {
|
||||
|
||||
@@ -239,9 +239,9 @@ func TestServerOfferContainsOptions(t *testing.T) {
|
||||
foundLease = true
|
||||
gotLease = maybeU32(data)
|
||||
}
|
||||
case OptRenewTimeValue:
|
||||
case OptT1Renewal:
|
||||
gotRenew = maybeU32(data)
|
||||
case OptRebindingTimeValue:
|
||||
case OptT2Rebinding:
|
||||
gotRebind = maybeU32(data)
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -67,8 +67,8 @@ func _() {
|
||||
_ = x[OptParameterRequestList-55]
|
||||
_ = x[OptMessage-56]
|
||||
_ = x[OptMaximumMessageSize-57]
|
||||
_ = x[OptRenewTimeValue-58]
|
||||
_ = x[OptRebindingTimeValue-59]
|
||||
_ = x[OptT1Renewal-58]
|
||||
_ = x[OptT2Rebinding-59]
|
||||
_ = x[OptClientIdentifier-60]
|
||||
_ = x[OptClientIdentifier1-61]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
// Package dhcp holds protocol-version-independent types shared by the DHCPv4
|
||||
// and DHCPv6 implementations. In particular it defines the [Allocator]
|
||||
// interface a DHCP server delegates address assignment and lease lifetime
|
||||
// management to, decoupling the higher-level state machine from the backing
|
||||
// lease database.
|
||||
//
|
||||
// The lease lifetime model follows the common shape described by RFC 2131
|
||||
// (DHCPv4) and RFC 8415 (DHCPv6): a per-client [Binding] carries renewal (T1)
|
||||
// and rebind (T2) times and holds one or more [Lease]s, each with a preferred
|
||||
// and valid lifetime. DHCPv4 assigns a single address whose preferred and
|
||||
// valid lifetimes equal the lease time; DHCPv6 may assign several addresses
|
||||
// (and prefixes) through identity associations, each with its own lifetimes.
|
||||
package dhcp
|
||||
|
||||
import "net/netip"
|
||||
|
||||
// Lease is a single address binding offered or assigned to a client.
|
||||
type Lease struct {
|
||||
// Addr is the assigned address. For DHCPv4 this is an IPv4 address.
|
||||
Addr netip.Addr
|
||||
// Preferred is the preferred lifetime in seconds (RFC 8415 §7.1). For
|
||||
// DHCPv4, where there is no distinct preferred lifetime, set it equal to
|
||||
// Valid.
|
||||
Preferred uint32
|
||||
// Valid is the valid lifetime in seconds. This is the DHCPv4 "IP Address
|
||||
// Lease Time" (RFC 2132 option 51).
|
||||
Valid uint32
|
||||
}
|
||||
|
||||
// Binding groups the leases assigned to a single client identity association
|
||||
// together with the renewal and rebind times that govern when the client
|
||||
// should attempt to extend them.
|
||||
type Binding struct {
|
||||
// T1 is the renewal time in seconds: when the client should contact the
|
||||
// allocating server to extend its leases. RFC 2131 §4.4.5 / RFC 8415 §14.2
|
||||
// default this to half the (shortest) lease time.
|
||||
T1 uint32
|
||||
// T2 is the rebinding time in seconds: when the client should broadcast to
|
||||
// any server to extend its leases. The RFCs default this to 0.875 of the
|
||||
// (shortest) lease time.
|
||||
T2 uint32
|
||||
// Leases holds the addresses bound to the client. DHCPv4 uses exactly one
|
||||
// lease; DHCPv6 may use more than one (IA_NA / IA_PD).
|
||||
Leases []Lease
|
||||
}
|
||||
|
||||
// Addr returns the first lease address and whether the binding holds any lease.
|
||||
// It is a convenience for single-address (DHCPv4) callers.
|
||||
func (b Binding) Addr() (netip.Addr, bool) {
|
||||
if len(b.Leases) == 0 {
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
return b.Leases[0].Addr, true
|
||||
}
|
||||
|
||||
// DefaultT1T2 returns the RFC 2131 §4.4.5 default renewal (T1) and rebind (T2)
|
||||
// times for a given lease duration: T1 = 0.5·lease and T2 = 0.875·lease.
|
||||
func DefaultT1T2(leaseSeconds uint32) (t1Renewal, t2Rebinding uint32) {
|
||||
return leaseSeconds / 2, leaseSeconds * 7 / 8
|
||||
}
|
||||
@@ -697,7 +697,7 @@ func (pc *PacketBreakdown) CaptureDHCPv4(dst []Frame, pkt []byte, bitOffset int)
|
||||
field.Class = FieldClassSize
|
||||
|
||||
// Time/duration options (seconds).
|
||||
case dhcpv4.OptIPAddressLeaseTime, dhcpv4.OptRenewTimeValue, dhcpv4.OptRebindingTimeValue,
|
||||
case dhcpv4.OptIPAddressLeaseTime, dhcpv4.OptT1Renewal, dhcpv4.OptT2Rebinding,
|
||||
dhcpv4.OptTimeOffset, dhcpv4.OptARPCacheTimeout, dhcpv4.OptPathMTUAgingTimeout,
|
||||
dhcpv4.OptTCPKeepaliveInterval, dhcpv4.OptDefaultIPTTL, dhcpv4.OptDefaultTCPTimetoLive:
|
||||
field.Class = FieldClassTimestamp
|
||||
|
||||
Reference in New Issue
Block a user