blob: 0811890a60f57a283e8d68f5e945dd0c34202946 [file] [log] [blame]
Lorenz Brun56a7ae62020-10-29 11:03:30 +01001// Copyright 2020 The Monogon Project Authors.
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17// Package dhcp4c implements a DHCPv4 Client as specified in RFC2131 (with some notable deviations).
18// It implements only the DHCP state machine itself, any configuration other than the interface IP
19// address (which is always assigned in DHCP and necessary for the protocol to work) is exposed
20// as [informers/observables/watchable variables/???] to consumers who then deal with it.
21package dhcp4c
22
23import (
24 "context"
25 "crypto/rand"
26 "errors"
27 "fmt"
28 "io"
29 "math"
30 "net"
31 "time"
32
33 "github.com/cenkalti/backoff/v4"
34 "github.com/insomniacslk/dhcp/dhcpv4"
35 "github.com/insomniacslk/dhcp/iana"
36
37 "git.monogon.dev/source/nexantic.git/core/internal/common/supervisor"
38 "git.monogon.dev/source/nexantic.git/core/pkg/dhcp4c/transport"
39)
40
41type state int
42
43const (
44 // stateDiscovering sends broadcast DHCPDISCOVER messages to the network and waits for either a DHCPOFFER or
45 // (in case of Rapid Commit) DHCPACK.
46 stateDiscovering state = iota
47 // stateRequesting sends broadcast DHCPREQUEST messages containing the server identifier for the selected lease and
48 // waits for a DHCPACK or a DHCPNAK. If it doesn't get either it transitions back into discovering.
49 stateRequesting
50 // stateBound just waits until RenewDeadline (derived from RenewTimeValue, half the lifetime by default) expires.
51 stateBound
52 // stateRenewing sends unicast DHCPREQUEST messages to the currently-selected server and waits for either a DHCPACK
53 // or DHCPNAK message. On DHCPACK it transitions to bound, otherwise to discovering.
54 stateRenewing
55 // stateRebinding sends broadcast DHCPREQUEST messages to the network and waits for either a DHCPACK or DHCPNAK from
56 // any server. Response processing is identical to stateRenewing.
57 stateRebinding
58)
59
60func (s state) String() string {
61 switch s {
62 case stateDiscovering:
63 return "DISCOVERING"
64 case stateRequesting:
65 return "REQUESTING"
66 case stateBound:
67 return "BOUND"
68 case stateRenewing:
69 return "RENEWING"
70 case stateRebinding:
71 return "REBINDING"
72 default:
73 return "INVALID"
74 }
75}
76
77// This only requests SubnetMask and IPAddressLeaseTime as renewal and rebinding times are fine if
78// they are just defaulted. They are respected (if valid, otherwise they are clamped to the nearest
79// valid value) if sent by the server.
80var internalOptions = dhcpv4.OptionCodeList{dhcpv4.OptionSubnetMask, dhcpv4.OptionIPAddressLeaseTime}
81
82// Transport represents a mechanism over which DHCP messages can be exchanged with a server.
83type Transport interface {
84 // Send attempts to send the given DHCP payload message to the transport target once. An empty return value
85 // does not indicate that the message was successfully received.
86 Send(payload *dhcpv4.DHCPv4) error
87 // SetReceiveDeadline sets a deadline for Receive() calls after which they return with DeadlineExceededErr
88 SetReceiveDeadline(time.Time) error
89 // Receive waits for a DHCP message to arrive and returns it. If the deadline expires without a message arriving
90 // it will return DeadlineExceededErr. If the message is completely malformed it will an instance of
91 // InvalidMessageError.
92 Receive() (*dhcpv4.DHCPv4, error)
93 // Close closes the given transport. Calls to any of the above methods will fail if the transport is closed.
94 // Specific transports can be reopened after being closed.
95 Close() error
96}
97
98// UnicastTransport represents a mechanism over which DHCP messages can be exchanged with a single server over an
99// arbitrary IPv4-based network. Implementers need to support servers running outside the local network via a router.
100type UnicastTransport interface {
101 Transport
102 // Open connects the transport to a new unicast target. Can only be called after calling Close() or after creating
103 // a new transport.
104 Open(serverIP, bindIP net.IP) error
105}
106
107// BroadcastTransport represents a mechanism over which DHCP messages can be exchanged with all servers on a Layer 2
108// broadcast domain. Implementers need to support sending and receiving messages without any IP being configured on
109// the interface.
110type BroadcastTransport interface {
111 Transport
112 // Open connects the transport. Can only be called after calling Close() or after creating a new transport.
113 Open() error
114}
115
116type LeaseCallback func(old, new *Lease) error
117
118// Client implements a DHCPv4 client.
119//
120// Note that the size of all data sent to the server (RequestedOptions, ClientIdentifier,
121// VendorClassIdentifier and ExtraRequestOptions) should be kept reasonably small (<500 bytes) in
122// order to maximize the chance that requests can be properly transmitted.
123type Client struct {
124 // RequestedOptions contains a list of extra options this client is interested in
125 RequestedOptions dhcpv4.OptionCodeList
126
127 // ClientIdentifier is used by the DHCP server to identify this client.
128 // If empty, on Ethernet the MAC address is used instead.
129 ClientIdentifier []byte
130
131 // VendorClassIdentifier is used by the DHCP server to identify options specific to this type of
132 // clients and to populate the vendor-specific option (43).
133 VendorClassIdentifier string
134
135 // ExtraRequestOptions are extra options sent to the server.
136 ExtraRequestOptions dhcpv4.Options
137
138 // Backoff strategies for each state. These all have sane defaults, override them only if
139 // necessary.
140 DiscoverBackoff backoff.BackOff
141 AcceptOfferBackoff backoff.BackOff
142 RenewBackoff backoff.BackOff
143 RebindBackoff backoff.BackOff
144
145 state state
146
147 lastBoundTransition time.Time
148
149 iface *net.Interface
150
151 // now can be used to override time for testing
152 now func() time.Time
153
154 // LeaseCallback is called every time a lease is aquired, renewed or lost
155 LeaseCallback LeaseCallback
156
157 // Valid in states Discovering, Requesting, Rebinding
158 broadcastConn BroadcastTransport
159
160 // Valid in states Requesting
161 offer *dhcpv4.DHCPv4
162
163 // Valid in states Bound, Renewing
164 unicastConn UnicastTransport
165
166 // Valid in states Bound, Renewing, Rebinding
167 lease *dhcpv4.DHCPv4
168 leaseDeadline time.Time
169 leaseBoundDeadline time.Time
170 leaseRenewDeadline time.Time
171}
172
173// newDefaultBackoff returns an infinitely-retrying randomized exponential backoff with a
174// DHCP-appropriate InitialInterval
175func newDefaultBackoff() *backoff.ExponentialBackOff {
176 b := backoff.NewExponentialBackOff()
177 b.MaxElapsedTime = 0 // No Timeout
178 // Lots of servers wait 1s for existing users of an IP. Wait at least for that and keep some
179 // slack for randomization, communication and processing overhead.
180 b.InitialInterval = 1400 * time.Millisecond
181 b.MaxInterval = 30 * time.Second
182 b.RandomizationFactor = 0.2
183 return b
184}
185
186// NewClient instantiates (but doesn't start) a new DHCPv4 client.
187// To have a working client it's required to set LeaseCallback to something that is capable of configuring the IP
188// address on the given interface. Unless managed through external means like a routing protocol, setting the default
189// route is also required. A simple example with the callback package thus looks like this:
190// c := dhcp4c.NewClient(yourInterface)
191// c.LeaseCallback = callback.Compose(callback.ManageIP(yourInterface), callback.ManageDefaultRoute(yourInterface))
192// c.Run(ctx)
193func NewClient(iface *net.Interface) (*Client, error) {
194 broadcastConn := transport.NewBroadcastTransport(iface)
195
196 // broadcastConn needs to be open in stateDiscovering
197 if err := broadcastConn.Open(); err != nil {
198 return nil, fmt.Errorf("failed to create DHCP broadcast transport: %w", err)
199 }
200
201 discoverBackoff := newDefaultBackoff()
202
203 acceptOfferBackoff := newDefaultBackoff()
204 // Abort after 30s and go back to discovering
205 acceptOfferBackoff.MaxElapsedTime = 30 * time.Second
206
207 renewBackoff := newDefaultBackoff()
208 // Increase maximum interval to reduce chatter when the server is down
209 renewBackoff.MaxInterval = 5 * time.Minute
210
211 rebindBackoff := newDefaultBackoff()
212 // Increase maximum interval to reduce chatter when the server is down
213 renewBackoff.MaxInterval = 5 * time.Minute
214
215 return &Client{
216 state: stateDiscovering,
217 broadcastConn: broadcastConn,
218 unicastConn: transport.NewUnicastTransport(iface),
219 iface: iface,
220 RequestedOptions: dhcpv4.OptionCodeList{},
221 lastBoundTransition: time.Now(),
222 now: time.Now,
223 DiscoverBackoff: discoverBackoff,
224 AcceptOfferBackoff: acceptOfferBackoff,
225 RenewBackoff: renewBackoff,
226 RebindBackoff: rebindBackoff,
227 }, nil
228}
229
230// acceptableLease checks if the given lease is valid enough to even be processed. This is
231// intentionally not exposed to users because under certain cirumstances it can end up acquiring all
232// available IP addresses from a server.
233func (c *Client) acceptableLease(offer *dhcpv4.DHCPv4) bool {
234 // RFC2131 Section 4.3.1 Table 3
235 if offer.ServerIdentifier() == nil || offer.ServerIdentifier().To4() == nil {
236 return false
237 }
238 // RFC2131 Section 4.3.1 Table 3
239 // Minimum representable lease time is 1s (Section 1.1)
240 if offer.IPAddressLeaseTime(0) < 1*time.Second {
241 return false
242 }
243
244 // Ignore IPs that are in no way valid for an interface (multicast, loopback, ...)
245 if offer.YourIPAddr.To4() == nil || (!offer.YourIPAddr.IsGlobalUnicast() && !offer.YourIPAddr.IsLinkLocalUnicast()) {
246 return false
247 }
248
249 // Technically the options Requested IP address, Parameter request list, Client identifier
250 // and Maximum message size should be refused (MUST NOT), but in the interest of interopatibilty
251 // let's simply remove them if they are present.
252 delete(offer.Options, dhcpv4.OptionRequestedIPAddress.Code())
253 delete(offer.Options, dhcpv4.OptionParameterRequestList.Code())
254 delete(offer.Options, dhcpv4.OptionClientIdentifier.Code())
255 delete(offer.Options, dhcpv4.OptionMaximumDHCPMessageSize.Code())
256
257 // Clamp rebindinding times longer than the lease time. Otherwise the state machine might misbehave.
258 if offer.IPAddressRebindingTime(0) > offer.IPAddressLeaseTime(0) {
259 offer.UpdateOption(dhcpv4.OptGeneric(dhcpv4.OptionRebindingTimeValue, dhcpv4.Duration(offer.IPAddressLeaseTime(0)).ToBytes()))
260 }
261 // Clamp renewal times longer than the rebinding time. Otherwise the state machine might misbehave.
262 if offer.IPAddressRenewalTime(0) > offer.IPAddressRebindingTime(0) {
263 offer.UpdateOption(dhcpv4.OptGeneric(dhcpv4.OptionRenewTimeValue, dhcpv4.Duration(offer.IPAddressRebindingTime(0)).ToBytes()))
264 }
265
266 // Normalize two options that can be represented either inline or as options.
267 if len(offer.ServerHostName) > 0 {
268 offer.Options[uint8(dhcpv4.OptionTFTPServerName)] = []byte(offer.ServerHostName)
269 }
270 if len(offer.BootFileName) > 0 {
271 offer.Options[uint8(dhcpv4.OptionBootfileName)] = []byte(offer.BootFileName)
272 }
273
274 // Normalize siaddr to option 150 (see RFC5859)
275 if len(offer.GetOneOption(dhcpv4.OptionTFTPServerAddress)) == 0 {
276 if offer.ServerIPAddr.To4() != nil && (offer.ServerIPAddr.IsGlobalUnicast() || offer.ServerIPAddr.IsLinkLocalUnicast()) {
277 offer.Options[uint8(dhcpv4.OptionTFTPServerAddress)] = offer.ServerIPAddr.To4()
278 }
279 }
280
281 return true
282}
283
284func earliestDeadline(dl1, dl2 time.Time) time.Time {
285 if dl1.Before(dl2) {
286 return dl1
287 } else {
288 return dl2
289 }
290}
291
292// newXID generates a new transaction ID
293func (c *Client) newXID() (dhcpv4.TransactionID, error) {
294 var xid dhcpv4.TransactionID
295 if _, err := io.ReadFull(rand.Reader, xid[:]); err != nil {
296 return xid, fmt.Errorf("cannot read randomness for transaction ID: %w", err)
297 }
298 return xid, nil
299}
300
301// As most servers out there cannot do reassembly, let's just hope for the best and
302// provide the local interface MTU. If the packet is too big it won't work anyways.
303// Also clamp to the biggest representable MTU in DHCPv4 (2 bytes unsigned int).
304func (c *Client) maxMsgSize() uint16 {
305 if c.iface.MTU < math.MaxUint16 {
306 return uint16(c.iface.MTU)
307 } else {
308 return math.MaxUint16
309 }
310}
311
312// newMsg creates a new DHCP message of a given type and adds common options.
313func (c *Client) newMsg(t dhcpv4.MessageType) (*dhcpv4.DHCPv4, error) {
314 xid, err := c.newXID()
315 if err != nil {
316 return nil, err
317 }
318 opts := make(dhcpv4.Options)
319 opts.Update(dhcpv4.OptMessageType(t))
320 if len(c.ClientIdentifier) > 0 {
321 opts.Update(dhcpv4.OptClientIdentifier(c.ClientIdentifier))
322 }
323 if t == dhcpv4.MessageTypeDiscover || t == dhcpv4.MessageTypeRequest || t == dhcpv4.MessageTypeInform {
324 opts.Update(dhcpv4.OptParameterRequestList(append(c.RequestedOptions, internalOptions...)...))
325 opts.Update(dhcpv4.OptMaxMessageSize(c.maxMsgSize()))
326 if c.VendorClassIdentifier != "" {
327 opts.Update(dhcpv4.OptClassIdentifier(c.VendorClassIdentifier))
328 }
329 for opt, val := range c.ExtraRequestOptions {
330 opts[opt] = val
331 }
332 }
333 return &dhcpv4.DHCPv4{
334 OpCode: dhcpv4.OpcodeBootRequest,
335 HWType: iana.HWTypeEthernet,
336 ClientHWAddr: c.iface.HardwareAddr,
337 HopCount: 0,
338 TransactionID: xid,
339 NumSeconds: 0,
340 Flags: 0,
341 ClientIPAddr: net.IPv4zero,
342 YourIPAddr: net.IPv4zero,
343 ServerIPAddr: net.IPv4zero,
344 GatewayIPAddr: net.IPv4zero,
345 Options: opts,
346 }, nil
347}
348
349// transactionStateSpec describes a state which is driven by a DHCP message transaction (sending a
350// specific message and then transitioning into a different state depending on the received messages)
351type transactionStateSpec struct {
352 // ctx is a context for canceling the process
353 ctx context.Context
354
355 // transport is used to send and receive messages in this state
356 transport Transport
357
358 // stateDeadline is a fixed external deadline for how long the FSM can remain in this state.
359 // If it's exceeded the stateDeadlineExceeded callback is called and responsible for
360 // transitioning out of this state. It can be left empty to signal that there's no external
361 // deadline for the state.
362 stateDeadline time.Time
363
364 // backoff controls how long to wait for answers until handing control back to the FSM.
365 // Since the FSM hasn't advanced until then this means we just get called again and retransmit.
366 backoff backoff.BackOff
367
368 // requestType is the type of DHCP request sent out in this state. This is used to populate
369 // the default options for the message.
370 requestType dhcpv4.MessageType
371
372 // setExtraOptions can modify the request and set extra options before transmitting. Returning
373 // an error here aborts the FSM an can be used to terminate when no valid request can be
374 // constructed.
375 setExtraOptions func(msg *dhcpv4.DHCPv4) error
376
377 // handleMessage gets called for every parseable (not necessarily valid) DHCP message received
378 // by the transport. It should return an error for every message that doesn't advance the
379 // state machine and no error for every one that does. It is responsible for advancing the FSM
380 // if the required information is present.
381 handleMessage func(msg *dhcpv4.DHCPv4, sentTime time.Time) error
382
383 // stateDeadlineExceeded gets called if either the backoff returns backoff.Stop or the
384 // stateDeadline runs out. It is responsible for advancing the FSM into the next state.
385 stateDeadlineExceeded func() error
386}
387
388func (c *Client) runTransactionState(s transactionStateSpec) error {
389 sentTime := c.now()
390 msg, err := c.newMsg(s.requestType)
391 if err != nil {
392 return fmt.Errorf("failed to get new DHCP message: %w", err)
393 }
394 if err := s.setExtraOptions(msg); err != nil {
395 return fmt.Errorf("failed to create DHCP message: %w", err)
396 }
397
398 wait := s.backoff.NextBackOff()
399 if wait == backoff.Stop {
400 return s.stateDeadlineExceeded()
401 }
402
403 receiveDeadline := sentTime.Add(wait)
404 if !s.stateDeadline.IsZero() {
405 receiveDeadline = earliestDeadline(s.stateDeadline, receiveDeadline)
406 }
407
408 // Jump out if deadline expires in less than 10ms. Minimum lease time is 1s and if we have less
409 // than 10ms to wait for an answer before switching state it makes no sense to send out another
410 // request. This nearly eliminates the problem of sending two different requests back-to-back.
411 if receiveDeadline.Add(-10 * time.Millisecond).Before(sentTime) {
412 return s.stateDeadlineExceeded()
413 }
414
415 if err := s.transport.Send(msg); err != nil {
416 return fmt.Errorf("failed to send message: %w", err)
417 }
418
419 if err := s.transport.SetReceiveDeadline(receiveDeadline); err != nil {
420 return fmt.Errorf("failed to set deadline: %w", err)
421 }
422
423 for {
424 offer, err := s.transport.Receive()
425 select {
426 case <-s.ctx.Done():
427 c.cleanup()
428 return s.ctx.Err()
429 default:
430 }
431 if errors.Is(err, transport.DeadlineExceededErr) {
432 return nil
433 }
434 var e transport.InvalidMessageError
435 if errors.As(err, &e) {
436 // Packet couldn't be read. Maybe log at some point in the future.
437 continue
438 }
439 if err != nil {
440 return fmt.Errorf("failed to receive packet: %w", err)
441 }
442 if offer.TransactionID != msg.TransactionID { // Not our transaction
443 continue
444 }
445 err = s.handleMessage(offer, sentTime)
446 if err == nil {
447 return nil
448 } else if !errors.Is(err, InvalidMsgErr) {
449 return err
450 }
451 }
452}
453
454var InvalidMsgErr = errors.New("invalid message")
455
456func (c *Client) runState(ctx context.Context) error {
457 switch c.state {
458 case stateDiscovering:
459 return c.runTransactionState(transactionStateSpec{
460 ctx: ctx,
461 transport: c.broadcastConn,
462 backoff: c.DiscoverBackoff,
463 requestType: dhcpv4.MessageTypeDiscover,
464 setExtraOptions: func(msg *dhcpv4.DHCPv4) error {
465 msg.UpdateOption(dhcpv4.OptGeneric(dhcpv4.OptionRapidCommit, []byte{}))
466 return nil
467 },
468 handleMessage: func(offer *dhcpv4.DHCPv4, sentTime time.Time) error {
469 switch offer.MessageType() {
470 case dhcpv4.MessageTypeOffer:
471 if c.acceptableLease(offer) {
472 c.offer = offer
473 c.AcceptOfferBackoff.Reset()
474 c.state = stateRequesting
475 return nil
476 }
477 case dhcpv4.MessageTypeAck:
478 if c.acceptableLease(offer) {
479 return c.transitionToBound(offer, sentTime)
480 }
481 }
482 return InvalidMsgErr
483 },
484 })
485 case stateRequesting:
486 return c.runTransactionState(transactionStateSpec{
487 ctx: ctx,
488 transport: c.broadcastConn,
489 backoff: c.AcceptOfferBackoff,
490 requestType: dhcpv4.MessageTypeRequest,
491 setExtraOptions: func(msg *dhcpv4.DHCPv4) error {
492 msg.UpdateOption(dhcpv4.OptServerIdentifier(c.offer.ServerIdentifier()))
493 msg.TransactionID = c.offer.TransactionID
494 msg.UpdateOption(dhcpv4.OptRequestedIPAddress(c.offer.YourIPAddr))
495 return nil
496 },
497 handleMessage: func(msg *dhcpv4.DHCPv4, sentTime time.Time) error {
498 switch msg.MessageType() {
499 case dhcpv4.MessageTypeAck:
500 if c.acceptableLease(msg) {
501 return c.transitionToBound(msg, sentTime)
502 }
503 case dhcpv4.MessageTypeNak:
504 c.requestingToDiscovering()
505 return nil
506 }
507 return InvalidMsgErr
508 },
509 stateDeadlineExceeded: func() error {
510 c.requestingToDiscovering()
511 return nil
512 },
513 })
514 case stateBound:
515 select {
516 case <-time.After(c.leaseBoundDeadline.Sub(c.now())):
517 c.state = stateRenewing
518 c.RenewBackoff.Reset()
519 return nil
520 case <-ctx.Done():
521 c.cleanup()
522 return ctx.Err()
523 }
524 case stateRenewing:
525 return c.runTransactionState(transactionStateSpec{
526 ctx: ctx,
527 transport: c.unicastConn,
528 backoff: c.RenewBackoff,
529 requestType: dhcpv4.MessageTypeRequest,
530 stateDeadline: c.leaseRenewDeadline,
531 setExtraOptions: func(msg *dhcpv4.DHCPv4) error {
532 msg.ClientIPAddr = c.lease.YourIPAddr
533 return nil
534 },
535 handleMessage: func(ack *dhcpv4.DHCPv4, sentTime time.Time) error {
536 switch ack.MessageType() {
537 case dhcpv4.MessageTypeAck:
538 if c.acceptableLease(ack) {
539 return c.transitionToBound(ack, sentTime)
540 }
541 case dhcpv4.MessageTypeNak:
542 return c.leaseToDiscovering()
543 }
544 return InvalidMsgErr
545 },
546 stateDeadlineExceeded: func() error {
547 c.state = stateRebinding
548 if err := c.switchToBroadcast(); err != nil {
549 return fmt.Errorf("failed to switch to broadcast: %w", err)
550 }
551 c.RebindBackoff.Reset()
552 return nil
553 },
554 })
555 case stateRebinding:
556 return c.runTransactionState(transactionStateSpec{
557 ctx: ctx,
558 transport: c.broadcastConn,
559 backoff: c.RebindBackoff,
560 stateDeadline: c.leaseDeadline,
561 requestType: dhcpv4.MessageTypeRequest,
562 setExtraOptions: func(msg *dhcpv4.DHCPv4) error {
563 msg.ClientIPAddr = c.lease.YourIPAddr
564 return nil
565 },
566 handleMessage: func(ack *dhcpv4.DHCPv4, sentTime time.Time) error {
567 switch ack.MessageType() {
568 case dhcpv4.MessageTypeAck:
569 if c.acceptableLease(ack) {
570 return c.transitionToBound(ack, sentTime)
571 }
572 case dhcpv4.MessageTypeNak:
573 return c.leaseToDiscovering()
574 }
575 return InvalidMsgErr
576 },
577 stateDeadlineExceeded: func() error {
578 return c.leaseToDiscovering()
579 },
580 })
581 }
582 return errors.New("state machine in invalid state")
583}
584
585func (c *Client) Run(ctx context.Context) error {
586 if c.LeaseCallback == nil {
587 panic("LeaseCallback must be set before calling Run")
588 }
589 logger := supervisor.Logger(ctx)
590 for {
591 oldState := c.state
592 if err := c.runState(ctx); err != nil {
593 return err
594 }
595 if c.state != oldState {
596 logger.Infof("%s => %s", oldState, c.state)
597 }
598 }
599}
600
601func (c *Client) cleanup() {
602 c.unicastConn.Close()
603 if c.lease != nil {
604 c.LeaseCallback(leaseFromAck(c.lease, c.leaseDeadline), nil)
605 }
606 c.broadcastConn.Close()
607}
608
609func (c *Client) requestingToDiscovering() {
610 c.offer = nil
611 c.DiscoverBackoff.Reset()
612 c.state = stateDiscovering
613}
614
615func (c *Client) leaseToDiscovering() error {
616 if c.state == stateRenewing {
617 if err := c.switchToBroadcast(); err != nil {
618 return err
619 }
620 }
621 c.state = stateDiscovering
622 c.DiscoverBackoff.Reset()
623 if err := c.LeaseCallback(leaseFromAck(c.lease, c.leaseDeadline), nil); err != nil {
624 return fmt.Errorf("lease callback failed: %w", err)
625 }
626 c.lease = nil
627 return nil
628}
629
630func leaseFromAck(ack *dhcpv4.DHCPv4, expiresAt time.Time) *Lease {
631 if ack == nil {
632 return nil
633 }
634 return &Lease{Options: ack.Options, AssignedIP: ack.YourIPAddr, ExpiresAt: expiresAt}
635}
636
637func (c *Client) transitionToBound(ack *dhcpv4.DHCPv4, sentTime time.Time) error {
638 // Guaranteed to exist, leases without a lease time are filtered
639 leaseTime := ack.IPAddressLeaseTime(0)
640 origLeaseDeadline := c.leaseDeadline
641 c.leaseDeadline = sentTime.Add(leaseTime)
642 c.leaseBoundDeadline = sentTime.Add(ack.IPAddressRenewalTime(time.Duration(float64(leaseTime) * 0.5)))
643 c.leaseRenewDeadline = sentTime.Add(ack.IPAddressRebindingTime(time.Duration(float64(leaseTime) * 0.85)))
644
645 if err := c.LeaseCallback(leaseFromAck(c.lease, origLeaseDeadline), leaseFromAck(ack, c.leaseDeadline)); err != nil {
646 return fmt.Errorf("lease callback failed: %w", err)
647 }
648
649 if c.state != stateRenewing {
650 if err := c.switchToUnicast(ack.ServerIdentifier(), ack.YourIPAddr); err != nil {
651 return fmt.Errorf("failed to switch transports: %w", err)
652 }
653 }
654 c.state = stateBound
655 c.lease = ack
656 return nil
657}
658
659func (c *Client) switchToUnicast(serverIP, bindIP net.IP) error {
660 if err := c.broadcastConn.Close(); err != nil {
661 return fmt.Errorf("failed to close broadcast transport: %w", err)
662 }
663 if err := c.unicastConn.Open(serverIP, bindIP); err != nil {
664 return fmt.Errorf("failed to open unicast transport: %w", err)
665 }
666 return nil
667}
668
669func (c *Client) switchToBroadcast() error {
670 if err := c.unicastConn.Close(); err != nil {
671 return fmt.Errorf("failed to close unicast transport: %w", err)
672 }
673 if err := c.broadcastConn.Open(); err != nil {
674 return fmt.Errorf("failed to open broadcast transport: %w", err)
675 }
676 return nil
677}