blob: 4c5fb11435475444b9108ca2a51e6eb751b16e42 [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
17package dhcp4c
18
19import (
20 "context"
21 "fmt"
22 "net"
23 "testing"
24 "time"
25
26 "github.com/cenkalti/backoff/v4"
27 "github.com/insomniacslk/dhcp/dhcpv4"
28 "github.com/stretchr/testify/assert"
29
30 "git.monogon.dev/source/nexantic.git/core/pkg/dhcp4c/transport"
31)
32
33type fakeTime struct {
34 time time.Time
35}
36
37func newFakeTime(t time.Time) *fakeTime {
38 return &fakeTime{
39 time: t,
40 }
41}
42
43func (ft *fakeTime) Now() time.Time {
44 return ft.time
45}
46
47func (ft *fakeTime) Advance(d time.Duration) {
48 ft.time = ft.time.Add(d)
49}
50
51type mockTransport struct {
52 sentPacket *dhcpv4.DHCPv4
53 sendError error
54 setDeadline time.Time
55 receivePackets []*dhcpv4.DHCPv4
56 receiveError error
57 receiveIdx int
58 closed bool
59}
60
61func (mt *mockTransport) sendPackets(pkts ...*dhcpv4.DHCPv4) {
62 mt.receiveIdx = 0
63 mt.receivePackets = pkts
64}
65
66func (mt *mockTransport) Open() error {
67 mt.closed = false
68 return nil
69}
70
71func (mt *mockTransport) Send(payload *dhcpv4.DHCPv4) error {
72 mt.sentPacket = payload
73 return mt.sendError
74}
75
76func (mt *mockTransport) Receive() (*dhcpv4.DHCPv4, error) {
77 if mt.receiveError != nil {
78 return nil, mt.receiveError
79 }
80 if len(mt.receivePackets) > mt.receiveIdx {
81 packet := mt.receivePackets[mt.receiveIdx]
82 packet, err := dhcpv4.FromBytes(packet.ToBytes()) // Clone packet
83 if err != nil {
84 panic("ToBytes => FromBytes failed")
85 }
86 packet.TransactionID = mt.sentPacket.TransactionID
87 mt.receiveIdx++
88 return packet, nil
89 }
90 return nil, transport.DeadlineExceededErr
91}
92
93func (mt *mockTransport) SetReceiveDeadline(t time.Time) error {
94 mt.setDeadline = t
95 return nil
96}
97
98func (mt *mockTransport) Close() error {
99 mt.closed = true
100 return nil
101}
102
103type unicastMockTransport struct {
104 mockTransport
105 serverIP net.IP
106 bindIP net.IP
107}
108
109func (umt *unicastMockTransport) Open(serverIP, bindIP net.IP) error {
110 if umt.serverIP != nil {
111 panic("double-open of unicast transport")
112 }
113 umt.serverIP = serverIP
114 umt.bindIP = bindIP
115 return nil
116}
117
118func (umt *unicastMockTransport) Close() error {
119 umt.serverIP = nil
120 umt.bindIP = nil
121 return umt.mockTransport.Close()
122}
123
124type mockBackoff struct {
125 indefinite bool
126 values []time.Duration
127 idx int
128}
129
130func newMockBackoff(vals []time.Duration, indefinite bool) *mockBackoff {
131 return &mockBackoff{values: vals, indefinite: indefinite}
132}
133
134func (mb *mockBackoff) NextBackOff() time.Duration {
135 if mb.idx < len(mb.values) || mb.indefinite {
136 val := mb.values[mb.idx%len(mb.values)]
137 mb.idx++
138 return val
139 }
140 return backoff.Stop
141}
142
143func (mb *mockBackoff) Reset() {
144 mb.idx = 0
145}
146
147func TestClient_runTransactionState(t *testing.T) {
148 ft := newFakeTime(time.Date(2020, 10, 28, 15, 02, 32, 352, time.UTC))
149 c := Client{
150 now: ft.Now,
151 iface: &net.Interface{MTU: 9324, HardwareAddr: net.HardwareAddr{0x12, 0x23, 0x34, 0x45, 0x56, 0x67}},
152 }
153 mt := &mockTransport{}
154 err := c.runTransactionState(transactionStateSpec{
155 ctx: context.Background(),
156 transport: mt,
157 backoff: newMockBackoff([]time.Duration{1 * time.Second}, true),
158 requestType: dhcpv4.MessageTypeDiscover,
159 setExtraOptions: func(msg *dhcpv4.DHCPv4) error {
160 msg.UpdateOption(dhcpv4.OptDomainName("just.testing.invalid"))
161 return nil
162 },
163 handleMessage: func(msg *dhcpv4.DHCPv4, sentTime time.Time) error {
164 return nil
165 },
166 stateDeadlineExceeded: func() error {
167 panic("shouldn't be called")
168 },
169 })
170 assert.NoError(t, err)
171 assert.Equal(t, "just.testing.invalid", mt.sentPacket.DomainName())
172 assert.Equal(t, dhcpv4.MessageTypeDiscover, mt.sentPacket.MessageType())
173}
174
175// TestAcceptableLease tests if a minimal valid lease is accepted by acceptableLease
176func TestAcceptableLease(t *testing.T) {
177 c := Client{}
178 offer := &dhcpv4.DHCPv4{
179 OpCode: dhcpv4.OpcodeBootReply,
180 }
181 offer.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer))
182 offer.UpdateOption(dhcpv4.OptServerIdentifier(net.IP{192, 0, 2, 1}))
183 offer.UpdateOption(dhcpv4.OptIPAddressLeaseTime(10 * time.Second))
184 offer.YourIPAddr = net.IP{192, 0, 2, 2}
185 assert.True(t, c.acceptableLease(offer), "Valid lease is not acceptable")
186}
187
188type dhcpClientPuppet struct {
189 ft *fakeTime
190 bmt *mockTransport
191 umt *unicastMockTransport
192 c *Client
193}
194
195func newPuppetClient(initState state) *dhcpClientPuppet {
196 ft := newFakeTime(time.Date(2020, 10, 28, 15, 02, 32, 352, time.UTC))
197 bmt := &mockTransport{}
198 umt := &unicastMockTransport{}
199 c := &Client{
200 state: initState,
201 now: ft.Now,
202 iface: &net.Interface{MTU: 9324, HardwareAddr: net.HardwareAddr{0x12, 0x23, 0x34, 0x45, 0x56, 0x67}},
203 broadcastConn: bmt,
204 unicastConn: umt,
205 DiscoverBackoff: newMockBackoff([]time.Duration{1 * time.Second}, true),
206 AcceptOfferBackoff: newMockBackoff([]time.Duration{1 * time.Second, 2 * time.Second}, false),
207 RenewBackoff: newMockBackoff([]time.Duration{1 * time.Second}, true),
208 RebindBackoff: newMockBackoff([]time.Duration{1 * time.Second}, true),
209 }
210 return &dhcpClientPuppet{
211 ft: ft,
212 bmt: bmt,
213 umt: umt,
214 c: c,
215 }
216}
217
218func newResponse(m dhcpv4.MessageType) *dhcpv4.DHCPv4 {
219 o := &dhcpv4.DHCPv4{
220 OpCode: dhcpv4.OpcodeBootReply,
221 }
222 o.UpdateOption(dhcpv4.OptMessageType(m))
223 return o
224}
225
226// TestDiscoverOffer tests if the DHCP state machine in discovering state properly selects the first valid lease
227// and transitions to requesting state
228func TestDiscoverRequesting(t *testing.T) {
229 p := newPuppetClient(stateDiscovering)
230
231 // A minimal valid lease
232 offer := newResponse(dhcpv4.MessageTypeOffer)
233 testIP := net.IP{192, 0, 2, 2}
234 offer.UpdateOption(dhcpv4.OptServerIdentifier(net.IP{192, 0, 2, 1}))
235 offer.UpdateOption(dhcpv4.OptIPAddressLeaseTime(10 * time.Second))
236 offer.YourIPAddr = testIP
237
238 // Intentionally bad offer with no lease time.
239 terribleOffer := newResponse(dhcpv4.MessageTypeOffer)
240 terribleOffer.UpdateOption(dhcpv4.OptServerIdentifier(net.IP{192, 0, 2, 2}))
241 terribleOffer.YourIPAddr = net.IPv4(192, 0, 2, 2)
242
243 // Send the bad offer first, then the valid offer
244 p.bmt.sendPackets(terribleOffer, offer)
245
246 if err := p.c.runState(context.Background()); err != nil {
247 t.Error(err)
248 }
249 assert.Equal(t, stateRequesting, p.c.state, "DHCP client didn't process offer")
250 assert.Equal(t, testIP, p.c.offer.YourIPAddr, "DHCP client requested invalid offer")
251}
252
253// TestOfferBound tests if the DHCP state machine in requesting state processes a valid DHCPACK and transitions to
254// bound state.
255func TestRequestingBound(t *testing.T) {
256 p := newPuppetClient(stateRequesting)
257
258 offer := newResponse(dhcpv4.MessageTypeAck)
259 testIP := net.IP{192, 0, 2, 2}
260 offer.UpdateOption(dhcpv4.OptServerIdentifier(net.IP{192, 0, 2, 1}))
261 offer.UpdateOption(dhcpv4.OptIPAddressLeaseTime(10 * time.Second))
262 offer.YourIPAddr = testIP
263
264 p.bmt.sendPackets(offer)
265 p.c.offer = offer
266 p.c.LeaseCallback = func(old, new *Lease) error {
267 assert.Nil(t, old, "old lease is not nil for new lease")
268 assert.Equal(t, testIP, new.AssignedIP, "new lease has wrong IP")
269 return nil
270 }
271
272 if err := p.c.runState(context.Background()); err != nil {
273 t.Error(err)
274 }
275 assert.Equal(t, stateBound, p.c.state, "DHCP client didn't process offer")
276 assert.Equal(t, testIP, p.c.lease.YourIPAddr, "DHCP client requested invalid offer")
277}
278
279// TestRequestingDiscover tests if the DHCP state machine in requesting state transitions back to discovering if it
280// takes too long to get a valid DHCPACK.
281func TestRequestingDiscover(t *testing.T) {
282 p := newPuppetClient(stateRequesting)
283
284 offer := newResponse(dhcpv4.MessageTypeOffer)
285 testIP := net.IP{192, 0, 2, 2}
286 offer.UpdateOption(dhcpv4.OptServerIdentifier(net.IP{192, 0, 2, 1}))
287 offer.UpdateOption(dhcpv4.OptIPAddressLeaseTime(10 * time.Second))
288 offer.YourIPAddr = testIP
289 p.c.offer = offer
290
291 for i := 0; i < 10; i++ {
292 p.bmt.sendPackets()
293 if err := p.c.runState(context.Background()); err != nil {
294 t.Error(err)
295 }
296 assert.Equal(t, dhcpv4.MessageTypeRequest, p.bmt.sentPacket.MessageType(), "Invalid message type for requesting")
297 if p.c.state == stateDiscovering {
298 break
299 }
300 p.ft.time = p.bmt.setDeadline
301
302 if i == 9 {
303 t.Fatal("Too many tries while requesting, backoff likely wrong")
304 }
305 }
306 assert.Equal(t, stateDiscovering, p.c.state, "DHCP client didn't switch back to offer after requesting expired")
307}
308
309// TestDiscoverRapidCommit tests if the DHCP state machine in discovering state transitions directly to bound if a
310// rapid commit response (DHCPACK) is received.
311func TestDiscoverRapidCommit(t *testing.T) {
312 testIP := net.IP{192, 0, 2, 2}
313 offer := newResponse(dhcpv4.MessageTypeAck)
314 offer.UpdateOption(dhcpv4.OptServerIdentifier(net.IP{192, 0, 2, 1}))
315 leaseTime := 10 * time.Second
316 offer.UpdateOption(dhcpv4.OptIPAddressLeaseTime(leaseTime))
317 offer.YourIPAddr = testIP
318
319 p := newPuppetClient(stateDiscovering)
320 p.c.LeaseCallback = func(old, new *Lease) error {
321 assert.Nil(t, old, "old is not nil")
322 assert.Equal(t, testIP, new.AssignedIP, "callback called with wrong IP")
323 assert.Equal(t, p.ft.Now().Add(leaseTime), new.ExpiresAt, "invalid ExpiresAt")
324 return nil
325 }
326 p.bmt.sendPackets(offer)
327 if err := p.c.runState(context.Background()); err != nil {
328 t.Error(err)
329 }
330 assert.Equal(t, stateBound, p.c.state, "DHCP client didn't process offer")
331 assert.Equal(t, testIP, p.c.lease.YourIPAddr, "DHCP client requested invalid offer")
332 assert.Equal(t, 5*time.Second, p.c.leaseBoundDeadline.Sub(p.ft.Now()), "Renewal time was incorrectly defaulted")
333}
334
335type TestOption uint8
336
337func (o TestOption) Code() uint8 {
338 return uint8(o) + 224 // Private options
339}
340func (o TestOption) String() string {
341 return fmt.Sprintf("Test Option %d", uint8(o))
342}
343
344// TestBoundRenewingBound tests if the DHCP state machine in bound correctly transitions to renewing after
345// leaseBoundDeadline expires, sends a DHCPREQUEST and after it gets a DHCPACK response calls LeaseCallback and
346// transitions back to bound with correct new deadlines.
347func TestBoundRenewingBound(t *testing.T) {
348 offer := newResponse(dhcpv4.MessageTypeAck)
349 testIP := net.IP{192, 0, 2, 2}
350 serverIP := net.IP{192, 0, 2, 1}
351 offer.UpdateOption(dhcpv4.OptServerIdentifier(serverIP))
352 leaseTime := 10 * time.Second
353 offer.UpdateOption(dhcpv4.OptIPAddressLeaseTime(leaseTime))
354 offer.YourIPAddr = testIP
355
356 p := newPuppetClient(stateBound)
357 p.umt.Open(serverIP, testIP)
358 p.c.lease, _ = dhcpv4.FromBytes(offer.ToBytes())
359 // Other deadlines are intentionally empty to make sure they aren't used
360 p.c.leaseRenewDeadline = p.ft.Now().Add(8500 * time.Millisecond)
361 p.c.leaseBoundDeadline = p.ft.Now().Add(5000 * time.Millisecond)
362
363 p.ft.Advance(5*time.Second - 5*time.Millisecond)
364 if err := p.c.runState(context.Background()); err != nil {
365 t.Error(err)
366 }
367 p.ft.Advance(5 * time.Millisecond) // We cannot intercept time.After so we just advance the clock by the time slept
368 assert.Equal(t, stateRenewing, p.c.state, "DHCP client not renewing")
369 offer.UpdateOption(dhcpv4.OptGeneric(TestOption(1), []byte{0x12}))
370 p.umt.sendPackets(offer)
371 p.c.LeaseCallback = func(old, new *Lease) error {
372 assert.Equal(t, testIP, old.AssignedIP, "callback called with wrong old IP")
373 assert.Equal(t, testIP, new.AssignedIP, "callback called with wrong IP")
374 assert.Equal(t, p.ft.Now().Add(leaseTime), new.ExpiresAt, "invalid ExpiresAt")
375 assert.Empty(t, old.Options.Get(TestOption(1)), "old contains options from new")
376 assert.Equal(t, []byte{0x12}, new.Options.Get(TestOption(1)), "renewal didn't add new option")
377 return nil
378 }
379 if err := p.c.runState(context.Background()); err != nil {
380 t.Error(err)
381 }
382 assert.Equal(t, stateBound, p.c.state, "DHCP client didn't renew")
383 assert.Equal(t, p.ft.Now().Add(leaseTime), p.c.leaseDeadline, "lease deadline not updated")
384 assert.Equal(t, dhcpv4.MessageTypeRequest, p.umt.sentPacket.MessageType(), "Invalid message type for renewal")
385}
386
387// TestRenewingRebinding tests if the DHCP state machine in renewing state correctly sends DHCPREQUESTs and transitions
388// to the rebinding state when it hasn't received a valid response until the deadline expires.
389func TestRenewingRebinding(t *testing.T) {
390 offer := newResponse(dhcpv4.MessageTypeAck)
391 testIP := net.IP{192, 0, 2, 2}
392 serverIP := net.IP{192, 0, 2, 1}
393 offer.UpdateOption(dhcpv4.OptServerIdentifier(serverIP))
394 leaseTime := 10 * time.Second
395 offer.UpdateOption(dhcpv4.OptIPAddressLeaseTime(leaseTime))
396 offer.YourIPAddr = testIP
397
398 p := newPuppetClient(stateRenewing)
399 p.umt.Open(serverIP, testIP)
400 p.c.lease, _ = dhcpv4.FromBytes(offer.ToBytes())
401 // Other deadlines are intentionally empty to make sure they aren't used
402 p.c.leaseRenewDeadline = p.ft.Now().Add(8500 * time.Millisecond)
403 p.c.leaseDeadline = p.ft.Now().Add(10000 * time.Millisecond)
404
405 startTime := p.ft.Now()
406 p.ft.Advance(5 * time.Second)
407
408 p.c.LeaseCallback = func(old, new *Lease) error {
409 t.Fatal("Lease callback called without valid offer")
410 return nil
411 }
412
413 for i := 0; i < 10; i++ {
414 p.umt.sendPackets()
415 if err := p.c.runState(context.Background()); err != nil {
416 t.Error(err)
417 }
418 assert.Equal(t, dhcpv4.MessageTypeRequest, p.umt.sentPacket.MessageType(), "Invalid message type for renewal")
419 p.ft.time = p.umt.setDeadline
420
421 if p.c.state == stateRebinding {
422 break
423 }
424 if i == 9 {
425 t.Fatal("Too many tries while renewing, backoff likely wrong")
426 }
427 }
428 assert.Equal(t, startTime.Add(8500*time.Millisecond), p.umt.setDeadline, "wrong listen deadline when renewing")
429 assert.Equal(t, stateRebinding, p.c.state, "DHCP client not renewing")
430 assert.False(t, p.bmt.closed)
431 assert.True(t, p.umt.closed)
432}
433
434// TestRebindingBound tests if the DHCP state machine in rebinding state sends DHCPREQUESTs to the network and if
435// it receives a valid DHCPACK correctly transitions back to bound state.
436func TestRebindingBound(t *testing.T) {
437 offer := newResponse(dhcpv4.MessageTypeAck)
438 testIP := net.IP{192, 0, 2, 2}
439 serverIP := net.IP{192, 0, 2, 1}
440 offer.UpdateOption(dhcpv4.OptServerIdentifier(serverIP))
441 leaseTime := 10 * time.Second
442 offer.UpdateOption(dhcpv4.OptIPAddressLeaseTime(leaseTime))
443 offer.YourIPAddr = testIP
444
445 p := newPuppetClient(stateRebinding)
446 p.c.lease, _ = dhcpv4.FromBytes(offer.ToBytes())
447 // Other deadlines are intentionally empty to make sure they aren't used
448 p.c.leaseDeadline = p.ft.Now().Add(10000 * time.Millisecond)
449
450 p.ft.Advance(9 * time.Second)
451 if err := p.c.runState(context.Background()); err != nil {
452 t.Error(err)
453 }
454 assert.Equal(t, dhcpv4.MessageTypeRequest, p.bmt.sentPacket.MessageType(), "DHCP rebind sent invalid message type")
455 assert.Equal(t, stateRebinding, p.c.state, "DHCP client transferred out of rebinding state without trigger")
456 offer.UpdateOption(dhcpv4.OptGeneric(TestOption(1), []byte{0x12})) // Mark answer
457 p.bmt.sendPackets(offer)
458 p.bmt.sentPacket = nil
459 p.c.LeaseCallback = func(old, new *Lease) error {
460 assert.Equal(t, testIP, old.AssignedIP, "callback called with wrong old IP")
461 assert.Equal(t, testIP, new.AssignedIP, "callback called with wrong IP")
462 assert.Equal(t, p.ft.Now().Add(leaseTime), new.ExpiresAt, "invalid ExpiresAt")
463 assert.Empty(t, old.Options.Get(TestOption(1)), "old contains options from new")
464 assert.Equal(t, []byte{0x12}, new.Options.Get(TestOption(1)), "renewal didn't add new option")
465 return nil
466 }
467 if err := p.c.runState(context.Background()); err != nil {
468 t.Error(err)
469 }
470 assert.Equal(t, dhcpv4.MessageTypeRequest, p.bmt.sentPacket.MessageType())
471 assert.Equal(t, stateBound, p.c.state, "DHCP client didn't go back to bound")
472}
473
474// TestRebindingBound tests if the DHCP state machine in rebinding state transitions to discovering state if
475// leaseDeadline expires and calls LeaseCallback with an empty new lease.
476func TestRebindingDiscovering(t *testing.T) {
477 offer := newResponse(dhcpv4.MessageTypeAck)
478 testIP := net.IP{192, 0, 2, 2}
479 serverIP := net.IP{192, 0, 2, 1}
480 offer.UpdateOption(dhcpv4.OptServerIdentifier(serverIP))
481 leaseTime := 10 * time.Second
482 offer.UpdateOption(dhcpv4.OptIPAddressLeaseTime(leaseTime))
483 offer.YourIPAddr = testIP
484
485 p := newPuppetClient(stateRebinding)
486 p.c.lease, _ = dhcpv4.FromBytes(offer.ToBytes())
487 // Other deadlines are intentionally empty to make sure they aren't used
488 p.c.leaseDeadline = p.ft.Now().Add(10000 * time.Millisecond)
489
490 p.ft.Advance(9 * time.Second)
491 p.c.LeaseCallback = func(old, new *Lease) error {
492 assert.Equal(t, testIP, old.AssignedIP, "callback called with wrong old IP")
493 assert.Nil(t, new, "transition to discovering didn't clear new lease on callback")
494 return nil
495 }
496 for i := 0; i < 10; i++ {
497 p.bmt.sendPackets()
498 p.bmt.sentPacket = nil
499 if err := p.c.runState(context.Background()); err != nil {
500 t.Error(err)
501 }
502 if p.c.state == stateDiscovering {
503 assert.Nil(t, p.bmt.sentPacket)
504 break
505 }
506 assert.Equal(t, dhcpv4.MessageTypeRequest, p.bmt.sentPacket.MessageType(), "Invalid message type for rebind")
507 p.ft.time = p.bmt.setDeadline
508 if i == 9 {
509 t.Fatal("Too many tries while rebinding, backoff likely wrong")
510 }
511 }
512 assert.Nil(t, p.c.lease, "Lease not zeroed on transition to discovering")
513 assert.Equal(t, stateDiscovering, p.c.state, "DHCP client didn't transition to discovering after loosing lease")
514}