github.com/jlowellwofford/u-root@v1.0.0/pkg/dhcp6client/client.go (about) 1 // Copyright 2017-2018 the u-root Authors. All rights reserved 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package dhcp6client implements a DHCPv6 client as per RFC 3315. 6 package dhcp6client 7 8 import ( 9 "context" 10 "fmt" 11 "net" 12 "strings" 13 "sync" 14 "time" 15 16 "github.com/mdlayher/dhcp6" 17 "github.com/mdlayher/dhcp6/dhcp6opts" 18 "github.com/mdlayher/eui64" 19 "github.com/vishvananda/netlink" 20 ) 21 22 // RFC 3315 Section 5.2. 23 const ( 24 // ClientPort is the port clients use to listen for DHCP messages. 25 ClientPort = 546 26 27 // ServerPort is the port servers and relay agents use to listen for 28 // DHCP messages. 29 ServerPort = 547 30 ) 31 32 var ( 33 // AllServers is all DHCP servers and relay agents on the local network 34 // segment (RFC 3315, Section 5.1.). 35 AllServers = net.ParseIP("ff02::1:2") 36 37 // DefaultServers is the default AllServers IP combined with the 38 // ServerPort. 39 DefaultServers = &net.UDPAddr{ 40 IP: AllServers, 41 Port: ServerPort, 42 } 43 ) 44 45 // Client is a simple DHCPv6 client implementing RFC 3315. 46 // 47 // 48 // Shortest Example: 49 // 50 // c, err := dhcp6client.New(iface) 51 // ... 52 // iana, packet, err := c.RapidSolicit() 53 // ... 54 // // iana now contains the IP assigned in the IAAddr option. 55 // 56 // 57 // Example selecting which advertising server to request from: 58 // 59 // c, err := dhcp6client.New(iface) 60 // ... 61 // ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second) 62 // defer cancel() 63 // 64 // ads, err := c.Solicit(ctx) 65 // ... 66 // // Selecting the advertisement of server 3. 67 // request, err := dhcp6client.RequestIANAFrom(ads[2]) 68 // ... 69 // iana, packet, err := c.RequestOne(request) 70 // ... 71 // // iana now contains the IP assigned in the IAAddr option. 72 type Client struct { 73 // The interface to send requests on. 74 iface netlink.Link 75 76 // Packet socket to send on. 77 conn net.PacketConn 78 79 // Max number of attempts to multicast DHCPv6 solicits. 80 // -1 means infinity. 81 retry int 82 83 // Timeout for each Solicit try. 84 timeout time.Duration 85 } 86 87 // New returns a new DHCPv6 client based on the given parameters. 88 func New(iface netlink.Link, opts ...ClientOpt) (*Client, error) { 89 haddr := iface.Attrs().HardwareAddr 90 ip, err := eui64.ParseMAC(net.ParseIP("fe80::"), haddr) 91 if err != nil { 92 return nil, err 93 } 94 95 conn, err := net.ListenUDP("udp6", &net.UDPAddr{ 96 IP: ip, 97 Port: ClientPort, 98 Zone: iface.Attrs().Name, 99 }) 100 if err != nil { 101 return nil, err 102 } 103 104 c := &Client{ 105 iface: iface, 106 conn: conn, 107 timeout: 10 * time.Second, 108 retry: 3, 109 } 110 111 for _, opt := range opts { 112 if err := opt(c); err != nil { 113 return nil, err 114 } 115 } 116 return c, nil 117 } 118 119 // ClientOpt is a function that configures the client. 120 type ClientOpt func(*Client) error 121 122 // WithTimeout configures the retransmission timeout. 123 // 124 // Default is 10 seconds. 125 // 126 // TODO(hugelgupf): Check RFC for retransmission behavior. 127 func WithTimeout(d time.Duration) ClientOpt { 128 return func(c *Client) error { 129 c.timeout = d 130 return nil 131 } 132 } 133 134 // WithRetry configures the retransmission counts. 135 // 136 // Default is 3. 137 // 138 // TODO(hugelgupf): Check RFC for retransmission behavior. 139 func WithRetry(retry int) ClientOpt { 140 return func(c *Client) error { 141 c.retry = retry 142 return nil 143 } 144 } 145 146 // RapidSolicit solicits one non-temporary address assignment by multicasting a 147 // DHCPv6 solicitation message with the rapid commit option. 148 // 149 // RapidSolicit returns the first valid, suitable response by any remote server. 150 func (c *Client) RapidSolicit() (*dhcp6opts.IANA, *dhcp6.Packet, error) { 151 solicit, err := NewRapidSolicit(c.iface.Attrs().HardwareAddr) 152 if err != nil { 153 return nil, nil, err 154 } 155 return c.RequestOne(solicit) 156 } 157 158 // RequestOne multicasts the `request` and returns the first matching IANA and 159 // its associated Packet returned by any server. 160 func (c *Client) RequestOne(request *dhcp6.Packet) (*dhcp6opts.IANA, *dhcp6.Packet, error) { 161 ianas, pkt, err := c.Request(request) 162 if err != nil { 163 return nil, nil, err 164 } 165 if len(ianas) != 1 { 166 return nil, nil, fmt.Errorf("got %d IANAs, expected 1", len(ianas)) 167 } 168 return ianas[0], pkt, nil 169 } 170 171 // Solicit multicasts a Solicit message and collects all Advertise responses 172 // received before c.timeout expires. 173 // 174 // Solicit blocks until either: 175 // - `ctx` is canceled; or 176 // - we have exhausted all configured retries and timeouts. 177 func (c *Client) Solicit(ctx context.Context) ([]*dhcp6.Packet, error) { 178 solicit, err := NewSolicitPacket(c.iface.Attrs().HardwareAddr) 179 if err != nil { 180 return nil, err 181 } 182 183 wg, out, errCh := c.SimpleSendAndRead(ctx, DefaultServers, solicit) 184 defer wg.Wait() 185 186 var ads []*dhcp6.Packet 187 // resps is closed by SimpleSendAndRead when done. 188 for r := range out { 189 if r.Packet.MessageType == dhcp6.MessageTypeAdvertise { 190 ads = append(ads, r.Packet) 191 } 192 } 193 194 if err, ok := <-errCh; ok && err != nil { 195 return nil, err 196 } 197 return ads, nil 198 } 199 200 // This name smells. 201 type errorList []string 202 203 func newManyErrs() *errorList { 204 return new(errorList) 205 } 206 207 func (e *errorList) add(err error) { 208 *e = append(*e, err.Error()) 209 } 210 211 func (e errorList) Error() string { 212 return strings.Join([]string(e), "; ") 213 } 214 215 // Request requests non-temporary address assignments by multicasting the given 216 // message. 217 // 218 // This request message may be any DHCPv6 request message type; e.g. a 219 // Solicit with the Rapid Commit option or a Rebind message. 220 func (c *Client) Request(request *dhcp6.Packet) ([]*dhcp6opts.IANA, *dhcp6.Packet, error) { 221 errs := newManyErrs() 222 223 // These are the IANAs we are looking for in responses. 224 reqIANAs, err := dhcp6opts.GetIANA(request.Options) 225 if err != nil { 226 return nil, nil, fmt.Errorf("request packet contains no IANAs: %v", err) 227 } 228 229 ctx, cancel := context.WithCancel(context.Background()) 230 wg, out, errCh := c.SimpleSendAndRead(ctx, DefaultServers, request) 231 // Explicitly cancel the goroutine first, then wait. 232 defer func() { 233 cancel() 234 wg.Wait() 235 }() 236 237 for packet := range out { 238 if ianas, err := SuitableReply(reqIANAs, packet.Packet); err != nil { 239 errs.add(err) 240 } else { 241 // Guess we found our IANAs! The context will cancel 242 // all our problems. 243 return ianas, packet.Packet, nil 244 } 245 } 246 247 // Check if an error occurred. 248 if err, ok := <-errCh; ok && err != nil { 249 errs.add(err) 250 } 251 252 errs.add(fmt.Errorf("no suitable responses")) 253 return nil, nil, errs 254 } 255 256 // ClientPacket is a DHCP packet and the interface it corresponds to. 257 type ClientPacket struct { 258 Interface netlink.Link 259 Packet *dhcp6.Packet 260 } 261 262 // ClientError is an error that occurred on the associated interface. 263 type ClientError struct { 264 Interface netlink.Link 265 Err error 266 } 267 268 // Error implements error. 269 func (ce *ClientError) Error() string { 270 if ce.Interface != nil { 271 return fmt.Sprintf("error on %q: %v", ce.Interface.Attrs().Name, ce.Err) 272 } 273 return fmt.Sprintf("error without interface: %v", ce.Err) 274 } 275 276 func (c *Client) newClientErr(err error) *ClientError { 277 if err == nil { 278 return nil 279 } 280 return &ClientError{ 281 Interface: c.iface, 282 Err: err, 283 } 284 } 285 286 // SimpleSendAndRead multicasts a DHCPv6 packet and launches a goroutine to 287 // read response packets. Those response packets will be sent on the channel 288 // returned. The sender will close both goroutines when it stops reading 289 // packets, for example when the context is canceled. 290 // 291 // Callers must cancel ctx when they have received the packet they are looking 292 // for. Otherwise, the spawned goroutine will keep reading until it times out. 293 // More importantly, if you send another packet, the spawned goroutine may read 294 // the response faster than the one launched for the other packet. 295 // 296 // See Client.Solicit for an example use of SendAndRead. 297 // 298 // Callers sending a packet on one interface should use this. Callers intending 299 // to send packets on many interface at the same time, should look at using 300 // SendAndRead instead. 301 // 302 // Example Usage: 303 // 304 // func sendRequest(someRequest *Packet...) (*Packet, error) { 305 // ctx, cancel := context.WithCancel(context.Background()) 306 // defer cancel() 307 // 308 // out, errCh := c.SimpleSendAndRead(ctx, DefaultServers, someRequest) 309 // 310 // for response := range out { 311 // if response == What You Want { 312 // // Context cancelation will stop the reading goroutine. 313 // return response, ... 314 // } 315 // } 316 // 317 // if err, ok := <-errCh; ok && err != nil { 318 // return nil, err 319 // } 320 // return nil, fmt.Errorf("got no valid responses") 321 // } 322 // 323 // TODO(hugelgupf): since the client only has one connection, maybe it should 324 // just have one dedicated goroutine for reading from the UDP socket, and use a 325 // request and response queue. 326 func (c *Client) SimpleSendAndRead(ctx context.Context, dest *net.UDPAddr, p *dhcp6.Packet) (*sync.WaitGroup, <-chan *ClientPacket, <-chan *ClientError) { 327 out := make(chan *ClientPacket, 10) 328 errOut := make(chan *ClientError, 1) 329 var wg sync.WaitGroup 330 wg.Add(1) 331 go func() { 332 c.SendAndRead(ctx, dest, p, out, errOut) 333 close(out) 334 close(errOut) 335 wg.Done() 336 }() 337 return &wg, out, errOut 338 } 339 340 // SendAndRead sends the given packet `dest` to `to` and reads 341 // responses on the UDP connection. Any valid DHCP reply with the correct 342 // Transaction ID is sent on `out`. 343 // 344 // SendAndRead blocks reading response packets until either: 345 // - `ctx` is canceled; or 346 // - we have exhausted all configured retries and timeouts. 347 // 348 // SendAndRead retries sending the packet and receiving responses according to 349 // the configured number of c.retry, using a response timeout of c.timeout. 350 // 351 // TODO(hugelgupf): SendAndRead should follow RFC 3315 Section 14 for 352 // retransmission behavior. Also conform to Section 15 for what kind of 353 // messages must be discarded. 354 func (c *Client) SendAndRead(ctx context.Context, dest *net.UDPAddr, p *dhcp6.Packet, out chan<- *ClientPacket, errCh chan<- *ClientError) { 355 // This ensures that 356 // - we send at most one error on errCh; and 357 // - we don't forget to send err on errCh in the many return statements 358 // of sendAndRead. 359 if err := c.sendAndRead(ctx, dest, p, out); err != nil { 360 errCh <- c.newClientErr(err) 361 } 362 } 363 364 func (c *Client) sendAndRead(ctx context.Context, dest *net.UDPAddr, p *dhcp6.Packet, out chan<- *ClientPacket) error { 365 pkt, err := p.MarshalBinary() 366 if err != nil { 367 return err 368 } 369 370 return c.retryFn(func() error { 371 if _, err := c.conn.WriteTo(pkt, dest); err != nil { 372 return fmt.Errorf("error writing packet to connection: %v", err) 373 } 374 375 var numPackets int 376 timeoutCtx, cancel := context.WithTimeout(ctx, c.timeout) 377 defer cancel() 378 for { 379 select { 380 case <-timeoutCtx.Done(): 381 if numPackets > 0 { 382 return nil 383 } 384 385 // No packets received. Sadness. 386 return timeoutCtx.Err() 387 default: 388 } 389 390 // Since a context can be canceled not just because of 391 // a deadline, we must check the context every once in 392 // a while. Use what is (hopefully) a small part of the 393 // context deadline rather than the context's deadline. 394 c.conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) 395 396 // TODO: Clients can send a "max packet size" option in 397 // their packets, IIRC. Choose a reasonable size and 398 // set it. 399 b := make([]byte, 1500) 400 n, _, err := c.conn.ReadFrom(b) 401 if oerr, ok := err.(*net.OpError); ok && oerr.Timeout() { 402 // Continue to check ctx.Done() above and 403 // return the appropriate error. 404 continue 405 } else if err != nil { 406 return fmt.Errorf("error reading from UDP connection: %v", err) 407 } 408 409 pkt := &dhcp6.Packet{} 410 if err := pkt.UnmarshalBinary(b[:n]); err != nil { 411 // Not a valid DHCPv6 reply; keep listening. 412 continue 413 } 414 415 if pkt.TransactionID != p.TransactionID { 416 // Not the right response packet. 417 continue 418 } 419 420 numPackets++ 421 422 clientPkt := &ClientPacket{ 423 Packet: pkt, 424 Interface: c.iface, 425 } 426 427 // Make sure that sending the response has priority. 428 select { 429 case out <- clientPkt: 430 continue 431 default: 432 } 433 434 // We deliberately only check the parent context here. 435 // c.timeout should only apply to reading from the 436 // conn, not sending on out. 437 select { 438 case <-ctx.Done(): 439 return ctx.Err() 440 case out <- clientPkt: 441 } 442 } 443 }) 444 } 445 446 // SuitableReply validates whether a pkt is a valid Reply message as defined by 447 // RFC 3315, Section 18.1.8. 448 // 449 // It returns all valid IANAs corresponding to requested IANAs. 450 func SuitableReply(reqIANAs []*dhcp6opts.IANA, pkt *dhcp6.Packet) ([]*dhcp6opts.IANA, error) { 451 // RFC 3315, Section 18.1.8. 452 // A suitable Reply packet must have: 453 // 454 // - non-negative status code (or no status), and 455 // - an IANA with IAID matching one of the ones we used in our request, and 456 // -- a non-negative status code (or no status) in the matching IANA, and 457 // -- a non-zero number of IAAddrs in the matching IANA. 458 if pkt.MessageType != dhcp6.MessageTypeReply { 459 return nil, fmt.Errorf("got DHCP message of type %s, wanted %s", pkt.MessageType, dhcp6.MessageTypeReply) 460 } 461 462 if status, err := dhcp6opts.GetStatusCode(pkt.Options); err == nil && status.Code != dhcp6.StatusSuccess { 463 return nil, fmt.Errorf("packet has status %s: %s", status.Code, status.Message) 464 } 465 466 ianas, err := dhcp6opts.GetIANA(pkt.Options) 467 if err != nil { 468 return nil, fmt.Errorf("successful packet had problem with IANA: %v", err) 469 } 470 471 var returned []*dhcp6opts.IANA 472 for _, iana := range ianas { 473 for _, reqIANA := range reqIANAs { 474 if iana.IAID != reqIANA.IAID { 475 continue 476 } 477 478 if status, err := dhcp6opts.GetStatusCode(iana.Options); err == nil && status.Code != dhcp6.StatusSuccess { 479 continue 480 } 481 482 iaAddrs, err := dhcp6opts.GetIAAddr(iana.Options) 483 if err != nil || len(iaAddrs) == 0 { 484 continue 485 } 486 487 returned = append(returned, iana) 488 } 489 } 490 491 return returned, nil 492 } 493 494 func (c *Client) retryFn(fn func() error) error { 495 // Each retry takes the amount of timeout at worst. 496 for i := 0; i < c.retry || c.retry < 0; i++ { 497 switch err := fn(); err { 498 case nil: 499 // Got it! 500 return nil 501 502 case context.DeadlineExceeded: 503 // Just retry. 504 // TODO(hugelgupf): Sleep here for some random amount of time. 505 506 default: 507 return err 508 } 509 } 510 511 return context.DeadlineExceeded 512 }