| // Copyright 2019 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // +build !build_with_native_toolchain |
| |
| package dhcp |
| |
| import ( |
| "bytes" |
| "context" |
| "fmt" |
| "math/rand" |
| "net" |
| "sync/atomic" |
| "time" |
| |
| "go.fuchsia.dev/fuchsia/src/connectivity/network/netstack/util" |
| syslog "go.fuchsia.dev/fuchsia/src/lib/syslog/go" |
| |
| "gvisor.dev/gvisor/pkg/tcpip" |
| "gvisor.dev/gvisor/pkg/tcpip/buffer" |
| "gvisor.dev/gvisor/pkg/tcpip/header" |
| "gvisor.dev/gvisor/pkg/tcpip/stack" |
| "gvisor.dev/gvisor/pkg/tcpip/transport/packet" |
| "gvisor.dev/gvisor/pkg/waiter" |
| ) |
| |
| const ( |
| tag = "DHCP" |
| defaultLeaseLength Seconds = 12 * 3600 |
| |
| // stateRecentHistoryLength is the length of recent DHCP state transitions. |
| // A value large enough must be used to allow the last 24h to be recorded even |
| // when the DHCP lease is renewed every hour. And if the DHCP state machine |
| // breaks and cycles through state very fast, this will be enough data to |
| // possibly find a pattern. |
| stateRecentHistoryLength = 128 |
| |
| // As per RFC 2131 section 3.1, |
| // |
| // If the client detects that the address is already in use (e.g., through |
| // the use of ARP), the client MUST send a DHCPDECLINE message to the server |
| // and restarts the configuration process. The client SHOULD wait a minimum |
| // of ten seconds before restarting the configuration process to avoid |
| // excessive network traffic in case of looping. |
| minBackoffAfterDupAddrDetetected = 10 * time.Second |
| ) |
| |
| // Based on RFC 2131 Sec. 4.4.5, this defaults to (0.5 * duration_of_lease). |
| func defaultRenewTime(leaseLength Seconds) Seconds { return leaseLength / 2 } |
| |
| // Based on RFC 2131 Sec. 4.4.5, this defaults to (0.875 * duration_of_lease). |
| func defaultRebindTime(leaseLength Seconds) Seconds { return (leaseLength * 875) / 1000 } |
| |
| type AcquiredFunc func(lost, acquired tcpip.AddressWithPrefix, cfg Config) |
| |
| // Client is a DHCP client. |
| type Client struct { |
| stack *stack.Stack |
| networkEndpoint stack.NetworkEndpoint |
| xid xid |
| |
| // info holds the Client's state as type Info. |
| info atomic.Value |
| |
| // TODO(https://fxbug.dev/71350): Once we can persist inspect snapshot across |
| // reboots, remove the recent state history. |
| stateRecentHistory util.CircularLogs |
| |
| stats Stats |
| |
| acquiredFunc AcquiredFunc |
| |
| wq waiter.Queue |
| |
| // Used to ensure that only one Run goroutine per interface may be |
| // permitted to run at a time. In certain cases, rapidly flapping the |
| // DHCP client on and off can cause a second instance of Run to start |
| // before the existing one has finished, which can violate invariants. |
| // At the time of writing, TestDhcpConfiguration was creating this |
| // scenario and causing panics. |
| sem chan struct{} |
| |
| // Stubbable in test. |
| rand *rand.Rand |
| retransTimeout func(time.Duration) <-chan time.Time |
| acquire func(context.Context, *Client, string, *Info) (Config, error) |
| now func() time.Time |
| } |
| |
| // Stats collects DHCP statistics per client. |
| type Stats struct { |
| InitAcquire tcpip.StatCounter |
| RenewAcquire tcpip.StatCounter |
| RebindAcquire tcpip.StatCounter |
| SendDiscovers tcpip.StatCounter |
| RecvOffers tcpip.StatCounter |
| SendRequests tcpip.StatCounter |
| RecvAcks tcpip.StatCounter |
| RecvNaks tcpip.StatCounter |
| SendDiscoverErrors tcpip.StatCounter |
| SendRequestErrors tcpip.StatCounter |
| RecvOfferErrors tcpip.StatCounter |
| RecvOfferUnexpectedType tcpip.StatCounter |
| RecvOfferOptsDecodeErrors tcpip.StatCounter |
| RecvOfferTimeout tcpip.StatCounter |
| RecvOfferAcquisitionTimeout tcpip.StatCounter |
| RecvAckErrors tcpip.StatCounter |
| RecvNakErrors tcpip.StatCounter |
| RecvAckOptsDecodeErrors tcpip.StatCounter |
| RecvAckAddrErrors tcpip.StatCounter |
| RecvAckUnexpectedType tcpip.StatCounter |
| RecvAckTimeout tcpip.StatCounter |
| RecvAckAcquisitionTimeout tcpip.StatCounter |
| ReacquireAfterNAK tcpip.StatCounter |
| } |
| |
| type Info struct { |
| // NICID is the identifer to the associated NIC. |
| NICID tcpip.NICID |
| // LinkAddr is the link-address of the associated NIC. |
| LinkAddr tcpip.LinkAddress |
| // Acquisition is the duration within which a complete DHCP transaction must |
| // complete before timing out. |
| Acquisition time.Duration |
| // Backoff is the duration for which the client must wait before starting a |
| // new DHCP transaction after a failed transaction. |
| Backoff time.Duration |
| // Retransmission is the duration to wait before resending a DISCOVER or |
| // REQUEST within an active transaction. |
| Retransmission time.Duration |
| // Acquired is the network address most recently acquired by the client. |
| Acquired tcpip.AddressWithPrefix |
| // State is the DHCP client state. |
| State dhcpClientState |
| // Assigned is the network address added by the client to its stack. |
| Assigned tcpip.AddressWithPrefix |
| // LeaseExpiration is the time at which the client's current lease will |
| // expire. |
| LeaseExpiration time.Time |
| // RenewTime is the at which the client will transition to its renewing state. |
| RenewTime time.Time |
| // RebindTime is the time at which the client will transition to its rebinding |
| // state. |
| RebindTime time.Time |
| // Config is the last DHCP configuration assigned to the client by the server. |
| Config Config |
| } |
| |
| // NewClient creates a DHCP client. |
| // |
| // acquiredFunc will be called after each DHCP acquisition, and is responsible |
| // for making necessary modifications to the stack state. |
| func NewClient( |
| s *stack.Stack, |
| nicid tcpip.NICID, |
| linkAddr tcpip.LinkAddress, |
| acquisition, |
| backoff, |
| retransmission time.Duration, |
| acquiredFunc AcquiredFunc, |
| ) *Client { |
| ep, err := s.GetNetworkEndpoint(nicid, header.IPv4ProtocolNumber) |
| if err != nil { |
| panic(fmt.Sprintf("stack.GetNetworkEndpoint(%d, header.IPv4ProtocolNumber): %s", nicid, err)) |
| } |
| c := &Client{ |
| stack: s, |
| networkEndpoint: ep, |
| acquiredFunc: acquiredFunc, |
| sem: make(chan struct{}, 1), |
| rand: rand.New(rand.NewSource(time.Now().UnixNano())), |
| retransTimeout: time.After, |
| acquire: acquire, |
| now: time.Now, |
| stateRecentHistory: util.MakeCircularLogs(stateRecentHistoryLength), |
| } |
| c.StoreInfo(&Info{ |
| NICID: nicid, |
| LinkAddr: linkAddr, |
| Acquisition: acquisition, |
| Retransmission: retransmission, |
| Backoff: backoff, |
| }) |
| return c |
| } |
| |
| // Info returns a copy of the synchronized state of the Info. |
| func (c *Client) Info() Info { |
| return c.info.Load().(Info) |
| } |
| |
| // StoreInfo updates the synchronized copy of the DHCP Info and if the client's |
| // state changed, it will log it in the state recent history. |
| // |
| // Because of the size of Info, it is passed as a pointer to avoid an extra |
| // unnecessary copy. |
| func (c *Client) StoreInfo(info *Info) { |
| c.stateRecentHistory.Push(info.State.String()) |
| c.info.Store(*info) |
| } |
| |
| // Stats returns a reference to the Client`s stats. |
| func (c *Client) Stats() *Stats { |
| return &c.stats |
| } |
| |
| func (c *Client) StateRecentHistory() []util.LogEntry { |
| return c.stateRecentHistory.BuildLogs() |
| } |
| |
| // Run runs the DHCP client. |
| // |
| // The function periodically searches for a new IP address. |
| func (c *Client) Run(ctx context.Context) { |
| info := c.Info() |
| |
| nicName := c.stack.FindNICNameFromID(info.NICID) |
| |
| // For the initial iteration of the acquisition loop, the client should |
| // be in the initSelecting state, corresponding to the |
| // INIT->SELECTING->REQUESTING->BOUND state transition: |
| // https://tools.ietf.org/html/rfc2131#section-4.4 |
| info.State = initSelecting |
| |
| c.sem <- struct{}{} |
| defer func() { <-c.sem }() |
| defer func() { |
| _ = syslog.InfoTf(tag, "%s: client is stopping, cleaning up", nicName) |
| c.cleanup(&info, nicName, true /* release */) |
| }() |
| |
| for { |
| if err := func() error { |
| acquisitionTimeout := info.Acquisition |
| |
| // Adjust the timeout to make sure client is not stuck in retransmission |
| // when it should transition to the next state. This can only happen for |
| // two time-driven transitions: RENEW->REBIND, REBIND->INIT. |
| // |
| // Another time-driven transition BOUND->RENEW is not handled here because |
| // the client does not have to send out any request during BOUND. |
| switch s := info.State; s { |
| case initSelecting: |
| // Nothing to do. The client is initializing, no leases have been acquired. |
| // Thus no times are set for renew, rebind, and lease expiration. |
| c.stats.InitAcquire.Increment() |
| case renewing: |
| c.stats.RenewAcquire.Increment() |
| // Instead of `time.Until`, use `now` stored on the client so |
| // it can be stubbed out in test for consistency. |
| if tilRebind := info.RebindTime.Sub(c.now()); tilRebind < acquisitionTimeout { |
| acquisitionTimeout = tilRebind |
| } |
| case rebinding: |
| c.stats.RebindAcquire.Increment() |
| // Instead of `time.Until`, use `now` stored on the client so |
| // it can be stubbed out in test for consistency. |
| if tilLeaseExpire := info.LeaseExpiration.Sub(c.now()); tilLeaseExpire < acquisitionTimeout { |
| acquisitionTimeout = tilLeaseExpire |
| } |
| default: |
| panic(fmt.Sprintf("unexpected state before acquire: %s", s)) |
| } |
| |
| ctx, cancel := context.WithTimeout(ctx, acquisitionTimeout) |
| defer cancel() |
| |
| cfg, err := c.acquire(ctx, c, nicName, &info) |
| if err != nil { |
| return err |
| } |
| if cfg.Declined { |
| c.stats.ReacquireAfterNAK.Increment() |
| c.cleanup(&info, nicName, false /* release */) |
| return nil |
| } |
| |
| if cfg.LeaseLength == 0 { |
| _ = syslog.WarnTf(tag, "%s: unspecified lease length; proceeding with default (%s)", nicName, defaultLeaseLength) |
| cfg.LeaseLength = defaultLeaseLength |
| } |
| { |
| renewTime := defaultRenewTime(cfg.LeaseLength) |
| if cfg.RenewTime == 0 { |
| _ = syslog.WarnTf(tag, "%s: unspecified renew time; proceeding with default (%s)", nicName, renewTime) |
| cfg.RenewTime = renewTime |
| } |
| if cfg.RenewTime >= cfg.LeaseLength { |
| _ = syslog.WarnTf(tag, "%s: renew time (%s) >= lease length (%s); proceeding with default (%s)", nicName, cfg.RenewTime, cfg.LeaseLength, renewTime) |
| cfg.RenewTime = renewTime |
| } |
| } |
| { |
| rebindTime := defaultRebindTime(cfg.LeaseLength) |
| if cfg.RebindTime == 0 { |
| cfg.RebindTime = rebindTime |
| } |
| if cfg.RebindTime <= cfg.RenewTime { |
| _ = syslog.WarnTf(tag, "%s: rebind time (%s) <= renew time (%s); proceeding with default (%s)", nicName, cfg.RebindTime, cfg.RenewTime, rebindTime) |
| cfg.RebindTime = rebindTime |
| } |
| } |
| |
| if info.State == initSelecting { |
| if err := func() error { |
| ch := make(chan stack.DADResult, 1) |
| addr := info.Acquired.Address |
| // Per RFC 2131 section 3.1: |
| // |
| // 5. The client receives the DHCPACK message with configuration |
| // parameters. The client SHOULD perform a final check on the |
| // parameters (e.g., ARP for allocated network address), and notes the |
| // duration of the lease specified in the DHCPACK message. At this |
| // point, the client is configured. If the client detects that the |
| // address is already in use (e.g., through the use of ARP), the |
| // client MUST send a DHCPDECLINE message to the server and restarts |
| // the configuration process. The client SHOULD wait a minimum of ten |
| // seconds before restarting the configuration process to avoid |
| // excessive network traffic in case of looping. |
| // |
| // Per RFC 2131 section 4.4.1: |
| // |
| // The client SHOULD perform a check on the suggested address to |
| // ensure that the address is not already in use. For example, if |
| // the client is on a network that supports ARP, the client may issue |
| // an ARP request for the suggested request. When broadcasting an |
| // ARP request for the suggested address, the client must fill in its |
| // own hardware address as the sender's hardware address, and 0 as |
| // the sender's IP address, to avoid confusing ARP caches in other |
| // hosts on the same subnet. If the network address appears to be in |
| // use, the client MUST send a DHCPDECLINE message to the server. The |
| // client SHOULD broadcast an ARP reply to announce the client's new |
| // IP address and clear any outdated ARP cache entries in hosts on |
| // the client's subnet. |
| res, err := c.stack.CheckDuplicateAddress(info.NICID, header.IPv4ProtocolNumber, addr, func(result stack.DADResult) { |
| ch <- result |
| }) |
| switch err.(type) { |
| case nil: |
| case *tcpip.ErrNotSupported: |
| // If the link does not support DAD, then we have no way to check if |
| // the address is already in use by a neighbor so be optimistic and |
| // proceed with the acquired address. |
| return nil |
| default: |
| return fmt.Errorf("failed to start duplicate address detection on %s: %s", addr, err) |
| } |
| switch res { |
| case stack.DADStarting, stack.DADAlreadyRunning: |
| case stack.DADDisabled: |
| // If the stack is not configured to perform DAD, then we have no |
| // way to check if the address is already in use by a neighbor so |
| // we proceed with the acquired address without checking if it is |
| // already assigned to a neighbor. |
| return nil |
| default: |
| panic(fmt.Sprintf("unexpected result = %d", res)) |
| } |
| |
| select { |
| case <-ctx.Done(): |
| return fmt.Errorf("failed to complete duplicate address detection on %s: %w", addr, ctx.Err()) |
| case result := <-ch: |
| switch result := result.(type) { |
| case *stack.DADSucceeded: |
| // DAD did not find a neighbor with the address assigned so we are |
| // safe to proceed with the address. |
| return nil |
| case *stack.DADError: |
| return fmt.Errorf("error performing duplicate address detection on %s: %s", addr, result.Err) |
| case *stack.DADAborted: |
| return fmt.Errorf("duplicate address detection aborted on %s", addr) |
| case *stack.DADDupAddrDetected: |
| info.Backoff = minBackoffAfterDupAddrDetetected |
| // As per RFC 2131 section 4.4.1, |
| // |
| // Option DHCPDECLINE |
| // ------ ----------- |
| // DHCP message type DHCPDECLINE |
| // |
| // Requested IP address MUST |
| // |
| // Server identifier MUST |
| // |
| // Client identifier MAY |
| if err := c.send( |
| ctx, |
| nicName, |
| &info, |
| options{ |
| {optDHCPMsgType, []byte{byte(dhcpDECLINE)}}, |
| {optReqIPAddr, []byte(addr)}, |
| {optDHCPServer, []byte(info.Config.ServerAddress)}, |
| }, |
| tcpip.FullAddress{ |
| NIC: info.NICID, |
| Addr: header.IPv4Broadcast, |
| Port: ServerPort, |
| }, |
| false, /* broadcast */ |
| false, /* ciaddr */ |
| ); err != nil { |
| return fmt.Errorf("%s: %w", dhcpDECLINE, err) |
| } |
| return fmt.Errorf("declined %s because it is held by %s", info.Acquired, result.HolderLinkAddress) |
| default: |
| panic(fmt.Sprintf("unhandled DAD result variant %#v", result)) |
| } |
| } |
| }(); err != nil { |
| return fmt.Errorf("DAD: %w", err) |
| } |
| } |
| |
| c.assign(&info, info.Acquired, cfg, c.now()) |
| |
| return nil |
| }(); err != nil { |
| if ctx.Err() != nil { |
| return |
| } |
| _ = syslog.InfoTf(tag, "%s: %s; retrying %s", nicName, err, info.State) |
| } |
| |
| // Synchronize info after attempt to acquire is complete. |
| c.StoreInfo(&info) |
| |
| // RFC 2131 Section 4.4.5 |
| // https://tools.ietf.org/html/rfc2131#section-4.4.5 |
| // |
| // T1 MUST be earlier than T2, which, in turn, MUST be earlier than |
| // the time at which the client's lease will expire. |
| var next dhcpClientState |
| var waitDuration time.Duration |
| switch now := c.now(); { |
| case !now.Before(info.LeaseExpiration): |
| next = initSelecting |
| case !now.Before(info.RebindTime): |
| next = rebinding |
| case !now.Before(info.RenewTime): |
| next = renewing |
| default: |
| switch s := info.State; s { |
| case renewing, rebinding: |
| // This means the client is stuck in a bad state, because if |
| // the timers are correctly set, previous cases should have matched. |
| panic(fmt.Sprintf( |
| "invalid client state %s, now=%s, leaseExpirationTime=%s, renewTime=%s, rebindTime=%s", |
| s, now, info.LeaseExpiration, info.RenewTime, info.RebindTime, |
| )) |
| } |
| waitDuration = info.RenewTime.Sub(now) |
| next = renewing |
| } |
| |
| // No state transition occurred, the client is retrying. |
| if info.State == next { |
| waitDuration = info.Backoff |
| } |
| |
| if info.State != next && next != renewing { |
| // Transition immediately for RENEW->REBIND, REBIND->INIT. |
| if ctx.Err() != nil { |
| return |
| } |
| } else { |
| _ = syslog.InfoTf(tag, "%s: scheduling renewal in %.fs", nicName, waitDuration.Seconds()) |
| select { |
| case <-ctx.Done(): |
| return |
| case <-c.retransTimeout(waitDuration): |
| } |
| } |
| |
| if info.State != initSelecting && next == initSelecting { |
| _ = syslog.WarnTf(tag, "%s: lease time expired, cleaning up", nicName) |
| c.cleanup(&info, nicName, true /* release */) |
| } |
| |
| info.State = next |
| |
| // Synchronize info after any state updates. |
| c.StoreInfo(&info) |
| } |
| } |
| |
| func (c *Client) assign(info *Info, acquired tcpip.AddressWithPrefix, config Config, now time.Time) { |
| c.updateInfo(info, acquired, config, now, bound) |
| } |
| |
| func (c *Client) updateInfo(info *Info, acquired tcpip.AddressWithPrefix, config Config, now time.Time, state dhcpClientState) { |
| prevAssigned := info.Assigned |
| info.Assigned = acquired |
| info.LeaseExpiration = now.Add(config.LeaseLength.Duration()) |
| info.RenewTime = now.Add(config.RenewTime.Duration()) |
| info.RebindTime = now.Add(config.RebindTime.Duration()) |
| info.Config = config |
| info.State = state |
| c.StoreInfo(info) |
| if fn := c.acquiredFunc; fn != nil { |
| fn(prevAssigned, acquired, config) |
| } |
| } |
| |
| func (c *Client) cleanup(info *Info, nicName string, release bool) { |
| if release && info.Assigned != (tcpip.AddressWithPrefix{}) { |
| if err := func() error { |
| // As per RFC 2131 section 4.4.1, |
| // |
| // Option DHCPRELEASE |
| // ------ ----------- |
| // DHCP message type DHCPRELEASE |
| // |
| // Requested IP address MUST NOT |
| // |
| // Server identifier MUST |
| // |
| // Client identifier MAY |
| if err := c.send( |
| context.Background(), |
| nicName, |
| info, |
| options{ |
| {optDHCPMsgType, []byte{byte(dhcpRELEASE)}}, |
| {optDHCPServer, []byte(info.Config.ServerAddress)}, |
| }, |
| tcpip.FullAddress{ |
| Addr: info.Config.ServerAddress, |
| Port: ServerPort, |
| NIC: info.NICID, |
| }, |
| false, /* broadcast */ |
| true, /* ciaddr */ |
| ); err != nil { |
| return fmt.Errorf("%s: %w", dhcpRELEASE, err) |
| } |
| return nil |
| }(); err != nil { |
| _ = syslog.WarnTf(tag, "%s, continuing", err) |
| } |
| } |
| |
| c.updateInfo(info, tcpip.AddressWithPrefix{}, Config{}, time.Time{}, info.State) |
| } |
| |
| const maxBackoff = 64 * time.Second |
| |
| // Exponential backoff calculates the backoff delay for this iteration (0-indexed) of retransmission. |
| // |
| // RFC 2131 section 4.1 |
| // https://tools.ietf.org/html/rfc2131#section-4.1 |
| // |
| // The delay between retransmissions SHOULD be |
| // chosen to allow sufficient time for replies from the server to be |
| // delivered based on the characteristics of the internetwork between |
| // the client and the server. For example, in a 10Mb/sec Ethernet |
| // internetwork, the delay before the first retransmission SHOULD be 4 |
| // seconds randomized by the value of a uniform random number chosen |
| // from the range -1 to +1. Clients with clocks that provide resolution |
| // granularity of less than one second may choose a non-integer |
| // randomization value. The delay before the next retransmission SHOULD |
| // be 8 seconds randomized by the value of a uniform number chosen from |
| // the range -1 to +1. The retransmission delay SHOULD be doubled with |
| // subsequent retransmissions up to a maximum of 64 seconds. |
| func (c *Client) exponentialBackoff(iteration uint) time.Duration { |
| jitter := time.Duration(c.rand.Int63n(int64(2*time.Second+1))) - time.Second // [-1s, +1s] |
| backoff := maxBackoff |
| // Guards against overflow. |
| if retransmission := c.Info().Retransmission; (maxBackoff/retransmission)>>iteration != 0 { |
| backoff = retransmission * (1 << iteration) |
| } |
| backoff += jitter |
| if backoff < 0 { |
| return 0 |
| } |
| return backoff |
| } |
| |
| func acquire(ctx context.Context, c *Client, nicName string, info *Info) (Config, error) { |
| // https://tools.ietf.org/html/rfc2131#section-4.3.6 Client messages: |
| // |
| // --------------------------------------------------------------------- |
| // | |INIT-REBOOT |SELECTING |RENEWING |REBINDING | |
| // --------------------------------------------------------------------- |
| // |broad/unicast |broadcast |broadcast |unicast |broadcast | |
| // |server-ip |MUST NOT |MUST |MUST NOT |MUST NOT | |
| // |requested-ip |MUST |MUST |MUST NOT |MUST NOT | |
| // |ciaddr |zero |zero |IP address |IP address| |
| // --------------------------------------------------------------------- |
| writeTo := tcpip.FullAddress{ |
| Addr: header.IPv4Broadcast, |
| Port: ServerPort, |
| NIC: info.NICID, |
| } |
| |
| ep, err := packet.NewEndpoint(c.stack, true /* cooked */, header.IPv4ProtocolNumber, &c.wq) |
| if err != nil { |
| return Config{}, fmt.Errorf("packet.NewEndpoint(_, true, header.IPv4ProtocolNumber, _): %s", err) |
| } |
| defer ep.Close() |
| |
| recvOn := tcpip.FullAddress{ |
| NIC: info.NICID, |
| } |
| if err := ep.Bind(recvOn); err != nil { |
| return Config{}, fmt.Errorf("ep.Bind(%+v): %s", recvOn, err) |
| } |
| |
| switch info.State { |
| case initSelecting: |
| case renewing: |
| writeTo.Addr = info.Config.ServerAddress |
| case rebinding: |
| default: |
| panic(fmt.Sprintf("unknown client state: c.State=%s", info.State)) |
| } |
| |
| we, ch := waiter.NewChannelEntry(nil) |
| c.wq.EventRegister(&we, waiter.EventIn) |
| defer c.wq.EventUnregister(&we) |
| |
| if _, err := c.rand.Read(c.xid[:]); err != nil { |
| return Config{}, fmt.Errorf("c.rand.Read(): %w", err) |
| } |
| |
| commonOpts := options{ |
| {optParamReq, []byte{ |
| 1, // request subnet mask |
| 3, // request router |
| 15, // domain name |
| 6, // domain name server |
| }}, |
| } |
| requestedAddr := info.Acquired |
| if info.State == initSelecting { |
| discOpts := append(options{ |
| {optDHCPMsgType, []byte{byte(dhcpDISCOVER)}}, |
| }, commonOpts...) |
| if len(requestedAddr.Address) != 0 { |
| discOpts = append(discOpts, option{optReqIPAddr, []byte(requestedAddr.Address)}) |
| } |
| |
| retransmitDiscover: |
| for i := uint(0); ; i++ { |
| if err := c.send( |
| ctx, |
| nicName, |
| info, |
| discOpts, |
| writeTo, |
| false, /* broadcast */ |
| false, /* ciaddr */ |
| ); err != nil { |
| c.stats.SendDiscoverErrors.Increment() |
| return Config{}, fmt.Errorf("%s: %w", dhcpDISCOVER, err) |
| } |
| c.stats.SendDiscovers.Increment() |
| |
| // Receive a DHCPOFFER message from a responding DHCP server. |
| retransmit := c.retransTimeout(c.exponentialBackoff(i)) |
| for { |
| result, retransmit, err := c.recv(ctx, nicName, ep, ch, retransmit) |
| if err != nil { |
| if retransmit { |
| c.stats.RecvOfferAcquisitionTimeout.Increment() |
| } else { |
| c.stats.RecvOfferErrors.Increment() |
| } |
| return Config{}, fmt.Errorf("recv %s: %w", dhcpOFFER, err) |
| } |
| if retransmit { |
| c.stats.RecvOfferTimeout.Increment() |
| _ = syslog.WarnTf(tag, "%s: recv timeout waiting for %s; retransmitting %s", nicName, dhcpOFFER, dhcpDISCOVER) |
| continue retransmitDiscover |
| } |
| |
| if result.typ != dhcpOFFER { |
| c.stats.RecvOfferUnexpectedType.Increment() |
| _ = syslog.InfoTf(tag, "%s: got DHCP type = %s from %s, want = %s; discarding", nicName, result.typ, result.source, dhcpOFFER) |
| continue |
| } |
| c.stats.RecvOffers.Increment() |
| |
| var cfg Config |
| if err := cfg.decode(result.options); err != nil { |
| c.stats.RecvOfferOptsDecodeErrors.Increment() |
| return Config{}, fmt.Errorf("%s decode: %w", result.typ, err) |
| } |
| |
| if len(cfg.SubnetMask) == 0 { |
| cfg.SubnetMask = tcpip.AddressMask(net.IP(result.yiaddr).DefaultMask()) |
| } |
| |
| // We do not perform sophisticated offer selection and instead merely |
| // select the first valid offer we receive. |
| info.Config = cfg |
| |
| prefixLen, _ := net.IPMask(info.Config.SubnetMask).Size() |
| requestedAddr = tcpip.AddressWithPrefix{ |
| Address: result.yiaddr, |
| PrefixLen: prefixLen, |
| } |
| |
| _ = syslog.InfoTf( |
| tag, |
| "%s: got %s from %s: Address=%s, server=%s, leaseLength=%s, renewTime=%s, rebindTime=%s", |
| nicName, |
| result.typ, |
| result.source, |
| requestedAddr, |
| info.Config.ServerAddress, |
| info.Config.LeaseLength, |
| info.Config.RenewTime, |
| info.Config.RebindTime, |
| ) |
| |
| break retransmitDiscover |
| } |
| } |
| } |
| |
| reqOpts := append(options{ |
| {optDHCPMsgType, []byte{byte(dhcpREQUEST)}}, |
| }, commonOpts...) |
| if info.State == initSelecting { |
| reqOpts = append(reqOpts, |
| options{ |
| {optDHCPServer, []byte(info.Config.ServerAddress)}, |
| {optReqIPAddr, []byte(requestedAddr.Address)}, |
| }...) |
| } |
| |
| retransmitRequest: |
| for i := uint(0); ; i++ { |
| if err := c.send( |
| ctx, |
| nicName, |
| info, |
| reqOpts, |
| writeTo, |
| false, /* broadcast */ |
| info.State != initSelecting, /* ciaddr */ |
| ); err != nil { |
| c.stats.SendRequestErrors.Increment() |
| return Config{}, fmt.Errorf("%s: %w", dhcpREQUEST, err) |
| } |
| c.stats.SendRequests.Increment() |
| |
| // RFC 2131 Section 4.4.5 |
| // https://tools.ietf.org/html/rfc2131#section-4.4.5 |
| // |
| // In both RENEWING and REBINDING states, if the client receives no |
| // response to its DHCPREQUEST message, the client SHOULD wait one-half of |
| // the remaining time until T2 (in RENEWING state) and one-half of the |
| // remaining lease time (in REBINDING state), down to a minimum of 60 |
| // seconds, before retransmitting the DHCPREQUEST message. |
| var retransmitAfter time.Duration |
| switch info.State { |
| case initSelecting: |
| retransmitAfter = c.exponentialBackoff(i) |
| case renewing: |
| retransmitAfter = info.RebindTime.Sub(c.now()) / 2 |
| if min := 60 * time.Second; retransmitAfter < min { |
| retransmitAfter = min |
| } |
| case rebinding: |
| retransmitAfter = info.LeaseExpiration.Sub(c.now()) / 2 |
| if min := 60 * time.Second; retransmitAfter < min { |
| retransmitAfter = min |
| } |
| default: |
| panic(fmt.Sprintf("invalid client state %s", info.State)) |
| } |
| |
| // Receive a DHCPACK/DHCPNAK from the server. |
| retransmit := c.retransTimeout(retransmitAfter) |
| for { |
| result, retransmit, err := c.recv(ctx, nicName, ep, ch, retransmit) |
| if err != nil { |
| if retransmit { |
| c.stats.RecvAckAcquisitionTimeout.Increment() |
| } else { |
| c.stats.RecvAckErrors.Increment() |
| } |
| return Config{}, fmt.Errorf("recv %s: %w", dhcpACK, err) |
| } |
| if retransmit { |
| c.stats.RecvAckTimeout.Increment() |
| _ = syslog.WarnTf(tag, "%s: recv timeout waiting for %s; retransmitting %s", nicName, dhcpACK, dhcpREQUEST) |
| continue retransmitRequest |
| } |
| |
| switch result.typ { |
| case dhcpACK: |
| var cfg Config |
| if err := cfg.decode(result.options); err != nil { |
| c.stats.RecvAckOptsDecodeErrors.Increment() |
| return Config{}, fmt.Errorf("%s decode: %w", result.typ, err) |
| } |
| prefixLen, _ := net.IPMask(cfg.SubnetMask).Size() |
| if addr := (tcpip.AddressWithPrefix{ |
| Address: result.yiaddr, |
| PrefixLen: prefixLen, |
| }); addr != requestedAddr { |
| c.stats.RecvAckAddrErrors.Increment() |
| return Config{}, fmt.Errorf("%s with unexpected address=%s expected=%s", result.typ, addr, requestedAddr) |
| } |
| c.stats.RecvAcks.Increment() |
| |
| // Now that we've successfully acquired the address, update the client state. |
| info.Acquired = requestedAddr |
| _ = syslog.InfoTf(tag, "%s: got %s from %s with leaseLength=%s", nicName, result.typ, result.source, cfg.LeaseLength) |
| return cfg, nil |
| case dhcpNAK: |
| c.stats.RecvNaks.Increment() |
| if msg := result.options.message(); len(msg) != 0 { |
| _ = syslog.InfoTf(tag, "%s: got %s from %s (%s)", nicName, result.typ, result.source, msg) |
| } else { |
| _ = syslog.InfoTf(tag, "%s: got %s from %s", nicName, result.typ, result.source) |
| } |
| // We lost the lease. |
| return Config{ |
| Declined: true, |
| }, nil |
| default: |
| c.stats.RecvAckUnexpectedType.Increment() |
| _ = syslog.InfoTf(tag, "%s: got DHCP type = %s from %s, want = %s or %s; discarding", nicName, result.typ, result.source, dhcpACK, dhcpNAK) |
| continue |
| } |
| } |
| } |
| } |
| |
| func (c *Client) send( |
| ctx context.Context, |
| nicName string, |
| info *Info, |
| opts options, |
| writeTo tcpip.FullAddress, |
| broadcast, |
| ciaddr bool, |
| ) error { |
| dhcpLength := headerBaseSize + opts.len() + 1 |
| b := buffer.NewPrependable(header.IPv4MinimumSize + header.UDPMinimumSize + dhcpLength) |
| dhcpPayload := hdr(b.Prepend(dhcpLength)) |
| dhcpPayload.init() |
| dhcpPayload.setOp(opRequest) |
| if n, l := copy(dhcpPayload.xidbytes(), c.xid[:]), len(c.xid); n != l { |
| panic(fmt.Sprintf("failed to copy xid bytes, want=%d got=%d", l, n)) |
| } |
| if broadcast { |
| dhcpPayload.setBroadcast() |
| } |
| if ciaddr { |
| ciaddr := info.Assigned.Address |
| if n, l := copy(dhcpPayload.ciaddr(), ciaddr), len(ciaddr); n != l { |
| panic(fmt.Sprintf("failed to copy ciaddr bytes, want=%d got=%d", l, n)) |
| } |
| } |
| |
| chaddr := info.LinkAddr |
| if n, l := copy(dhcpPayload.chaddr(), chaddr), len(chaddr); n != l { |
| panic(fmt.Sprintf("failed to copy chaddr bytes, want=%d got=%d", l, n)) |
| } |
| dhcpPayload.setOptions(opts) |
| |
| typ, err := opts.dhcpMsgType() |
| if err != nil { |
| panic(err) |
| } |
| |
| _ = syslog.InfoTf( |
| tag, |
| "%s: send %s from %s:%d to %s:%d on NIC:%d (bcast=%t ciaddr=%t)", |
| nicName, |
| typ, |
| info.Assigned.Address, |
| ClientPort, |
| writeTo.Addr, |
| writeTo.Port, |
| writeTo.NIC, |
| broadcast, |
| ciaddr, |
| ) |
| |
| // TODO(https://gvisor.dev/issues/4957): Use more streamlined serialization |
| // functions when available. |
| |
| // Initialize the UDP header. |
| udp := header.UDP(b.Prepend(header.UDPMinimumSize)) |
| length := uint16(b.UsedLength()) |
| udp.Encode(&header.UDPFields{ |
| SrcPort: ClientPort, |
| DstPort: writeTo.Port, |
| Length: length, |
| }) |
| xsum := header.PseudoHeaderChecksum(header.UDPProtocolNumber, info.Assigned.Address, writeTo.Addr, length) |
| xsum = header.Checksum(dhcpPayload, xsum) |
| udp.SetChecksum(^udp.CalculateChecksum(xsum)) |
| |
| // Initialize the IP header. |
| ip := header.IPv4(b.Prepend(header.IPv4MinimumSize)) |
| ip.Encode(&header.IPv4Fields{ |
| TotalLength: uint16(b.UsedLength()), |
| Flags: header.IPv4FlagDontFragment, |
| ID: 0, |
| TTL: c.networkEndpoint.DefaultTTL(), |
| TOS: stack.DefaultTOS, |
| Protocol: uint8(header.UDPProtocolNumber), |
| SrcAddr: info.Assigned.Address, |
| DstAddr: writeTo.Addr, |
| }) |
| ip.SetChecksum(^ip.CalculateChecksum()) |
| |
| var linkAddress tcpip.LinkAddress |
| { |
| ch := make(chan stack.LinkResolutionResult, 1) |
| err := c.stack.GetLinkAddress(info.NICID, writeTo.Addr, info.Assigned.Address, header.IPv4ProtocolNumber, func(result stack.LinkResolutionResult) { |
| ch <- result |
| }) |
| switch err.(type) { |
| case nil: |
| result := <-ch |
| linkAddress = result.LinkAddress |
| err = result.Err |
| case *tcpip.ErrWouldBlock: |
| select { |
| case <-ctx.Done(): |
| return fmt.Errorf("client address resolution: %w", ctx.Err()) |
| case result := <-ch: |
| linkAddress = result.LinkAddress |
| err = result.Err |
| } |
| } |
| if err != nil { |
| return fmt.Errorf("failed to resolve link address: %s", err) |
| } |
| } |
| |
| if err := c.stack.WritePacketToRemote( |
| writeTo.NIC, |
| linkAddress, |
| header.IPv4ProtocolNumber, |
| b.View().ToVectorisedView(), |
| ); err != nil { |
| return fmt.Errorf("failed to write packet: %s", err) |
| } |
| return nil |
| } |
| |
| type recvResult struct { |
| source tcpip.Address |
| yiaddr tcpip.Address |
| options options |
| typ dhcpMsgType |
| } |
| |
| func (c *Client) recv( |
| ctx context.Context, |
| nicName string, |
| ep tcpip.Endpoint, |
| read <-chan struct{}, |
| retransmit <-chan time.Time, |
| ) (recvResult, bool, error) { |
| var b bytes.Buffer |
| for { |
| b.Reset() |
| |
| res, err := ep.Read(&b, tcpip.ReadOptions{ |
| NeedRemoteAddr: true, |
| NeedLinkPacketInfo: true, |
| }) |
| senderAddr := tcpip.LinkAddress(res.RemoteAddr.Addr) |
| if _, ok := err.(*tcpip.ErrWouldBlock); ok { |
| select { |
| case <-read: |
| continue |
| case <-retransmit: |
| return recvResult{}, true, nil |
| case <-ctx.Done(): |
| return recvResult{}, true, fmt.Errorf("read: %w", ctx.Err()) |
| } |
| } |
| if err != nil { |
| return recvResult{}, false, fmt.Errorf("read: %s", err) |
| } |
| |
| if res.LinkPacketInfo.Protocol != header.IPv4ProtocolNumber { |
| continue |
| } |
| |
| switch res.LinkPacketInfo.PktType { |
| case tcpip.PacketHost, tcpip.PacketBroadcast: |
| default: |
| continue |
| } |
| |
| v := b.Bytes() |
| ip := header.IPv4(v) |
| if !ip.IsValid(len(v)) { |
| _ = syslog.WarnTf( |
| tag, |
| "%s: received malformed IP frame from %s; discarding %d bytes", |
| nicName, |
| senderAddr, |
| len(v), |
| ) |
| continue |
| } |
| // TODO(https://gvisor.dev/issues/5049): Abstract away checksum validation when possible. |
| if ip.CalculateChecksum() != 0xffff { |
| _ = syslog.WarnTf( |
| tag, |
| "%s: received damaged IP frame from %s; discarding %d bytes", |
| nicName, |
| senderAddr, |
| len(v), |
| ) |
| continue |
| } |
| if ip.More() || ip.FragmentOffset() != 0 { |
| _ = syslog.WarnTf( |
| tag, |
| "%s: received fragmented IP frame from %s; discarding %d bytes", |
| nicName, |
| senderAddr, |
| len(v), |
| ) |
| continue |
| } |
| if ip.TransportProtocol() != header.UDPProtocolNumber { |
| continue |
| } |
| udp := header.UDP(ip.Payload()) |
| if len(udp) < header.UDPMinimumSize { |
| _ = syslog.WarnTf( |
| tag, |
| "%s: received malformed UDP frame (%s@%s -> %s); discarding %d bytes", |
| nicName, |
| ip.SourceAddress(), |
| senderAddr, |
| ip.DestinationAddress(), |
| len(udp), |
| ) |
| continue |
| } |
| if udp.DestinationPort() != ClientPort { |
| continue |
| } |
| if udp.Length() > uint16(len(udp)) { |
| _ = syslog.WarnTf( |
| tag, |
| "%s: received malformed UDP frame (%s@%s -> %s); discarding %d bytes", |
| nicName, |
| ip.SourceAddress(), |
| senderAddr, |
| ip.DestinationAddress(), |
| len(udp), |
| ) |
| continue |
| } |
| payload := udp.Payload() |
| if xsum := udp.Checksum(); xsum != 0 { |
| xsum := header.PseudoHeaderChecksum(header.UDPProtocolNumber, ip.DestinationAddress(), ip.SourceAddress(), udp.Length()) |
| xsum = header.Checksum(payload, xsum) |
| if udp.CalculateChecksum(xsum) != 0xffff { |
| _ = syslog.WarnTf( |
| tag, |
| "%s: received damaged UDP frame (%s@%s -> %s); discarding %d bytes", |
| nicName, |
| ip.SourceAddress(), |
| senderAddr, |
| ip.DestinationAddress(), |
| len(udp), |
| ) |
| continue |
| } |
| } |
| |
| h := hdr(payload) |
| if !h.isValid() { |
| return recvResult{}, false, fmt.Errorf("invalid hdr: %x", h) |
| } |
| |
| if op := h.op(); op != opReply { |
| return recvResult{}, false, fmt.Errorf("op-code=%s, want=%s", h, opReply) |
| } |
| |
| if !bytes.Equal(h.xidbytes(), c.xid[:]) { |
| // This message is for another client, ignore silently. |
| continue |
| } |
| |
| { |
| opts, err := h.options() |
| if err != nil { |
| return recvResult{}, false, fmt.Errorf("invalid options: %w", err) |
| } |
| |
| typ, err := opts.dhcpMsgType() |
| if err != nil { |
| return recvResult{}, false, fmt.Errorf("invalid type: %w", err) |
| } |
| |
| return recvResult{ |
| source: ip.SourceAddress(), |
| yiaddr: tcpip.Address(h.yiaddr()), |
| options: opts, |
| typ: typ, |
| }, false, nil |
| } |
| } |
| } |