blob: 0eef2cc4c822d867d2c07c85468d33bf6d9e2da3 [file] [log] [blame]
Serge Bazanskicdb8c782020-02-17 12:34:02 +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
Lorenz Brun52f7f292020-06-24 16:42:02 +020017package dhcp
Serge Bazanskicdb8c782020-02-17 12:34:02 +010018
19import (
20 "context"
Serge Bazanski1a5a6672020-02-18 10:09:43 +010021 "fmt"
22 "net"
Serge Bazanskicdb8c782020-02-17 12:34:02 +010023
Serge Bazanskib1b742f2020-03-24 13:58:19 +010024 "git.monogon.dev/source/nexantic.git/core/internal/common/supervisor"
25
Serge Bazanskicdb8c782020-02-17 12:34:02 +010026 "github.com/insomniacslk/dhcp/dhcpv4"
27 "github.com/insomniacslk/dhcp/dhcpv4/nclient4"
28 "github.com/vishvananda/netlink"
29 "go.uber.org/zap"
30)
31
Lorenz Brun52f7f292020-06-24 16:42:02 +020032type Client struct {
Serge Bazanskib1b742f2020-03-24 13:58:19 +010033 reqC chan *dhcpStatusReq
Serge Bazanskicdb8c782020-02-17 12:34:02 +010034}
35
Lorenz Brun52f7f292020-06-24 16:42:02 +020036func New() *Client {
37 return &Client{
Serge Bazanskib1b742f2020-03-24 13:58:19 +010038 reqC: make(chan *dhcpStatusReq),
Serge Bazanskicdb8c782020-02-17 12:34:02 +010039 }
40}
41
42type dhcpStatusReq struct {
Lorenz Brun52f7f292020-06-24 16:42:02 +020043 resC chan *Status
Serge Bazanskicdb8c782020-02-17 12:34:02 +010044 wait bool
45}
46
Lorenz Brun52f7f292020-06-24 16:42:02 +020047func (r *dhcpStatusReq) fulfill(s *Status) {
Serge Bazanskicdb8c782020-02-17 12:34:02 +010048 go func() {
Serge Bazanski1a5a6672020-02-18 10:09:43 +010049 r.resC <- s
Serge Bazanskicdb8c782020-02-17 12:34:02 +010050 }()
51}
52
Lorenz Brun52f7f292020-06-24 16:42:02 +020053// Status is the IPv4 configuration provisioned via DHCP for a given interface. It does not necessarily represent
Serge Bazanski1a5a6672020-02-18 10:09:43 +010054// a configuration that is active or even valid.
Lorenz Brun52f7f292020-06-24 16:42:02 +020055type Status struct {
56 // Address is 'our' (the node's) IPv4 Address on the network.
57 Address net.IPNet
58 // Gateway is the default Gateway/router of this network, or 0.0.0.0 if none was given.
59 Gateway net.IP
60 // DNS is a list of IPv4 DNS servers to use.
61 DNS []net.IP
Serge Bazanski1a5a6672020-02-18 10:09:43 +010062}
63
Lorenz Brun52f7f292020-06-24 16:42:02 +020064func (s *Status) String() string {
65 return fmt.Sprintf("Address: %s, Gateway: %s, DNS: %v", s.Address.String(), s.Gateway.String(), s.DNS)
Serge Bazanski1a5a6672020-02-18 10:09:43 +010066}
67
Lorenz Brun52f7f292020-06-24 16:42:02 +020068func (c *Client) Run(iface netlink.Link) supervisor.Runnable {
Serge Bazanskib1b742f2020-03-24 13:58:19 +010069 return func(ctx context.Context) error {
70 logger := supervisor.Logger(ctx)
Serge Bazanskicdb8c782020-02-17 12:34:02 +010071
Lorenz Brun52f7f292020-06-24 16:42:02 +020072 // Channel updated with Address once one gets assigned/updated
73 newC := make(chan *Status)
Serge Bazanskib1b742f2020-03-24 13:58:19 +010074 // Status requests waiting for configuration
75 waiters := []*dhcpStatusReq{}
76
77 // Start lease acquisition
78 // TODO(q3k): actually maintain the lease instead of hoping we never get
79 // kicked off.
80
81 client, err := nclient4.New(iface.Attrs().Name)
Serge Bazanskicdb8c782020-02-17 12:34:02 +010082 if err != nil {
Serge Bazanskib1b742f2020-03-24 13:58:19 +010083 return fmt.Errorf("nclient4.New: %w", err)
Serge Bazanskicdb8c782020-02-17 12:34:02 +010084 }
Serge Bazanskicdb8c782020-02-17 12:34:02 +010085
Serge Bazanskib1b742f2020-03-24 13:58:19 +010086 err = supervisor.Run(ctx, "client", func(ctx context.Context) error {
87 supervisor.Signal(ctx, supervisor.SignalHealthy)
88 _, ack, err := client.Request(ctx)
89 if err != nil {
90 // TODO(q3k): implement retry logic with full state machine
91 logger.Error("DHCP lease request failed", zap.Error(err))
92 return err
Serge Bazanskicdb8c782020-02-17 12:34:02 +010093 }
Serge Bazanskib1b742f2020-03-24 13:58:19 +010094 newC <- parseAck(ack)
95 supervisor.Signal(ctx, supervisor.SignalDone)
96 return nil
97 })
98 if err != nil {
99 return err
100 }
Serge Bazanskicdb8c782020-02-17 12:34:02 +0100101
Serge Bazanskib1b742f2020-03-24 13:58:19 +0100102 supervisor.Signal(ctx, supervisor.SignalHealthy)
103
104 // State machine
105 // Two implicit states: WAITING -> ASSIGNED
106 // We start at WAITING, once we get a current config we move to ASSIGNED
107 // Once this becomes more complex (ie. has to handle link state changes)
108 // this should grow into a real state machine.
Lorenz Brun52f7f292020-06-24 16:42:02 +0200109 var current *Status
Serge Bazanskib1b742f2020-03-24 13:58:19 +0100110 logger.Info("DHCP client WAITING")
111 for {
112 select {
113 case <-ctx.Done():
114 // TODO(q3k): don't leave waiters hanging
115 return err
116
117 case cfg := <-newC:
118 current = cfg
119 logger.Info("DHCP client ASSIGNED", zap.String("ip", current.String()))
120 for _, w := range waiters {
121 w.fulfill(current)
122 }
123 waiters = []*dhcpStatusReq{}
124
125 case r := <-c.reqC:
126 if current != nil || !r.wait {
127 r.fulfill(current)
128 } else {
129 waiters = append(waiters, r)
130 }
Serge Bazanskicdb8c782020-02-17 12:34:02 +0100131 }
132 }
133 }
134}
135
Lorenz Brun52f7f292020-06-24 16:42:02 +0200136// parseAck turns an internal Status (from the dhcpv4 library) into a Status
137func parseAck(ack *dhcpv4.DHCPv4) *Status {
Serge Bazanski1a5a6672020-02-18 10:09:43 +0100138 address := net.IPNet{IP: ack.YourIPAddr, Mask: ack.SubnetMask()}
139
Lorenz Brun52f7f292020-06-24 16:42:02 +0200140 // DHCP routers are optional - if none are provided, assume no router and set Gateway to 0.0.0.0
141 // (this makes Gateway.IsUnspecified() == true)
Serge Bazanski1a5a6672020-02-18 10:09:43 +0100142 gateway, _, _ := net.ParseCIDR("0.0.0.0/0")
143 if routers := ack.Router(); len(routers) > 0 {
144 gateway = routers[0]
145 }
Lorenz Brun52f7f292020-06-24 16:42:02 +0200146 return &Status{
147 Address: address,
148 Gateway: gateway,
149 DNS: ack.DNS(),
Serge Bazanski1a5a6672020-02-18 10:09:43 +0100150 }
151}
152
Lorenz Brun52f7f292020-06-24 16:42:02 +0200153// Status returns the DHCP configuration requested from us by the local DHCP server.
154// If wait is true, this function will block until a DHCP configuration is available. Otherwise, a nil Status may be
Serge Bazanski1a5a6672020-02-18 10:09:43 +0100155// returned to indicate that no configuration has been received yet.
Lorenz Brun52f7f292020-06-24 16:42:02 +0200156func (c *Client) Status(ctx context.Context, wait bool) (*Status, error) {
157 resC := make(chan *Status)
Serge Bazanskicdb8c782020-02-17 12:34:02 +0100158 c.reqC <- &dhcpStatusReq{
159 resC: resC,
160 wait: wait,
161 }
162 select {
163 case <-ctx.Done():
164 return nil, ctx.Err()
165 case r := <-resC:
166 return r, nil
167 }
168}