blob: ad4c9dd4b61d806130111bf877551592ee08d939 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
Lorenz Brun56a7ae62020-10-29 11:03:30 +01002// SPDX-License-Identifier: Apache-2.0
Lorenz Brun56a7ae62020-10-29 11:03:30 +01003
Serge Bazanski216fe7b2021-05-21 18:36:16 +02004// Package dhcp4c implements a DHCPv4 Client as specified in RFC2131 (with some
5// notable deviations). It implements only the DHCP state machine itself, any
6// configuration other than the interface IP address (which is always assigned
7// in DHCP and necessary for the protocol to work) is exposed as
8// [informers/observables/watchable variables/???] to consumers who then deal
9// with it.
Lorenz Brun56a7ae62020-10-29 11:03:30 +010010package dhcp4c
11
12import (
13 "context"
14 "crypto/rand"
15 "errors"
16 "fmt"
17 "io"
18 "math"
19 "net"
20 "time"
21
22 "github.com/cenkalti/backoff/v4"
23 "github.com/insomniacslk/dhcp/dhcpv4"
24 "github.com/insomniacslk/dhcp/iana"
25
Jan Schär07a39e22025-09-04 11:16:59 +020026 "source.monogon.dev/osbase/net/dhcp4c/transport"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020027 "source.monogon.dev/osbase/supervisor"
Lorenz Brun56a7ae62020-10-29 11:03:30 +010028)
29
30type state int
31
32const (
Serge Bazanski216fe7b2021-05-21 18:36:16 +020033 // stateDiscovering sends broadcast DHCPDISCOVER messages to the network
34 // and waits for either a DHCPOFFER or (in case of Rapid Commit) DHCPACK.
Lorenz Brun56a7ae62020-10-29 11:03:30 +010035 stateDiscovering state = iota
Serge Bazanski216fe7b2021-05-21 18:36:16 +020036 // stateRequesting sends broadcast DHCPREQUEST messages containing the
37 // server identifier for the selected lease and waits for a DHCPACK or a
38 // DHCPNAK. If it doesn't get either it transitions back into discovering.
Lorenz Brun56a7ae62020-10-29 11:03:30 +010039 stateRequesting
Serge Bazanski216fe7b2021-05-21 18:36:16 +020040 // stateBound just waits until RenewDeadline (derived from RenewTimeValue,
41 // half the lifetime by default) expires.
Lorenz Brun56a7ae62020-10-29 11:03:30 +010042 stateBound
Serge Bazanski216fe7b2021-05-21 18:36:16 +020043 // stateRenewing sends unicast DHCPREQUEST messages to the
44 // currently-selected server and waits for either a DHCPACK or DHCPNAK
45 // message. On DHCPACK it transitions to bound, otherwise to discovering.
Lorenz Brun56a7ae62020-10-29 11:03:30 +010046 stateRenewing
Serge Bazanski216fe7b2021-05-21 18:36:16 +020047 // stateRebinding sends broadcast DHCPREQUEST messages to the network and
48 // waits for either a DHCPACK or DHCPNAK from any server. Response
49 // processing is identical to stateRenewing.
Lorenz Brun56a7ae62020-10-29 11:03:30 +010050 stateRebinding
51)
52
53func (s state) String() string {
54 switch s {
55 case stateDiscovering:
56 return "DISCOVERING"
57 case stateRequesting:
58 return "REQUESTING"
59 case stateBound:
60 return "BOUND"
61 case stateRenewing:
62 return "RENEWING"
63 case stateRebinding:
64 return "REBINDING"
65 default:
66 return "INVALID"
67 }
68}
69
Serge Bazanski216fe7b2021-05-21 18:36:16 +020070// This only requests SubnetMask and IPAddressLeaseTime as renewal and
71// rebinding times are fine if they are just defaulted. They are respected (if
72// valid, otherwise they are clamped to the nearest valid value) if sent by the
73// server.
Lorenz Brun56a7ae62020-10-29 11:03:30 +010074var internalOptions = dhcpv4.OptionCodeList{dhcpv4.OptionSubnetMask, dhcpv4.OptionIPAddressLeaseTime}
75
Serge Bazanski216fe7b2021-05-21 18:36:16 +020076// Transport represents a mechanism over which DHCP messages can be exchanged
77// with a server.
Lorenz Brun56a7ae62020-10-29 11:03:30 +010078type Transport interface {
Serge Bazanski216fe7b2021-05-21 18:36:16 +020079 // Send attempts to send the given DHCP payload message to the transport
80 // target once. An empty return value does not indicate that the message
81 // was successfully received.
Lorenz Brun56a7ae62020-10-29 11:03:30 +010082 Send(payload *dhcpv4.DHCPv4) error
Serge Bazanski216fe7b2021-05-21 18:36:16 +020083 // SetReceiveDeadline sets a deadline for Receive() calls after which they
Tim Windelschmidt51daf252024-04-18 23:18:43 +020084 // return with ErrDeadlineExceeded
Lorenz Brun56a7ae62020-10-29 11:03:30 +010085 SetReceiveDeadline(time.Time) error
Serge Bazanski216fe7b2021-05-21 18:36:16 +020086 // Receive waits for a DHCP message to arrive and returns it. If the
87 // deadline expires without a message arriving it will return
Tim Windelschmidt51daf252024-04-18 23:18:43 +020088 // ErrDeadlineExceeded. If the message is completely malformed it will an
Serge Bazanski216fe7b2021-05-21 18:36:16 +020089 // instance of InvalidMessageError.
Lorenz Brun56a7ae62020-10-29 11:03:30 +010090 Receive() (*dhcpv4.DHCPv4, error)
Serge Bazanski216fe7b2021-05-21 18:36:16 +020091 // Close closes the given transport. Calls to any of the above methods will
92 // fail if the transport is closed. Specific transports can be reopened
93 // after being closed.
Lorenz Brun56a7ae62020-10-29 11:03:30 +010094 Close() error
95}
96
Serge Bazanski216fe7b2021-05-21 18:36:16 +020097// UnicastTransport represents a mechanism over which DHCP messages can be
98// exchanged with a single server over an arbitrary IPv4-based network.
99// Implementers need to support servers running outside the local network via a
100// router.
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100101type UnicastTransport interface {
102 Transport
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200103 // Open connects the transport to a new unicast target. Can only be called
104 // after calling Close() or after creating a new transport.
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100105 Open(serverIP, bindIP net.IP) error
106}
107
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200108// BroadcastTransport represents a mechanism over which DHCP messages can be
109// exchanged with all servers on a Layer 2 broadcast domain. Implementers need
110// to support sending and receiving messages without any IP being configured on
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100111// the interface.
112type BroadcastTransport interface {
113 Transport
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200114 // Open connects the transport. Can only be called after calling Close() or
115 // after creating a new transport.
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100116 Open() error
117}
118
Jan Schärba404a62024-07-11 10:46:27 +0200119type LeaseCallback func(*Lease) error
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100120
121// Client implements a DHCPv4 client.
122//
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200123// Note that the size of all data sent to the server (RequestedOptions,
124// ClientIdentifier, VendorClassIdentifier and ExtraRequestOptions) should be
125// kept reasonably small (<500 bytes) in order to maximize the chance that
126// requests can be properly transmitted.
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100127type Client struct {
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200128 // RequestedOptions contains a list of extra options this client is
129 // interested in
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100130 RequestedOptions dhcpv4.OptionCodeList
131
132 // ClientIdentifier is used by the DHCP server to identify this client.
133 // If empty, on Ethernet the MAC address is used instead.
134 ClientIdentifier []byte
135
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200136 // VendorClassIdentifier is used by the DHCP server to identify options
137 // specific to this type of clients and to populate the vendor-specific
138 // option (43).
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100139 VendorClassIdentifier string
140
141 // ExtraRequestOptions are extra options sent to the server.
142 ExtraRequestOptions dhcpv4.Options
143
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200144 // Backoff strategies for each state. These all have sane defaults,
145 // override them only if necessary.
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100146 DiscoverBackoff backoff.BackOff
147 AcceptOfferBackoff backoff.BackOff
148 RenewBackoff backoff.BackOff
149 RebindBackoff backoff.BackOff
150
151 state state
152
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100153 iface *net.Interface
154
155 // now can be used to override time for testing
156 now func() time.Time
157
158 // LeaseCallback is called every time a lease is aquired, renewed or lost
159 LeaseCallback LeaseCallback
160
161 // Valid in states Discovering, Requesting, Rebinding
162 broadcastConn BroadcastTransport
163
164 // Valid in states Requesting
165 offer *dhcpv4.DHCPv4
166
167 // Valid in states Bound, Renewing
168 unicastConn UnicastTransport
169
170 // Valid in states Bound, Renewing, Rebinding
171 lease *dhcpv4.DHCPv4
172 leaseDeadline time.Time
173 leaseBoundDeadline time.Time
174 leaseRenewDeadline time.Time
175}
176
Jan Schär2a3e3362024-07-11 18:08:18 +0200177// defaultBackoffOpts can be passed to NewExponentialBackOff and configures it
178// to retry infinitely and use a DHCP-appropriate InitialInterval.
179func defaultBackoffOpts(b *backoff.ExponentialBackOff) {
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100180 b.MaxElapsedTime = 0 // No Timeout
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200181 // Lots of servers wait 1s for existing users of an IP. Wait at least for
182 // that and keep some slack for randomization, communication and processing
183 // overhead.
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100184 b.InitialInterval = 1400 * time.Millisecond
185 b.MaxInterval = 30 * time.Second
186 b.RandomizationFactor = 0.2
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100187}
188
189// NewClient instantiates (but doesn't start) a new DHCPv4 client.
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200190// To have a working client it's required to set LeaseCallback to something
191// that is capable of configuring the IP address on the given interface. Unless
192// managed through external means like a routing protocol, setting the default
193// route is also required. A simple example with the callback package thus
194// looks like this:
Jan Schärd6a88022024-03-18 17:03:37 +0100195//
196// c := dhcp4c.NewClient(yourInterface)
197// c.LeaseCallback = callback.Compose(callback.ManageIP(yourInterface), callback.ManageDefaultRoute(yourInterface))
198// c.Run(ctx)
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100199func NewClient(iface *net.Interface) (*Client, error) {
200 broadcastConn := transport.NewBroadcastTransport(iface)
201
202 // broadcastConn needs to be open in stateDiscovering
203 if err := broadcastConn.Open(); err != nil {
204 return nil, fmt.Errorf("failed to create DHCP broadcast transport: %w", err)
205 }
206
Jan Schär2a3e3362024-07-11 18:08:18 +0200207 discoverBackoff := backoff.NewExponentialBackOff(defaultBackoffOpts)
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100208
Jan Schär2a3e3362024-07-11 18:08:18 +0200209 acceptOfferBackoff := backoff.NewExponentialBackOff(defaultBackoffOpts,
210 // Abort after 30s and go back to discovering
211 backoff.WithMaxElapsedTime(30*time.Second))
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100212
Jan Schär2a3e3362024-07-11 18:08:18 +0200213 renewBackoff := backoff.NewExponentialBackOff(defaultBackoffOpts,
214 // Increase maximum interval to reduce chatter when the server is down
215 backoff.WithMaxInterval(5*time.Minute))
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100216
Jan Schär2a3e3362024-07-11 18:08:18 +0200217 rebindBackoff := backoff.NewExponentialBackOff(defaultBackoffOpts,
218 // Increase maximum interval to reduce chatter when the server is down
219 backoff.WithMaxInterval(5*time.Minute))
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100220
Lorenz Brun1ce80392024-05-27 20:06:43 +0000221 // Check if the hardware address contains at least one non-zero value.
222 // This exists to catch undefined/non-supplied hardware address values,
223 // it does not check for L2 protocol-specific hardware address constraints.
224 hasValidHWAddr := false
225 for _, b := range iface.HardwareAddr {
226 if b != 0x00 {
227 hasValidHWAddr = true
228 break
229 }
230 }
231 if !hasValidHWAddr {
232 return nil, fmt.Errorf("iface HardwareAddr is invalid (only zeroes or invalid length): %x", iface.HardwareAddr)
233 }
234
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100235 return &Client{
Jan Schär2a3e3362024-07-11 18:08:18 +0200236 state: stateDiscovering,
237 broadcastConn: broadcastConn,
238 unicastConn: transport.NewUnicastTransport(iface),
239 iface: iface,
240 RequestedOptions: dhcpv4.OptionCodeList{},
241 now: time.Now,
242 DiscoverBackoff: discoverBackoff,
243 AcceptOfferBackoff: acceptOfferBackoff,
244 RenewBackoff: renewBackoff,
245 RebindBackoff: rebindBackoff,
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100246 }, nil
247}
248
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200249// acceptableLease checks if the given lease is valid enough to even be
250// processed. This is intentionally not exposed to users because under certain
251// cirumstances it can end up acquiring all available IP addresses from a
252// server.
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100253func (c *Client) acceptableLease(offer *dhcpv4.DHCPv4) bool {
254 // RFC2131 Section 4.3.1 Table 3
255 if offer.ServerIdentifier() == nil || offer.ServerIdentifier().To4() == nil {
256 return false
257 }
258 // RFC2131 Section 4.3.1 Table 3
259 // Minimum representable lease time is 1s (Section 1.1)
260 if offer.IPAddressLeaseTime(0) < 1*time.Second {
261 return false
262 }
263
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200264 // Ignore IPs that are in no way valid for an interface (multicast,
265 // loopback, ...)
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100266 if offer.YourIPAddr.To4() == nil || (!offer.YourIPAddr.IsGlobalUnicast() && !offer.YourIPAddr.IsLinkLocalUnicast()) {
267 return false
268 }
269
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200270 // Technically the options Requested IP address, Parameter request list,
271 // Client identifier and Maximum message size should be refused (MUST NOT),
272 // but in the interest of interopatibilty let's simply remove them if they
273 // are present.
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100274 delete(offer.Options, dhcpv4.OptionRequestedIPAddress.Code())
275 delete(offer.Options, dhcpv4.OptionParameterRequestList.Code())
276 delete(offer.Options, dhcpv4.OptionClientIdentifier.Code())
277 delete(offer.Options, dhcpv4.OptionMaximumDHCPMessageSize.Code())
278
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200279 // Clamp rebindinding times longer than the lease time. Otherwise the state
280 // machine might misbehave.
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100281 if offer.IPAddressRebindingTime(0) > offer.IPAddressLeaseTime(0) {
282 offer.UpdateOption(dhcpv4.OptGeneric(dhcpv4.OptionRebindingTimeValue, dhcpv4.Duration(offer.IPAddressLeaseTime(0)).ToBytes()))
283 }
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200284 // Clamp renewal times longer than the rebinding time. Otherwise the state
285 // machine might misbehave.
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100286 if offer.IPAddressRenewalTime(0) > offer.IPAddressRebindingTime(0) {
287 offer.UpdateOption(dhcpv4.OptGeneric(dhcpv4.OptionRenewTimeValue, dhcpv4.Duration(offer.IPAddressRebindingTime(0)).ToBytes()))
288 }
289
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200290 // Normalize two options that can be represented either inline or as
291 // options.
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100292 if len(offer.ServerHostName) > 0 {
293 offer.Options[uint8(dhcpv4.OptionTFTPServerName)] = []byte(offer.ServerHostName)
294 }
295 if len(offer.BootFileName) > 0 {
296 offer.Options[uint8(dhcpv4.OptionBootfileName)] = []byte(offer.BootFileName)
297 }
298
299 // Normalize siaddr to option 150 (see RFC5859)
300 if len(offer.GetOneOption(dhcpv4.OptionTFTPServerAddress)) == 0 {
301 if offer.ServerIPAddr.To4() != nil && (offer.ServerIPAddr.IsGlobalUnicast() || offer.ServerIPAddr.IsLinkLocalUnicast()) {
302 offer.Options[uint8(dhcpv4.OptionTFTPServerAddress)] = offer.ServerIPAddr.To4()
303 }
304 }
305
306 return true
307}
308
309func earliestDeadline(dl1, dl2 time.Time) time.Time {
310 if dl1.Before(dl2) {
311 return dl1
312 } else {
313 return dl2
314 }
315}
316
317// newXID generates a new transaction ID
318func (c *Client) newXID() (dhcpv4.TransactionID, error) {
319 var xid dhcpv4.TransactionID
320 if _, err := io.ReadFull(rand.Reader, xid[:]); err != nil {
321 return xid, fmt.Errorf("cannot read randomness for transaction ID: %w", err)
322 }
323 return xid, nil
324}
325
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200326// As most servers out there cannot do reassembly, let's just hope for the best
327// and provide the local interface MTU. If the packet is too big it won't work
328// anyways. Also clamp to the biggest representable MTU in DHCPv4 (2 bytes
329// unsigned int).
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100330func (c *Client) maxMsgSize() uint16 {
331 if c.iface.MTU < math.MaxUint16 {
332 return uint16(c.iface.MTU)
333 } else {
334 return math.MaxUint16
335 }
336}
337
338// newMsg creates a new DHCP message of a given type and adds common options.
339func (c *Client) newMsg(t dhcpv4.MessageType) (*dhcpv4.DHCPv4, error) {
340 xid, err := c.newXID()
341 if err != nil {
342 return nil, err
343 }
344 opts := make(dhcpv4.Options)
345 opts.Update(dhcpv4.OptMessageType(t))
346 if len(c.ClientIdentifier) > 0 {
347 opts.Update(dhcpv4.OptClientIdentifier(c.ClientIdentifier))
348 }
349 if t == dhcpv4.MessageTypeDiscover || t == dhcpv4.MessageTypeRequest || t == dhcpv4.MessageTypeInform {
Jan Schärd6a88022024-03-18 17:03:37 +0100350 opts.Update(dhcpv4.OptParameterRequestList(append(append(dhcpv4.OptionCodeList(nil), c.RequestedOptions...), internalOptions...)...))
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100351 opts.Update(dhcpv4.OptMaxMessageSize(c.maxMsgSize()))
352 if c.VendorClassIdentifier != "" {
353 opts.Update(dhcpv4.OptClassIdentifier(c.VendorClassIdentifier))
354 }
355 for opt, val := range c.ExtraRequestOptions {
356 opts[opt] = val
357 }
358 }
359 return &dhcpv4.DHCPv4{
360 OpCode: dhcpv4.OpcodeBootRequest,
361 HWType: iana.HWTypeEthernet,
362 ClientHWAddr: c.iface.HardwareAddr,
363 HopCount: 0,
364 TransactionID: xid,
365 NumSeconds: 0,
366 Flags: 0,
367 ClientIPAddr: net.IPv4zero,
368 YourIPAddr: net.IPv4zero,
369 ServerIPAddr: net.IPv4zero,
370 GatewayIPAddr: net.IPv4zero,
371 Options: opts,
372 }, nil
373}
374
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200375// transactionStateSpec describes a state which is driven by a DHCP message
376// transaction (sending a specific message and then transitioning into a
377// different state depending on the received messages)
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100378type transactionStateSpec struct {
379 // ctx is a context for canceling the process
380 ctx context.Context
381
382 // transport is used to send and receive messages in this state
383 transport Transport
384
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200385 // stateDeadline is a fixed external deadline for how long the FSM can
386 // remain in this state.
387 // If it's exceeded the stateDeadlineExceeded callback is called and
388 // responsible for transitioning out of this state. It can be left empty to
389 // signal that there's no external deadline for the state.
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100390 stateDeadline time.Time
391
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200392 // backoff controls how long to wait for answers until handing control back
393 // to the FSM.
394 // Since the FSM hasn't advanced until then this means we just get called
395 // again and retransmit.
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100396 backoff backoff.BackOff
397
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200398 // requestType is the type of DHCP request sent out in this state. This is
399 // used to populate the default options for the message.
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100400 requestType dhcpv4.MessageType
401
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200402 // setExtraOptions can modify the request and set extra options before
403 // transmitting. Returning an error here aborts the FSM an can be used to
404 // terminate when no valid request can be constructed.
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100405 setExtraOptions func(msg *dhcpv4.DHCPv4) error
406
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200407 // handleMessage gets called for every parseable (not necessarily valid)
408 // DHCP message received by the transport. It should return an error for
409 // every message that doesn't advance the state machine and no error for
410 // every one that does. It is responsible for advancing the FSM if the
411 // required information is present.
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100412 handleMessage func(msg *dhcpv4.DHCPv4, sentTime time.Time) error
413
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200414 // stateDeadlineExceeded gets called if either the backoff returns
415 // backoff.Stop or the stateDeadline runs out. It is responsible for
416 // advancing the FSM into the next state.
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100417 stateDeadlineExceeded func() error
418}
419
420func (c *Client) runTransactionState(s transactionStateSpec) error {
421 sentTime := c.now()
422 msg, err := c.newMsg(s.requestType)
423 if err != nil {
424 return fmt.Errorf("failed to get new DHCP message: %w", err)
425 }
426 if err := s.setExtraOptions(msg); err != nil {
427 return fmt.Errorf("failed to create DHCP message: %w", err)
428 }
429
430 wait := s.backoff.NextBackOff()
431 if wait == backoff.Stop {
432 return s.stateDeadlineExceeded()
433 }
434
435 receiveDeadline := sentTime.Add(wait)
436 if !s.stateDeadline.IsZero() {
437 receiveDeadline = earliestDeadline(s.stateDeadline, receiveDeadline)
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100438
Jan Schär2a3e3362024-07-11 18:08:18 +0200439 // Jump out if deadline expires in less than 10ms. Minimum lease time is 1s
440 // and if we have less than 10ms to wait for an answer before switching
441 // state it makes no sense to send out another request. This nearly
442 // eliminates the problem of sending two different requests back-to-back.
443 if s.stateDeadline.Add(-10 * time.Millisecond).Before(sentTime) {
444 return s.stateDeadlineExceeded()
445 }
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100446 }
447
448 if err := s.transport.Send(msg); err != nil {
449 return fmt.Errorf("failed to send message: %w", err)
450 }
451
452 if err := s.transport.SetReceiveDeadline(receiveDeadline); err != nil {
453 return fmt.Errorf("failed to set deadline: %w", err)
454 }
455
456 for {
457 offer, err := s.transport.Receive()
458 select {
459 case <-s.ctx.Done():
460 c.cleanup()
461 return s.ctx.Err()
462 default:
463 }
Tim Windelschmidt51daf252024-04-18 23:18:43 +0200464 if errors.Is(err, transport.ErrDeadlineExceeded) {
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100465 return nil
466 }
467 var e transport.InvalidMessageError
468 if errors.As(err, &e) {
469 // Packet couldn't be read. Maybe log at some point in the future.
470 continue
471 }
472 if err != nil {
473 return fmt.Errorf("failed to receive packet: %w", err)
474 }
475 if offer.TransactionID != msg.TransactionID { // Not our transaction
476 continue
477 }
478 err = s.handleMessage(offer, sentTime)
479 if err == nil {
480 return nil
Tim Windelschmidt513df182024-04-18 23:44:50 +0200481 } else if !errors.Is(err, ErrInvalidMsg) {
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100482 return err
483 }
484 }
485}
486
Tim Windelschmidt513df182024-04-18 23:44:50 +0200487var ErrInvalidMsg = errors.New("invalid message")
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100488
489func (c *Client) runState(ctx context.Context) error {
490 switch c.state {
491 case stateDiscovering:
492 return c.runTransactionState(transactionStateSpec{
493 ctx: ctx,
494 transport: c.broadcastConn,
495 backoff: c.DiscoverBackoff,
496 requestType: dhcpv4.MessageTypeDiscover,
497 setExtraOptions: func(msg *dhcpv4.DHCPv4) error {
498 msg.UpdateOption(dhcpv4.OptGeneric(dhcpv4.OptionRapidCommit, []byte{}))
499 return nil
500 },
501 handleMessage: func(offer *dhcpv4.DHCPv4, sentTime time.Time) error {
502 switch offer.MessageType() {
503 case dhcpv4.MessageTypeOffer:
504 if c.acceptableLease(offer) {
505 c.offer = offer
506 c.AcceptOfferBackoff.Reset()
507 c.state = stateRequesting
508 return nil
509 }
510 case dhcpv4.MessageTypeAck:
511 if c.acceptableLease(offer) {
512 return c.transitionToBound(offer, sentTime)
513 }
514 }
Tim Windelschmidt513df182024-04-18 23:44:50 +0200515 return ErrInvalidMsg
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100516 },
517 })
518 case stateRequesting:
519 return c.runTransactionState(transactionStateSpec{
520 ctx: ctx,
521 transport: c.broadcastConn,
522 backoff: c.AcceptOfferBackoff,
523 requestType: dhcpv4.MessageTypeRequest,
524 setExtraOptions: func(msg *dhcpv4.DHCPv4) error {
525 msg.UpdateOption(dhcpv4.OptServerIdentifier(c.offer.ServerIdentifier()))
526 msg.TransactionID = c.offer.TransactionID
527 msg.UpdateOption(dhcpv4.OptRequestedIPAddress(c.offer.YourIPAddr))
528 return nil
529 },
530 handleMessage: func(msg *dhcpv4.DHCPv4, sentTime time.Time) error {
531 switch msg.MessageType() {
532 case dhcpv4.MessageTypeAck:
533 if c.acceptableLease(msg) {
534 return c.transitionToBound(msg, sentTime)
535 }
536 case dhcpv4.MessageTypeNak:
537 c.requestingToDiscovering()
538 return nil
539 }
Tim Windelschmidt513df182024-04-18 23:44:50 +0200540 return ErrInvalidMsg
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100541 },
542 stateDeadlineExceeded: func() error {
543 c.requestingToDiscovering()
544 return nil
545 },
546 })
547 case stateBound:
548 select {
549 case <-time.After(c.leaseBoundDeadline.Sub(c.now())):
550 c.state = stateRenewing
551 c.RenewBackoff.Reset()
552 return nil
553 case <-ctx.Done():
554 c.cleanup()
555 return ctx.Err()
556 }
557 case stateRenewing:
558 return c.runTransactionState(transactionStateSpec{
559 ctx: ctx,
560 transport: c.unicastConn,
561 backoff: c.RenewBackoff,
562 requestType: dhcpv4.MessageTypeRequest,
563 stateDeadline: c.leaseRenewDeadline,
564 setExtraOptions: func(msg *dhcpv4.DHCPv4) error {
565 msg.ClientIPAddr = c.lease.YourIPAddr
566 return nil
567 },
568 handleMessage: func(ack *dhcpv4.DHCPv4, sentTime time.Time) error {
569 switch ack.MessageType() {
570 case dhcpv4.MessageTypeAck:
571 if c.acceptableLease(ack) {
572 return c.transitionToBound(ack, sentTime)
573 }
574 case dhcpv4.MessageTypeNak:
575 return c.leaseToDiscovering()
576 }
Tim Windelschmidt513df182024-04-18 23:44:50 +0200577 return ErrInvalidMsg
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100578 },
579 stateDeadlineExceeded: func() error {
580 c.state = stateRebinding
581 if err := c.switchToBroadcast(); err != nil {
582 return fmt.Errorf("failed to switch to broadcast: %w", err)
583 }
584 c.RebindBackoff.Reset()
585 return nil
586 },
587 })
588 case stateRebinding:
589 return c.runTransactionState(transactionStateSpec{
590 ctx: ctx,
591 transport: c.broadcastConn,
592 backoff: c.RebindBackoff,
593 stateDeadline: c.leaseDeadline,
594 requestType: dhcpv4.MessageTypeRequest,
595 setExtraOptions: func(msg *dhcpv4.DHCPv4) error {
596 msg.ClientIPAddr = c.lease.YourIPAddr
597 return nil
598 },
599 handleMessage: func(ack *dhcpv4.DHCPv4, sentTime time.Time) error {
600 switch ack.MessageType() {
601 case dhcpv4.MessageTypeAck:
602 if c.acceptableLease(ack) {
603 return c.transitionToBound(ack, sentTime)
604 }
605 case dhcpv4.MessageTypeNak:
606 return c.leaseToDiscovering()
607 }
Tim Windelschmidt513df182024-04-18 23:44:50 +0200608 return ErrInvalidMsg
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100609 },
610 stateDeadlineExceeded: func() error {
611 return c.leaseToDiscovering()
612 },
613 })
614 }
615 return errors.New("state machine in invalid state")
616}
617
618func (c *Client) Run(ctx context.Context) error {
619 if c.LeaseCallback == nil {
620 panic("LeaseCallback must be set before calling Run")
621 }
622 logger := supervisor.Logger(ctx)
623 for {
624 oldState := c.state
625 if err := c.runState(ctx); err != nil {
626 return err
627 }
628 if c.state != oldState {
629 logger.Infof("%s => %s", oldState, c.state)
630 }
631 }
632}
633
634func (c *Client) cleanup() {
635 c.unicastConn.Close()
636 if c.lease != nil {
Jan Schärba404a62024-07-11 10:46:27 +0200637 c.LeaseCallback(nil)
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100638 }
639 c.broadcastConn.Close()
640}
641
642func (c *Client) requestingToDiscovering() {
643 c.offer = nil
644 c.DiscoverBackoff.Reset()
645 c.state = stateDiscovering
646}
647
648func (c *Client) leaseToDiscovering() error {
649 if c.state == stateRenewing {
650 if err := c.switchToBroadcast(); err != nil {
651 return err
652 }
653 }
654 c.state = stateDiscovering
Jan Schärba404a62024-07-11 10:46:27 +0200655 c.lease = nil
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100656 c.DiscoverBackoff.Reset()
Jan Schärba404a62024-07-11 10:46:27 +0200657 if err := c.LeaseCallback(nil); err != nil {
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100658 return fmt.Errorf("lease callback failed: %w", err)
659 }
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100660 return nil
661}
662
663func leaseFromAck(ack *dhcpv4.DHCPv4, expiresAt time.Time) *Lease {
664 if ack == nil {
665 return nil
666 }
667 return &Lease{Options: ack.Options, AssignedIP: ack.YourIPAddr, ExpiresAt: expiresAt}
668}
669
670func (c *Client) transitionToBound(ack *dhcpv4.DHCPv4, sentTime time.Time) error {
671 // Guaranteed to exist, leases without a lease time are filtered
672 leaseTime := ack.IPAddressLeaseTime(0)
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100673 c.leaseDeadline = sentTime.Add(leaseTime)
674 c.leaseBoundDeadline = sentTime.Add(ack.IPAddressRenewalTime(time.Duration(float64(leaseTime) * 0.5)))
675 c.leaseRenewDeadline = sentTime.Add(ack.IPAddressRebindingTime(time.Duration(float64(leaseTime) * 0.85)))
676
Jan Schärba404a62024-07-11 10:46:27 +0200677 if err := c.LeaseCallback(leaseFromAck(ack, c.leaseDeadline)); err != nil {
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100678 return fmt.Errorf("lease callback failed: %w", err)
679 }
680
681 if c.state != stateRenewing {
682 if err := c.switchToUnicast(ack.ServerIdentifier(), ack.YourIPAddr); err != nil {
683 return fmt.Errorf("failed to switch transports: %w", err)
684 }
685 }
686 c.state = stateBound
687 c.lease = ack
688 return nil
689}
690
691func (c *Client) switchToUnicast(serverIP, bindIP net.IP) error {
692 if err := c.broadcastConn.Close(); err != nil {
693 return fmt.Errorf("failed to close broadcast transport: %w", err)
694 }
695 if err := c.unicastConn.Open(serverIP, bindIP); err != nil {
696 return fmt.Errorf("failed to open unicast transport: %w", err)
697 }
698 return nil
699}
700
701func (c *Client) switchToBroadcast() error {
702 if err := c.unicastConn.Close(); err != nil {
703 return fmt.Errorf("failed to close unicast transport: %w", err)
704 }
705 if err := c.broadcastConn.Open(); err != nil {
706 return fmt.Errorf("failed to open broadcast transport: %w", err)
707 }
708 return nil
709}