1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
|
/*
An example, bind to eth0, using option82/remote-id for client identification:
package main
import (
"context"
"log"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv4/nclient4"
)
func main() {
ifname := "eth0"
remote_id := "client-1"
var idoptlist dhcpv4.OptionCodeList
//specify option82 is part of client identification used by DHCPv4 server
idoptlist.Add(dhcpv4.OptionRelayAgentInformation)
clnt_options := []nclient4.ClientOpt{nclient4.WithClientIdOptions(idoptlist), nclient4.WithDebugLogger()}
clnt, err := nclient4.New(ifname, clnt_options...)
if err != nil {
log.Fatalf("failed to create dhcpv4 client,%v", err)
}
//adding option82/remote-id option to discovery and request
remote_id_sub_opt := dhcpv4.OptGeneric(dhcpv4.AgentRemoteIDSubOption, []byte(remote_id))
option82 := dhcpv4.OptRelayAgentInfo(remote_id_sub_opt)
_, _, err = clnt.RequestSavingLease(context.Background(), dhcpv4.WithOption(option82))
if err != nil {
log.Fatal(err)
}
//print the lease
log.Printf("Got lease:\n%v", clnt.GetLease())
//release the lease
log.Print("Releasing lease...")
err = clnt.Release()
if err != nil {
log.Fatal(err)
}
log.Print("done")
}
*/
package nclient4
import (
"context"
"fmt"
"net"
"time"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/vishvananda/netlink"
)
const (
//default lease time if server doesn't return lease time option or return zero
defaultLeaseTime = time.Hour
)
//DHCPv4ClientLease contains a DHCPv4 lease after DORA,
//could be used for creating a new Client with NewWithLease()
type DHCPv4ClientLease struct {
IfName string
MACAddr net.HardwareAddr
ServerAddr net.UDPAddr
AssignedIP net.IP
AssignedIPMask net.IPMask
CreationTime time.Time
LeaseDuration time.Duration
RenewInterval time.Duration
RebindInterval time.Duration
IdOptions dhcpv4.Options //DHCPv4 options to identify the client like client-id, option82/remote-id
AckOptions dhcpv4.Options //DHCPv4 options in ACK, could be used for applying lease
}
//return a string representation
func (lease DHCPv4ClientLease) String() string {
const FMTSTR = "%-35s\t%-35s\n"
const TIME_FMT = "01/02/2006 15:04:05.000"
rstr := fmt.Sprintf(FMTSTR, fmt.Sprintf("Interface:%v", lease.IfName), fmt.Sprintf("MAC:%v", lease.MACAddr))
rstr += fmt.Sprintf(FMTSTR, fmt.Sprintf("Svr:%v", lease.ServerAddr.IP), fmt.Sprintf("Created:%v", lease.CreationTime.Format(TIME_FMT)))
prefixlen, _ := lease.AssignedIPMask.Size()
rstr += fmt.Sprintf(FMTSTR, fmt.Sprintf("IP:%v/%v", lease.AssignedIP, prefixlen), fmt.Sprintf("Lease time:%v", lease.LeaseDuration))
rstr += fmt.Sprintf(FMTSTR, fmt.Sprintf("Renew interval:%v", lease.RenewInterval), fmt.Sprintf("Rebind interval:%v", lease.RebindInterval))
rstr += fmt.Sprintf("Id options:\n%v", lease.IdOptions)
rstr += fmt.Sprintf("ACK options:\n%v", lease.AckOptions)
return rstr
}
// WithClientIdOptions configures a list of DHCPv4 option code that DHCP server
// uses to identify client, beside the MAC address.
func WithClientIdOptions(cidl dhcpv4.OptionCodeList) ClientOpt {
return func(c *Client) (err error) {
c.clientIdOptions = cidl
return
}
}
// WithApplyLeaseHandler specifies a handler function which is called when
// Client.ApplyLease() is called; without this, a default handler function is called.
// the default handler will add/remove the assigned address to/from the binding interface;
// bool parameter is true when lease is applied, false when lease is released
func WithApplyLeaseHandler(h func(DHCPv4ClientLease, bool) error) ClientOpt {
return func(c *Client) (err error) {
c.leaseApplyHandler = h
return
}
}
//default lease apply handler
//add/remove address to/from binding interface
func defaultLeaseApplyHandler(l DHCPv4ClientLease, enable bool) error {
link, err := netlink.LinkByName(l.IfName)
if err != nil {
return err
}
plen, _ := l.AssignedIPMask.Size()
prefixstr := fmt.Sprintf("%v/%v", l.AssignedIP, plen)
naddr, err := netlink.ParseAddr(prefixstr)
if err != nil {
return err
}
if enable {
err = netlink.AddrReplace(link, naddr)
} else {
err = netlink.AddrDel(link, naddr)
}
return err
}
//ApplyLease apply/unapply the lease, call the c.leaseApplyHandler
func (c *Client) ApplyLease(enable bool) error {
if c.lease == nil {
return fmt.Errorf("no lease to apply")
}
return c.leaseApplyHandler(c.GetLease(), enable)
}
//GetLease return the lease
func (c *Client) GetLease() (clease DHCPv4ClientLease) {
clease = *c.lease
clease.MACAddr = c.ifaceHWAddr
clease.IfName = c.ifName
clease.ServerAddr = *c.serverAddr
return
}
// RequestRequestSavingLease completes DORA handshake and store&apply the lease
//
// Note that modifiers will be applied *both* to Discover and Request packets.
func (c *Client) RequestSavingLease(ctx context.Context, modifiers ...dhcpv4.Modifier) (offer, ack *dhcpv4.DHCPv4, err error) {
offer, err = c.DiscoverOffer(ctx, modifiers...)
if err != nil {
err = fmt.Errorf("unable to receive an offer: %w", err)
return
}
// TODO(chrisko): should this be unicast to the server?
request, err := dhcpv4.NewRequestFromOffer(offer, dhcpv4.PrependModifiers(modifiers,
dhcpv4.WithOption(dhcpv4.OptMaxMessageSize(MaxMessageSize)))...)
if err != nil {
err = fmt.Errorf("unable to create a request: %w", err)
return
}
ack, err = c.SendAndRead(ctx, c.serverAddr, request, nil)
if err != nil {
err = fmt.Errorf("got an error while processing the request: %w", err)
return
}
//save lease
c.lease = &DHCPv4ClientLease{}
c.lease.AssignedIP = ack.YourIPAddr
c.lease.AssignedIPMask = ack.SubnetMask()
c.lease.CreationTime = time.Now()
c.lease.LeaseDuration = ack.IPAddressLeaseTime(0)
if c.lease.LeaseDuration == 0 {
c.lease.LeaseDuration = defaultLeaseTime
c.logger.Printf("warning: server doesn't include Lease Time option or it is zero seconds, setting lease time to default %v", c.lease.LeaseDuration)
}
c.lease.RenewInterval = ack.IPAddressRenewalTime(0)
if c.lease.RenewInterval == 0 {
//setting default to half of lease time based on RFC2131,section 4.4.5
c.lease.RenewInterval = time.Duration(float64(c.lease.LeaseDuration) / 2)
c.logger.Printf("warning: server doesn't include Renew Time option or it is zero seconds, setting lease time to default %v", c.lease.RenewInterval)
}
c.lease.RebindInterval = ack.IPAddressRebindingTime(0)
if c.lease.RebindInterval == 0 {
//setting default to 0.875 of lease time based on RFC2131,section 4.4.5
c.lease.RebindInterval = time.Duration(float64(c.lease.LeaseDuration) * 0.875)
c.logger.Printf("warning: server doesn't include Renew Time option or it is zero seconds, setting lease time to default %v", c.lease.RebindInterval)
}
c.lease.IdOptions = dhcpv4.Options{}
for _, optioncode := range c.clientIdOptions {
v := request.Options.Get(optioncode)
c.lease.IdOptions.Update(dhcpv4.OptGeneric(optioncode, v))
}
c.lease.AckOptions = ack.Options
//update server address
c.serverAddr = &(net.UDPAddr{IP: ack.ServerIdentifier(), Port: 67})
err = c.ApplyLease(true)
return
}
//Release send DHCPv4 release messsage to server.
//release is sent as unicast per RFC2131, section 4.4.4.
//The lease need to be applied with c.ApplyLease(true) first before calling Release.
func (c *Client) Release() error {
if c.lease == nil {
return fmt.Errorf("There is no lease to release")
}
req, err := dhcpv4.New()
if err != nil {
return err
}
//This is to make sure use same client identification options used during
//DORA, so that DHCP server could identify the required lease
req.Options = c.lease.IdOptions
req.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeRelease))
req.ClientHWAddr = c.ifaceHWAddr
req.ClientIPAddr = c.lease.AssignedIP
req.UpdateOption(dhcpv4.OptServerIdentifier(c.serverAddr.IP))
req.SetUnicast()
luaddr, err := net.ResolveUDPAddr("udp4", fmt.Sprintf("%v:%v", c.lease.AssignedIP, 68))
if err != nil {
return err
}
uniconn, err := net.DialUDP("udp4", luaddr, c.serverAddr)
if err != nil {
return err
}
_, err = uniconn.Write(req.ToBytes())
if err != nil {
return err
}
c.logger.PrintMessage("sent message:", req)
return c.ApplyLease(false)
}
//NewWithLease return a Client with populated lease.
//this function could be used to release a saved lease.
func NewWithLease(clease DHCPv4ClientLease, opts ...ClientOpt) (*Client, error) {
clntoptlist := []ClientOpt{
WithServerAddr(&clease.ServerAddr),
WithHWAddr(clease.MACAddr),
}
clntoptlist = append(clntoptlist, opts...)
clnt, err := New(clease.IfName, clntoptlist...)
if err != nil {
return nil, err
}
clnt.ifName = clease.IfName
clnt.lease = &clease
for optioncode := range clease.IdOptions {
clnt.clientIdOptions.Add(dhcpv4.GenericOptionCode(optioncode))
}
return clnt, nil
}
|