mirror of
https://github.com/soypat/lneto.git
synced 2026-08-04 23:17:52 +00:00
Compare commits
1 Commits
tls
...
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 {
|
func (c *Client) setOptions(frm Frame) error {
|
||||||
err := frm.ForEachOption(func(_ int, opt OptNum, data []byte) error {
|
err := frm.ForEachOption(func(_ int, opt OptNum, data []byte) error {
|
||||||
switch opt {
|
switch opt {
|
||||||
case OptRenewTimeValue:
|
case OptT1Renewal:
|
||||||
c.tRenew = maybeU32(data)
|
c.tRenew = maybeU32(data)
|
||||||
case OptIPAddressLeaseTime:
|
case OptIPAddressLeaseTime:
|
||||||
c.tIPLease = maybeU32(data)
|
c.tIPLease = maybeU32(data)
|
||||||
case OptRebindingTimeValue:
|
case OptT2Rebinding:
|
||||||
c.tRebind = maybeU32(data)
|
c.tRebind = maybeU32(data)
|
||||||
case OptServerIdentification:
|
case OptServerIdentification:
|
||||||
c.svip.setmaybe(data)
|
c.svip.setmaybe(data)
|
||||||
|
|||||||
@@ -128,8 +128,8 @@ const (
|
|||||||
OptParameterRequestList OptNum = 55 // Parameter request list
|
OptParameterRequestList OptNum = 55 // Parameter request list
|
||||||
OptMessage OptNum = 56 // DHCP error message
|
OptMessage OptNum = 56 // DHCP error message
|
||||||
OptMaximumMessageSize OptNum = 57 // DHCP maximum message size
|
OptMaximumMessageSize OptNum = 57 // DHCP maximum message size
|
||||||
OptRenewTimeValue OptNum = 58 // DHCP renewal (T1) time
|
OptT1Renewal OptNum = 58 // DHCP renewal (T1) time
|
||||||
OptRebindingTimeValue OptNum = 59 // DHCP rebinding (T2) time
|
OptT2Rebinding OptNum = 59 // DHCP rebinding (T2) time
|
||||||
OptClientIdentifier OptNum = 60 // Client identifier
|
OptClientIdentifier OptNum = 60 // Client identifier
|
||||||
OptClientIdentifier1 OptNum = 61 // Client identifier(1)
|
OptClientIdentifier1 OptNum = 61 // Client identifier(1)
|
||||||
|
|
||||||
|
|||||||
+245
-168
@@ -4,26 +4,38 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/soypat/lneto"
|
"github.com/soypat/lneto"
|
||||||
|
"github.com/soypat/lneto/dhcp"
|
||||||
"github.com/soypat/lneto/internal"
|
"github.com/soypat/lneto/internal"
|
||||||
"github.com/soypat/lneto/ipv4"
|
"github.com/soypat/lneto/ipv4"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errOptionNotFit = errors.New("DHCPv4: options dont fit")
|
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 {
|
type Server struct {
|
||||||
connID uint64
|
connID uint64
|
||||||
nextAddr [4]byte
|
subnet ipv4.Prefix
|
||||||
subnet ipv4.Prefix
|
alloc dhcp.Allocator
|
||||||
hosts map[[36]byte]serverEntry
|
txns []txn
|
||||||
vld lneto.Validator
|
vld lneto.Validator
|
||||||
pending int
|
maxPending int
|
||||||
leaseSeconds uint32
|
port uint16
|
||||||
port uint16
|
siaddr [4]byte
|
||||||
siaddr [4]byte
|
gwaddr [4]byte
|
||||||
gwaddr [4]byte
|
dns [4]byte
|
||||||
dns [4]byte
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServerConfig contains configuration parameters for [Server.Configure].
|
// ServerConfig contains configuration parameters for [Server.Configure].
|
||||||
@@ -36,64 +48,82 @@ type ServerConfig struct {
|
|||||||
DNS [4]byte
|
DNS [4]byte
|
||||||
// Subnet defines the network prefix for address allocation and subnet mask responses.
|
// Subnet defines the network prefix for address allocation and subnet mask responses.
|
||||||
Subnet ipv4.Prefix
|
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
|
LeaseSeconds uint32
|
||||||
// Port is the server listening port. Zero defaults to DefaultServerPort.
|
// Port is the server listening port. Zero defaults to DefaultServerPort.
|
||||||
Port uint16
|
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 {
|
// txn is an in-flight DORA transaction awaiting a response or a follow-up
|
||||||
hostname string
|
// request. The committed lease lives in the allocator; txn only carries what
|
||||||
xid uint32
|
// the server needs to emit OFFER/ACK frames.
|
||||||
port uint16
|
type txn struct {
|
||||||
addr [4]byte
|
binding dhcp.Binding
|
||||||
requestlist [10]byte
|
clientID [36]byte
|
||||||
hwaddr [6]byte
|
xid uint32
|
||||||
clientIdlen uint8
|
port uint16
|
||||||
// Possible states:
|
offered [4]byte
|
||||||
// - 0: No entry/uninitialized
|
hwaddr [6]byte
|
||||||
// - Init: Server received discover, pending Offer sent out.
|
clientLen uint8
|
||||||
// - Selecting: Server sent out offer, request not received.
|
state ClientState // StateSelecting (offer pending/sent) or StateRequesting (ack pending).
|
||||||
// - Requesting: Request received, pending Ack sent out.
|
respond MessageType // MsgOffer, MsgAck, or 0 when nothing is queued to send.
|
||||||
// - Bound: Ack sent out, no more pending data to be sent.
|
|
||||||
state ClientState
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure resets and configures the server with the given configuration.
|
// Configure resets and configures the server with the given configuration.
|
||||||
// The connection ID is incremented on each call to invalidate existing connections.
|
// 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 {
|
func (sv *Server) Configure(cfg ServerConfig) error {
|
||||||
if !cfg.Subnet.IsValid() {
|
alloc := cfg.Allocator
|
||||||
return errors.New("dhcpv4 server: invalid subnet")
|
if alloc == nil {
|
||||||
} else if !cfg.Subnet.Contains(cfg.ServerAddr) {
|
if !cfg.Subnet.IsValid() {
|
||||||
return errors.New("dhcpv4 server: server address outside subnet")
|
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
|
port := cfg.Port
|
||||||
if port == 0 {
|
if port == 0 {
|
||||||
port = DefaultServerPort
|
port = DefaultServerPort
|
||||||
}
|
}
|
||||||
lease := cfg.LeaseSeconds
|
maxPending := cfg.MaxPending
|
||||||
if lease == 0 {
|
if maxPending <= 0 {
|
||||||
lease = 3600
|
maxPending = defaultMaxPending
|
||||||
}
|
}
|
||||||
hosts := sv.hosts
|
txns := sv.txns
|
||||||
if hosts == nil {
|
if cap(txns) < maxPending {
|
||||||
hosts = make(map[[36]byte]serverEntry)
|
txns = make([]txn, 0, maxPending)
|
||||||
} else {
|
} else {
|
||||||
for k := range hosts {
|
txns = txns[:0]
|
||||||
delete(hosts, k)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
*sv = Server{
|
*sv = Server{
|
||||||
connID: sv.connID + 1,
|
connID: sv.connID + 1,
|
||||||
siaddr: cfg.ServerAddr,
|
siaddr: cfg.ServerAddr,
|
||||||
gwaddr: cfg.Gateway,
|
gwaddr: cfg.Gateway,
|
||||||
dns: cfg.DNS,
|
dns: cfg.DNS,
|
||||||
subnet: cfg.Subnet,
|
subnet: cfg.Subnet,
|
||||||
port: port,
|
port: port,
|
||||||
leaseSeconds: lease,
|
alloc: alloc,
|
||||||
nextAddr: cfg.Subnet.Next(cfg.ServerAddr),
|
txns: txns,
|
||||||
hosts: hosts,
|
maxPending: maxPending,
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -148,115 +178,114 @@ func (sv *Server) Demux(carrierData []byte, frameOffset int) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var clientIDRaw [36]byte
|
|
||||||
var client serverEntry
|
// Resolve client identity: the client identifier option when present,
|
||||||
var clientExists bool
|
// otherwise the hardware address.
|
||||||
if len(clientID) == 0 {
|
if len(clientID) == 0 {
|
||||||
client, clientIDRaw, clientExists = sv.getClientByIP(*dfrm.CIAddr())
|
clientID = dfrm.CHAddrAs6()[:]
|
||||||
} else {
|
}
|
||||||
copy(clientIDRaw[:], clientID)
|
|
||||||
client, clientExists = sv.getClient(clientIDRaw)
|
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 {
|
switch msgType {
|
||||||
case MsgDiscover:
|
case MsgDiscover:
|
||||||
if clientExists && (client.state == StateInit || client.state == StateRequesting) {
|
binding, err := sv.alloc.Offer(req)
|
||||||
sv.pending-- // Cancel unfulfilled pending response.
|
if err != nil {
|
||||||
|
return fmt.Errorf("dhcpv4 server offer: %w", err)
|
||||||
}
|
}
|
||||||
if !clientExists {
|
offered, ok := binding.Addr()
|
||||||
addr, ok := sv.allocAddr(reqAddr)
|
if !ok || !offered.Is4() {
|
||||||
if !ok {
|
return errors.New("dhcpv4 server: allocator returned no IPv4 address")
|
||||||
return errors.New("dhcpv4 server: address pool exhausted")
|
|
||||||
}
|
|
||||||
client.addr = addr
|
|
||||||
}
|
}
|
||||||
copy(client.requestlist[:], reqlist)
|
t := sv.upsertTxn(clientID)
|
||||||
client.state = StateInit
|
if t == nil {
|
||||||
client.hostname = string(hostname)
|
return errors.New("dhcpv4 server: too many pending transactions")
|
||||||
client.xid = dfrm.XID()
|
}
|
||||||
client.hwaddr = *dfrm.CHAddrAs6()
|
t.binding = binding
|
||||||
|
t.offered = offered.As4()
|
||||||
|
t.xid = dfrm.XID()
|
||||||
|
t.hwaddr = *dfrm.CHAddrAs6()
|
||||||
if isIPLayer {
|
if isIPLayer {
|
||||||
_, client.port, _ = getSrcIPPort(carrierData)
|
_, t.port, _ = getSrcIPPort(carrierData)
|
||||||
}
|
}
|
||||||
client.clientIdlen = uint8(len(clientID))
|
t.state = StateSelecting
|
||||||
sv.pending++
|
t.respond = MsgOffer
|
||||||
|
|
||||||
case MsgRequest:
|
case MsgRequest:
|
||||||
if !clientExists {
|
t := sv.findTxn(clientID)
|
||||||
err = errors.New("request for non existing client")
|
if t == nil {
|
||||||
} else if dfrm.XID() != client.xid {
|
return errors.New("request for non existing client")
|
||||||
err = errors.New("unexpected XID for client")
|
} else if dfrm.XID() != t.xid {
|
||||||
} else if client.state != StateSelecting && client.state != StateRequesting {
|
return errors.New("unexpected XID for client")
|
||||||
err = errors.New("DHCP request unexpected state")
|
} else if t.state != StateSelecting && t.state != StateRequesting {
|
||||||
|
return errors.New("DHCP request unexpected state")
|
||||||
}
|
}
|
||||||
|
binding, err := sv.alloc.Commit(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
return fmt.Errorf("dhcpv4 server commit: %w", err)
|
||||||
}
|
}
|
||||||
if client.state == StateSelecting {
|
offered, ok := binding.Addr()
|
||||||
client.state = StateRequesting
|
if !ok || !offered.Is4() {
|
||||||
sv.pending++
|
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:
|
case MsgRelease:
|
||||||
if clientExists {
|
err = sv.alloc.Release(clientID)
|
||||||
if client.state == StateInit || client.state == StateRequesting {
|
sv.dropTxn(clientID)
|
||||||
sv.pending--
|
if err != nil {
|
||||||
}
|
return fmt.Errorf("dhcpv4 server release: %w", err)
|
||||||
delete(sv.hosts, clientIDRaw)
|
}
|
||||||
return nil
|
|
||||||
|
case MsgDecline:
|
||||||
|
err = sv.alloc.Decline(req)
|
||||||
|
sv.dropTxn(clientID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("dhcpv4 server decline: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sv *Server) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int) (int, error) {
|
func (sv *Server) Encapsulate(carrierData []byte, offsetToIP, offsetToFrame int) (int, error) {
|
||||||
carrierIsIP := offsetToIP >= 0
|
carrierIsIP := offsetToIP >= 0
|
||||||
dfrm, err := NewFrame(carrierData[offsetToFrame:])
|
dfrm, err := NewFrame(carrierData[offsetToFrame:])
|
||||||
optBuf := dfrm.OptionsPayload()[:]
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
} else if len(optBuf) < 255 {
|
}
|
||||||
|
optBuf := dfrm.OptionsPayload()
|
||||||
|
if len(optBuf) < 255 {
|
||||||
return 0, errOptionNotFit
|
return 0, errOptionNotFit
|
||||||
}
|
}
|
||||||
if sv.pending == 0 {
|
|
||||||
|
t := sv.nextPending()
|
||||||
|
if t == nil {
|
||||||
return 0, nil // No pending outgoing frames.
|
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
|
var nopt int
|
||||||
switch client.state {
|
n, err := EncodeOption(optBuf[nopt:], OptMessageType, byte(t.respond))
|
||||||
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
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
n, _ := EncodeOption(optBuf[nopt:], OptServerIdentification, sv.siaddr[:]...)
|
nopt += n
|
||||||
|
n, _ = EncodeOption(optBuf[nopt:], OptServerIdentification, sv.siaddr[:]...)
|
||||||
nopt += n
|
nopt += n
|
||||||
if sv.gwaddr != [4]byte{} {
|
if sv.gwaddr != [4]byte{} {
|
||||||
n, _ = EncodeOption(optBuf[nopt:], OptRouter, sv.gwaddr[:]...)
|
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[:]...)
|
n, _ = EncodeOption(optBuf[nopt:], OptDNSServers, sv.dns[:]...)
|
||||||
nopt += n
|
nopt += n
|
||||||
}
|
}
|
||||||
if sv.leaseSeconds > 0 {
|
if lease, ok := leaseOf(t.binding); ok && lease.Valid > 0 {
|
||||||
n, _ = EncodeOption32(optBuf[nopt:], OptIPAddressLeaseTime, sv.leaseSeconds)
|
n, _ = EncodeOption32(optBuf[nopt:], OptIPAddressLeaseTime, lease.Valid)
|
||||||
nopt += n
|
nopt += n
|
||||||
n, _ = EncodeOption32(optBuf[nopt:], OptRenewTimeValue, sv.leaseSeconds/2)
|
n, _ = EncodeOption32(optBuf[nopt:], OptT1Renewal, t.binding.T1)
|
||||||
nopt += n
|
nopt += n
|
||||||
n, _ = EncodeOption32(optBuf[nopt:], OptRebindingTimeValue, sv.leaseSeconds*7/8)
|
n, _ = EncodeOption32(optBuf[nopt:], OptT2Rebinding, t.binding.T2)
|
||||||
nopt += n
|
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)
|
optBuf[nopt] = byte(OptEnd)
|
||||||
nopt++
|
nopt++
|
||||||
|
|
||||||
dfrm.ClearHeader()
|
dfrm.ClearHeader()
|
||||||
dfrm.SetOp(OpReply)
|
dfrm.SetOp(OpReply)
|
||||||
dfrm.SetHardware(1, 6, 0)
|
dfrm.SetHardware(1, 6, 0)
|
||||||
dfrm.SetXID(client.xid)
|
dfrm.SetXID(t.xid)
|
||||||
dfrm.SetSecs(0)
|
dfrm.SetSecs(0)
|
||||||
dfrm.SetFlags(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.SIAddr() = sv.siaddr
|
||||||
*dfrm.GIAddr() = sv.gwaddr
|
*dfrm.GIAddr() = sv.gwaddr
|
||||||
copy(dfrm.CHAddrAs6()[:], client.hwaddr[:])
|
copy(dfrm.CHAddrAs6()[:], t.hwaddr[:])
|
||||||
dfrm.SetMagicCookie(MagicCookie)
|
dfrm.SetMagicCookie(MagicCookie)
|
||||||
if carrierIsIP {
|
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 {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client.state = futureState
|
if t.respond == MsgAck {
|
||||||
|
// Transaction complete; the lease now lives in the allocator.
|
||||||
// Set server state.
|
sv.dropTxnAt(t)
|
||||||
sv.hosts[clientID] = client
|
} else {
|
||||||
sv.pending--
|
t.respond = 0 // Offer sent, await the client's REQUEST.
|
||||||
|
}
|
||||||
return OptionsOffset + nopt, nil
|
return OptionsOffset + nopt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// allocAddr allocates the next available address from the pool.
|
// leaseOf returns the first lease of a binding.
|
||||||
// If reqAddr is a valid 4-byte address within the subnet and not already assigned,
|
func leaseOf(b dhcp.Binding) (dhcp.Lease, bool) {
|
||||||
// it is preferred. Returns false if the pool is exhausted.
|
if len(b.Leases) == 0 {
|
||||||
func (sv *Server) allocAddr(reqAddr []byte) ([4]byte, bool) {
|
return dhcp.Lease{}, false
|
||||||
if len(reqAddr) == 4 {
|
|
||||||
candidate := [4]byte(reqAddr)
|
|
||||||
if sv.subnet.Contains(candidate) && candidate != sv.siaddr && !sv.isAddrAssigned(candidate) {
|
|
||||||
return candidate, true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Reject broadcast address (all host bits set).
|
return b.Leases[0], true
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sv *Server) isAddrAssigned(addr [4]byte) bool {
|
// findTxn returns the in-flight transaction for clientID, or nil.
|
||||||
for _, v := range sv.hosts {
|
func (sv *Server) findTxn(clientID []byte) *txn {
|
||||||
if v.addr == addr {
|
for i := range sv.txns {
|
||||||
return true
|
if sv.txns[i].matches(clientID) {
|
||||||
|
return &sv.txns[i]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sv *Server) getClient(clientID [36]byte) (serverEntry, bool) {
|
// upsertTxn returns the existing transaction for clientID, reusing its slot, or
|
||||||
entry, ok := sv.hosts[clientID]
|
// appends a new one. It returns nil if the table is full.
|
||||||
return entry, ok
|
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) {
|
// nextPending returns the next transaction with a queued response, or nil.
|
||||||
for k, v := range sv.hosts {
|
func (sv *Server) nextPending() *txn {
|
||||||
if v.addr == ip {
|
for i := range sv.txns {
|
||||||
return v, k, true
|
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) {
|
func getSrcIPPort(ipCarrier []byte) (srcaddr []byte, port uint16, err error) {
|
||||||
|
|||||||
@@ -239,9 +239,9 @@ func TestServerOfferContainsOptions(t *testing.T) {
|
|||||||
foundLease = true
|
foundLease = true
|
||||||
gotLease = maybeU32(data)
|
gotLease = maybeU32(data)
|
||||||
}
|
}
|
||||||
case OptRenewTimeValue:
|
case OptT1Renewal:
|
||||||
gotRenew = maybeU32(data)
|
gotRenew = maybeU32(data)
|
||||||
case OptRebindingTimeValue:
|
case OptT2Rebinding:
|
||||||
gotRebind = maybeU32(data)
|
gotRebind = maybeU32(data)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -67,8 +67,8 @@ func _() {
|
|||||||
_ = x[OptParameterRequestList-55]
|
_ = x[OptParameterRequestList-55]
|
||||||
_ = x[OptMessage-56]
|
_ = x[OptMessage-56]
|
||||||
_ = x[OptMaximumMessageSize-57]
|
_ = x[OptMaximumMessageSize-57]
|
||||||
_ = x[OptRenewTimeValue-58]
|
_ = x[OptT1Renewal-58]
|
||||||
_ = x[OptRebindingTimeValue-59]
|
_ = x[OptT2Rebinding-59]
|
||||||
_ = x[OptClientIdentifier-60]
|
_ = x[OptClientIdentifier-60]
|
||||||
_ = x[OptClientIdentifier1-61]
|
_ = 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
|
field.Class = FieldClassSize
|
||||||
|
|
||||||
// Time/duration options (seconds).
|
// 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.OptTimeOffset, dhcpv4.OptARPCacheTimeout, dhcpv4.OptPathMTUAgingTimeout,
|
||||||
dhcpv4.OptTCPKeepaliveInterval, dhcpv4.OptDefaultIPTTL, dhcpv4.OptDefaultTCPTimetoLive:
|
dhcpv4.OptTCPKeepaliveInterval, dhcpv4.OptDefaultIPTTL, dhcpv4.OptDefaultTCPTimetoLive:
|
||||||
field.Class = FieldClassTimestamp
|
field.Class = FieldClassTimestamp
|
||||||
|
|||||||
Reference in New Issue
Block a user