/****************************************************************************
 * apps/netutils/dhcp6c/dhcp6c.c
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.  The
 * ASF licenses this file to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the
 * License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations
 * under the License.
 *
 ****************************************************************************/

/****************************************************************************
 * Included Files
 ****************************************************************************/

#include <nuttx/config.h>
#include <nuttx/compiler.h>

#include <time.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <limits.h>
#include <resolv.h>
#include <string.h>
#include <unistd.h>
#include <debug.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdint.h>
#include <malloc.h>
#include <pthread.h>
#include <sys/time.h>
#include <nuttx/clock.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/udp.h>
#include <net/if.h>
#include <net/ethernet.h>
#include <arpa/inet.h>

#include "netutils/netlib.h"
#include "netutils/dhcp6c.h"

/****************************************************************************
 * Pre-processor Definitions
 ****************************************************************************/

#define DHCPV6_ALL_RELAYS {{{0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\
                             0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02}}}
#define DHCPV6_CLIENT_PORT 546
#define DHCPV6_SERVER_PORT 547
#define DHCPV6_DUID_LLADDR 3
#define DHCPV6_REQ_DELAY 1

#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))

#define dhcpv6_for_each_option(_o, start, end, otype, olen, odata)\
    for ((_o) = (FAR uint8_t *)(start); (_o) + 4 <= (FAR uint8_t *)(end) &&\
        ((otype) = (_o)[0] << 8 | (_o)[1]) && ((odata) = (FAR void *)&(_o)[4]) &&\
        ((olen) = (_o)[2] << 8 | (_o)[3]) + (odata) <= (FAR uint8_t *)(end); \
        (_o) += 4 + ((_o)[2] << 8 | (_o)[3]))

/****************************************************************************
 * Private Types
 ****************************************************************************/

enum dhcpv6_opt_e
{
  DHCPV6_OPT_CLIENTID = 1,
  DHCPV6_OPT_SERVERID = 2,
  DHCPV6_OPT_IA_NA = 3,
  DHCPV6_OPT_IA_ADDR = 5,
  DHCPV6_OPT_ORO = 6,
  DHCPV6_OPT_PREF = 7,
  DHCPV6_OPT_ELAPSED = 8,
  DHCPV6_OPT_RELAY_MSG = 9,
  DHCPV6_OPT_AUTH = 11,
  DHCPV6_OPT_STATUS = 13,
  DHCPV6_OPT_RAPID_COMMIT = 14,
  DHCPV6_OPT_RECONF_MESSAGE = 19,
  DHCPV6_OPT_RECONF_ACCEPT = 20,
  DHCPV6_OPT_DNS_SERVERS = 23,
  DHCPV6_OPT_DNS_DOMAIN = 24,
  DHCPV6_OPT_IA_PD = 25,
  DHCPV6_OPT_IA_PREFIX = 26,
  DHCPV6_OPT_INFO_REFRESH = 32,
  DHCPV6_OPT_FQDN = 39,
  DHCPV6_OPT_NTP_SERVER = 56,
  DHCPV6_OPT_SIP_SERVER_D = 21,
  DHCPV6_OPT_SIP_SERVER_A = 22,
};

enum dhcpv6_opt_npt_e
{
  NTP_SRV_ADDR = 1,
  NTP_MC_ADDR = 2,
  NTP_SRV_FQDN = 3
};

enum dhcpv6_msg_e
{
  DHCPV6_MSG_UNKNOWN = 0,
  DHCPV6_MSG_SOLICIT = 1,
  DHCPV6_MSG_ADVERT = 2,
  DHCPV6_MSG_REQUEST = 3,
  DHCPV6_MSG_RENEW = 5,
  DHCPV6_MSG_REBIND = 6,
  DHCPV6_MSG_REPLY = 7,
  DHCPV6_MSG_RELEASE = 8,
  DHCPV6_MSG_DECLINE = 9,
  DHCPV6_MSG_RECONF = 10,
  DHCPV6_MSG_INFO_REQ = 11,
  DHCPV6_MSG_MAX
};

enum dhcpv6_status_e
{
  DHCPV6_NOADDRSAVAIL = 2,
  DHCPV6_NOPREFIXAVAIL = 6
};

enum dhcpv6_mode_e
{
  DHCPV6_UNKNOWN,
  DHCPV6_STATELESS,
  DHCPV6_STATEFUL
};

enum dhcpv6_state_e
{
  STATE_CLIENT_ID,
  STATE_SERVER_ID,
  STATE_SERVER_CAND,
  STATE_ORO,
  STATE_DNS,
  STATE_SEARCH,
  STATE_IA_NA,
  STATE_IA_PD,
  STATE_CUSTOM_OPTS,
  STATE_SNTP_IP,
  STATE_SNTP_FQDN,
  STATE_SIP_IP,
  STATE_SIP_FQDN,
  STATE_MAX
};

enum dhcp6c_ia_mode_e
{
  IA_MODE_NONE,
  IA_MODE_TRY,
  IA_MODE_FORCE,
};

/* DHCPV6 Protocol Headers */

begin_packed_struct struct dhcpv6_header_s
{
  uint8_t msg_type;
  uint8_t tr_id[3];
} end_packed_struct;

begin_packed_struct struct dhcpv6_ia_hdr_s
{
  uint16_t type;
  uint16_t len;
  uint32_t iaid;
  uint32_t t1;
  uint32_t t2;
} end_packed_struct;

begin_packed_struct struct dhcpv6_ia_addr_s
{
  uint16_t type;
  uint16_t len;
  struct in6_addr addr;
  uint32_t preferred;
  uint32_t valid;
} end_packed_struct;

begin_packed_struct struct dhcpv6_ia_prefix_s
{
  uint16_t type;
  uint16_t len;
  uint32_t preferred;
  uint32_t valid;
  uint8_t prefix;
  struct in6_addr addr;
} end_packed_struct;

struct dhcpv6_server_cand_s
{
  bool has_noaddravail;
  bool wants_reconfigure;
  int16_t preference;
  uint8_t duid_len;
  uint8_t duid[130];
};

struct dhcp6c_retx_s
{
  bool delay;
  uint8_t init_timeo;
  uint16_t max_timeo;
  char name[8];
  int(*handler_reply)(FAR void *handle, enum dhcpv6_msg_e orig,
                      FAR const void *opt, FAR const void *end,
                      uint32_t elapsed);
  int(*handler_finish)(FAR void *handle, uint32_t elapsed);
};

struct dhcp6c_state_s
{
  pthread_t thread;
  bool cancel;
  dhcp6c_callback_t callback;
  int sockfd;
  int urandom_fd;
  int ifindex;
  time_t t1;
  time_t t2;
  time_t t3;
  bool request_prefix;
  enum dhcp6c_ia_mode_e ia_mode;
  bool accept_reconfig;
  FAR uint8_t *state_data[STATE_MAX];
  size_t state_len[STATE_MAX];
};

/****************************************************************************
 * Private Function Prototypes
 ****************************************************************************/

static int dhcp6c_handle_reconfigure(FAR void *handle,
                                     enum dhcpv6_msg_e orig,
                                     FAR const void *opt,
                                     FAR const void *end,
                                     uint32_t elapsed);
static int dhcp6c_handle_advert(FAR void *handle, enum dhcpv6_msg_e orig,
                                FAR const void *opt, FAR const void *end,
                                uint32_t elapsed);
static int dhcp6c_commit_advert(FAR void *handle, uint32_t elapsed);
static int dhcp6c_handle_reply(FAR void *handle, enum dhcpv6_msg_e orig,
                               FAR const void *opt, FAR const void *end,
                               uint32_t elapsed);
static int dhcp6c_handle_rebind_reply(FAR void *handle,
                                      enum dhcpv6_msg_e orig,
                                      FAR const void *opt,
                                      FAR const void *end,
                                      uint32_t elapsed);

/****************************************************************************
 * Private Data
 ****************************************************************************/

/* RFC 3315 - 5.5 Timeout and Delay values */

static const struct dhcp6c_retx_s g_dhcp6c_retx[DHCPV6_MSG_MAX] =
{
  {false, 1, 120, "<POLL>", dhcp6c_handle_reconfigure, NULL},
  {true, 1, 120, "SOLICIT", dhcp6c_handle_advert, dhcp6c_commit_advert},
  {0},
  {true, 1, 30, "REQUEST", dhcp6c_handle_reply, NULL},
  {0},
  {false, 10, 600, "RENEW", dhcp6c_handle_reply, NULL},
  {false, 10, 600, "REBIND", dhcp6c_handle_rebind_reply, NULL},
  {0},
  {false, 1, 600, "RELEASE", NULL, NULL},
  {false, 1, 3, "DECLINE", NULL, NULL},
  {0},
  {true, 1, 120, "INFOREQ", dhcp6c_handle_reply, NULL},
};

/****************************************************************************
 * Private Functions
 ****************************************************************************/

static uint64_t dhcp6c_get_milli_time(void)
{
  struct timespec t;

  clock_gettime(CLOCK_MONOTONIC, &t);

  return t.tv_sec * MSEC_PER_SEC + t.tv_nsec / USEC_PER_SEC;
}

static FAR uint8_t *dhcp6c_resize_state(FAR void *handle,
                                        enum dhcpv6_state_e state,
                                        ssize_t len)
{
  FAR struct dhcp6c_state_s *pdhcp6c = (FAR struct dhcp6c_state_s *)handle;
  FAR uint8_t *n;

  if (len == 0)
    {
      return pdhcp6c->state_data[state] + pdhcp6c->state_len[state];
    }

  n = realloc(pdhcp6c->state_data[state], pdhcp6c->state_len[state] + len);
  if (n != NULL || pdhcp6c->state_len[state] + len == 0)
    {
      pdhcp6c->state_data[state] = n;
      n += pdhcp6c->state_len[state];
      pdhcp6c->state_len[state] += len;
    }

  return n;
}

static void dhcp6c_clear_state(FAR void *handle, enum dhcpv6_state_e state)
{
  FAR struct dhcp6c_state_s *pdhcp6c = (FAR struct dhcp6c_state_s *)handle;

  pdhcp6c->state_len[state] = 0;
}

static void dhcp6c_add_state(FAR void *handle, enum dhcpv6_state_e state,
                             FAR const void *data, size_t len)
{
  FAR uint8_t *n = dhcp6c_resize_state(handle, state, len);

  if (n != NULL)
    {
      memcpy(n, data, len);
    }
}

static size_t dhcp6c_remove_state(FAR void *handle,
                                  enum dhcpv6_state_e state,
                                  size_t offset, size_t len)
{
  FAR struct dhcp6c_state_s *pdhcp6c = (FAR struct dhcp6c_state_s *)handle;
  FAR uint8_t *data = pdhcp6c->state_data[state];
  ssize_t len_after = pdhcp6c->state_len[state] - (offset + len);

  if (len_after < 0)
    {
      return pdhcp6c->state_len[state];
    }

  memmove(data + offset, data + offset + len, len_after);

  return pdhcp6c->state_len[state] -= len;
}

static bool dhcp6c_commit_state(FAR void *handle, enum dhcpv6_state_e state,
                                size_t old_len)
{
  FAR struct dhcp6c_state_s *pdhcp6c = (FAR struct dhcp6c_state_s *)handle;
  size_t new_len = pdhcp6c->state_len[state] - old_len;
  FAR uint8_t *old_data = pdhcp6c->state_data[state];
  FAR uint8_t *new_data = old_data + old_len;
  bool upd = (new_len != old_len) ||
             (memcmp(old_data, new_data, new_len) != 0);

  memmove(old_data, new_data, new_len);
  dhcp6c_resize_state(handle, state, -old_len);

  return upd;
}

static FAR void *dhcp6c_get_state(FAR void *handle,
                                  enum dhcpv6_state_e state,
                                  FAR size_t *len)
{
  FAR struct dhcp6c_state_s *pdhcp6c = (FAR struct dhcp6c_state_s *)handle;

  *len = pdhcp6c->state_len[state];
  return pdhcp6c->state_data[state];
}

static void dhcp6c_get_result(FAR void *handle,
                              FAR struct dhcp6c_state *presult)
{
  FAR struct dhcp6c_state_s *pdhcp6c = (FAR struct dhcp6c_state_s *)handle;
  size_t s_len;
  FAR uint8_t *s_data;
  uint16_t olen;
  uint16_t otype;
  FAR uint8_t *ite;
  FAR uint8_t *odata;
  FAR struct dhcpv6_ia_addr_s *addr;
  FAR struct dhcpv6_ia_prefix_s *pd;
  FAR struct in6_addr *dns;
  char addr_str[INET6_ADDRSTRLEN];

  if (handle == NULL || presult == NULL)
    {
      return;
    }

  s_data = dhcp6c_get_state(handle, STATE_IA_NA, &s_len);
  dhcpv6_for_each_option(ite, s_data, s_data + s_len, otype, olen, odata)
    {
      addr = (FAR void *)(odata - 4);
      memcpy(&presult->addr, &addr->addr, sizeof(presult->addr));
      inet_ntop(AF_INET6, &presult->addr, addr_str, sizeof(addr_str));
      ninfo("IA_NA %s for iface %i\n", addr_str, pdhcp6c->ifindex);
    }

  s_data = dhcp6c_get_state(handle, STATE_IA_PD, &s_len);
  dhcpv6_for_each_option(ite, s_data, s_data + s_len, otype, olen, odata)
    {
      pd = (FAR void *)(odata - 4);
      memcpy(&presult->pd, &pd->addr, sizeof(presult->pd));
      presult->pl = pd->prefix;
      netlib_prefix2ipv6netmask(presult->pl, &presult->netmask);
      inet_ntop(AF_INET6, &presult->pd, addr_str, sizeof(addr_str));
      ninfo("IA_PD %s for iface %i\n", addr_str, pdhcp6c->ifindex);
      inet_ntop(AF_INET6, &presult->netmask, addr_str, sizeof(addr_str));
      ninfo("netmask %s for iface %i\n", addr_str, pdhcp6c->ifindex);
    }

  dns = dhcp6c_get_state(handle, STATE_DNS, &s_len);
  memcpy(&presult->dns, dns, sizeof(presult->dns));
  inet_ntop(AF_INET6, &presult->dns, addr_str, sizeof(addr_str));
  ninfo("DNS server %s for iface %i\n", addr_str, pdhcp6c->ifindex);

  presult->t1 = pdhcp6c->t1;
  presult->t2 = pdhcp6c->t2;
  ninfo("T1:%d T2:%d for iface %i\n", presult->t1, presult->t2,
        pdhcp6c->ifindex);
}

static void dhcp6c_switch_process(FAR void *handle, FAR const char *name)
{
  FAR struct dhcp6c_state_s *pdhcp6c = (FAR struct dhcp6c_state_s *)handle;
  struct dhcp6c_state result;

  ninfo("Process switch to %s\n", name);

  /* Delete lost prefixes and user opts */

  dhcp6c_clear_state(handle, STATE_CUSTOM_OPTS);

  if (pdhcp6c->callback != NULL)
    {
      memset(&result, 0, sizeof(result));
      dhcp6c_get_result(pdhcp6c, &result);
      pdhcp6c->callback(&result);
    }
}

static void dhcp6c_remove_addrs(FAR void *handle)
{
  FAR struct dhcp6c_state_s *pdhcp6c = (FAR struct dhcp6c_state_s *)handle;
  size_t ia_na_len;
  FAR uint8_t *ite;
  FAR uint8_t *odata;
  FAR uint8_t *ia_na = dhcp6c_get_state(handle, STATE_IA_NA, &ia_na_len);
  uint16_t otype;
  uint16_t olen;
  FAR struct dhcpv6_ia_addr_s *addr;
  char addr_str[INET6_ADDRSTRLEN];

  dhcpv6_for_each_option(ite, ia_na, ia_na + ia_na_len, otype, olen, odata)
    {
      addr = (FAR void *)(odata - 4);
      inet_ntop(AF_INET6, &addr->addr, addr_str, sizeof(addr_str));
      ninfo("removing address %s/128 for iface %i\n",
            addr_str, pdhcp6c->ifindex);
    }
}

static void dhcp6c_set_iov(FAR struct iovec *piov,
                           FAR void *base, size_t len)
{
  piov->iov_base = base;
  piov->iov_len = len;
}

static void dhcp6c_send(FAR void *handle, enum dhcpv6_msg_e type,
                        uint8_t trid[3], uint32_t ecs)
{
  FAR struct dhcp6c_state_s *pdhcp6c = (FAR struct dhcp6c_state_s *)handle;
  char fqdn_buf[256];
  struct
  {
    uint16_t type;
    uint16_t len;
    uint8_t flags;
    uint8_t data[256];
  } fqdn;

  size_t fqdn_len;
  size_t cl_id_len;
  FAR void *cl_id;
  size_t srv_id_len;
  FAR void *srv_id;
  size_t ia_pd_len;
  FAR void *ia_pd;
  struct dhcpv6_ia_hdr_s hdr_ia_pd;
  struct dhcpv6_ia_prefix_s pref;
  size_t ia_na_len;
  FAR void *ia_na;
  struct dhcpv6_ia_hdr_s hdr_ia_na;
  struct
  {
    uint16_t type;
    uint16_t length;
  } reconf_accept;

  uint16_t oro_refresh;
  size_t oro_len;
  FAR void *oro;
  struct
  {
    uint8_t type;
    uint8_t trid[3];
    uint16_t elapsed_type;
    uint16_t elapsed_len;
    uint16_t elapsed_value;
    uint16_t oro_type;
    uint16_t oro_len;
  } hdr;

  struct iovec iov[11];
  size_t cnt;
  struct sockaddr_in6 dest =
  {
    AF_INET6, htons(DHCPV6_SERVER_PORT),
    0, DHCPV6_ALL_RELAYS, pdhcp6c->ifindex
  };

  struct msghdr msg;

  /* Build FQDN */

  gethostname(fqdn_buf, sizeof(fqdn_buf));
  fqdn_len = 5 + dn_comp(fqdn_buf, fqdn.data, sizeof(fqdn.data), NULL, NULL);
  fqdn.type = htons(DHCPV6_OPT_FQDN);
  fqdn.len = htons(fqdn_len - 4);
  fqdn.flags = 0;

  /* Build Client ID */

  cl_id = dhcp6c_get_state(handle, STATE_CLIENT_ID, &cl_id_len);

  /* Build Server ID */

  srv_id = dhcp6c_get_state(handle, STATE_SERVER_ID, &srv_id_len);

  /* Build IA_PDs */

  ia_pd = dhcp6c_get_state(handle, STATE_IA_PD, &ia_pd_len);
  hdr_ia_pd.type = htons(DHCPV6_OPT_IA_PD);
  hdr_ia_pd.len = htons(sizeof(hdr_ia_pd) - 4 + ia_pd_len);
  hdr_ia_pd.iaid = 1;
  hdr_ia_pd.t1 = 0;
  hdr_ia_pd.t2 = 0;
  pref.type = htons(DHCPV6_OPT_IA_PREFIX);
  pref.len = htons(25);
  pref.prefix = pdhcp6c->request_prefix;

  if (ia_pd_len == 0 && pdhcp6c->request_prefix &&
      (type == DHCPV6_MSG_SOLICIT || type == DHCPV6_MSG_REQUEST))
    {
      ia_pd = &pref;
      ia_pd_len = sizeof(pref);
    }

  /* Build IA_NAs */

  ia_na = dhcp6c_get_state(handle, STATE_IA_NA, &ia_na_len);
  hdr_ia_na.type = htons(DHCPV6_OPT_IA_NA);
  hdr_ia_na.len = htons(sizeof(hdr_ia_na) - 4 + ia_na_len);
  hdr_ia_na.iaid = 1;
  hdr_ia_na.t1 = 0;
  hdr_ia_na.t2 = 0;

  /* Reconfigure Accept */

  reconf_accept.type = htons(DHCPV6_OPT_RECONF_ACCEPT);
  reconf_accept.length = 0;

  /* Request Information Refresh */

  oro_refresh = htons(DHCPV6_OPT_INFO_REFRESH);

  /* Prepare Header */

  oro = dhcp6c_get_state(handle, STATE_ORO, &oro_len);
  hdr.type = type;
  hdr.trid[0] = trid[0];
  hdr.trid[1] = trid[1];
  hdr.trid[2] = trid[2];
  hdr.elapsed_type = htons(DHCPV6_OPT_ELAPSED);
  hdr.elapsed_len =  htons(2);
  hdr.elapsed_value = htons((ecs > 0xffff) ? 0xffff : ecs);
  hdr.oro_type = htons(DHCPV6_OPT_ORO);
  hdr.oro_len = htons(oro_len);

  /* Prepare iov */

  dhcp6c_set_iov(&iov[0], &hdr, sizeof(hdr));
  dhcp6c_set_iov(&iov[1], oro, oro_len);
  dhcp6c_set_iov(&iov[2], &oro_refresh, 0);
  dhcp6c_set_iov(&iov[3], cl_id, cl_id_len);
  dhcp6c_set_iov(&iov[4], srv_id, srv_id_len);
  dhcp6c_set_iov(&iov[5], &reconf_accept, 0);
  dhcp6c_set_iov(&iov[6], &fqdn, fqdn_len);
  dhcp6c_set_iov(&iov[7], &hdr_ia_na, sizeof(hdr_ia_na));
  dhcp6c_set_iov(&iov[8], ia_na, ia_na_len);
  dhcp6c_set_iov(&iov[9], &hdr_ia_pd, sizeof(hdr_ia_pd));
  dhcp6c_set_iov(&iov[10], ia_pd, ia_pd_len);

  cnt = ARRAY_SIZE(iov);
  if (type == DHCPV6_MSG_INFO_REQ)
    {
      cnt = 5;
      iov[2].iov_len = sizeof(oro_refresh);
      hdr.oro_len = htons(oro_len + sizeof(oro_refresh));
    }
  else if (!pdhcp6c->request_prefix)
    {
      cnt = 9;
    }

  /* Disable IAs if not used */

  if (type == DHCPV6_MSG_SOLICIT)
    {
      iov[5].iov_len = sizeof(reconf_accept);
    }
  else if (type != DHCPV6_MSG_REQUEST)
    {
      if (ia_na_len == 0)
        {
          iov[7].iov_len = 0;
        }

      if (ia_pd_len == 0)
        {
          iov[9].iov_len = 0;
        }
    }

  if (pdhcp6c->ia_mode == IA_MODE_NONE)
    {
      iov[7].iov_len = 0;
    }

  msg.msg_name = &dest;
  msg.msg_namelen = sizeof(dest);
  msg.msg_iov = iov;
  msg.msg_iovlen = cnt;
  msg.msg_control = NULL;
  msg.msg_controllen = 0;
  msg.msg_flags = 0;

  sendmsg(pdhcp6c->sockfd, &msg, 0);
}

static int64_t dhcp6c_rand_delay(FAR void *handle, int64_t time)
{
  FAR struct dhcp6c_state_s *pdhcp6c = (FAR struct dhcp6c_state_s *)handle;
  int random;

  read(pdhcp6c->urandom_fd, &random, sizeof(random));
  return (time * (random % 1000)) / 10000;
}

static bool dhcp6c_response_is_valid(FAR void *handle, FAR const void *buf,
                                     ssize_t len,
                                     const uint8_t transaction[3],
                                     enum dhcpv6_msg_e type)
{
  FAR struct dhcp6c_state_s *pdhcp6c = (FAR struct dhcp6c_state_s *)handle;
  FAR const struct dhcpv6_header_s *rep = buf;
  FAR uint8_t *ite;
  FAR uint8_t *end;
  FAR uint8_t *odata;
  uint16_t otype;
  uint16_t olen;
  bool clientid_ok = false;
  bool serverid_ok = false;
  size_t client_id_len;
  size_t server_id_len;
  FAR void *client_id;
  FAR void *server_id;

  if (len < sizeof(*rep) ||
      memcmp(rep->tr_id, transaction, sizeof(rep->tr_id)) != 0)
    {
      return false;
    }

  if (type == DHCPV6_MSG_SOLICIT)
    {
      if (rep->msg_type != DHCPV6_MSG_ADVERT &&
          rep->msg_type != DHCPV6_MSG_REPLY)
        {
          return false;
        }
    }
  else if (type == DHCPV6_MSG_UNKNOWN)
    {
      if (!pdhcp6c->accept_reconfig ||
          rep->msg_type != DHCPV6_MSG_RECONF)
        {
          return false;
        }
    }
  else if (rep->msg_type != DHCPV6_MSG_REPLY)
    {
      return false;
    }

  end = ((FAR uint8_t *)buf) + len;
  client_id = dhcp6c_get_state(handle, STATE_CLIENT_ID, &client_id_len);
  server_id = dhcp6c_get_state(handle, STATE_SERVER_ID, &server_id_len);

  dhcpv6_for_each_option(ite, &rep[1], end, otype, olen, odata)
    {
      if (otype == DHCPV6_OPT_CLIENTID)
        {
          clientid_ok = (olen + 4u == client_id_len) &&
                        (memcmp((odata - 4), client_id, client_id_len) == 0);
        }
      else if (otype == DHCPV6_OPT_SERVERID)
        {
          serverid_ok = (olen + 4u == server_id_len) &&
                        (memcmp((odata - 4), server_id, server_id_len) == 0);
        }
    }

  return clientid_ok && (serverid_ok || server_id_len == 0);
}

static int dhcp6c_command(FAR void *handle, enum dhcpv6_msg_e type)
{
  FAR struct dhcp6c_state_s *pdhcp6c = (FAR struct dhcp6c_state_s *)handle;
  const int buf_length = 1536;
  FAR uint8_t *buf = (FAR uint8_t *)malloc(buf_length);
  uint32_t timeout = CONFIG_NETUTILS_DHCP6C_REQUEST_TIMEOUT < 3 ? 3 :
                     CONFIG_NETUTILS_DHCP6C_REQUEST_TIMEOUT;
  FAR const struct dhcp6c_retx_s *retx = &g_dhcp6c_retx[type];
  uint64_t start;
  uint64_t round_start;
  uint64_t round_end;
  uint64_t elapsed;
  uint8_t trid[3];
  ssize_t len = -1;
  int64_t rto = 0;

  if (buf == NULL)
    {
      return -1;
    }

  if (retx->delay)
    {
      struct timespec ts;
      ts.tv_sec = 0;
      ts.tv_nsec = dhcp6c_rand_delay(handle, 10 * DHCPV6_REQ_DELAY);
      nanosleep(&ts, NULL);
    }

  if (type == DHCPV6_MSG_RELEASE || type == DHCPV6_MSG_DECLINE)
    {
      timeout = 3;
    }
  else if (type == DHCPV6_MSG_UNKNOWN)
    {
      timeout = pdhcp6c->t1;
    }
  else if (type == DHCPV6_MSG_RENEW)
    {
      timeout = pdhcp6c->t2 - pdhcp6c->t1;
    }
  else if (type == DHCPV6_MSG_REBIND)
    {
      timeout = pdhcp6c->t3 - pdhcp6c->t2;
    }

  if (timeout == 0)
    {
      len = -1;
      goto end;
    }

  ninfo("Sending %s (timeout %u s)\n", retx->name, timeout);
  start = dhcp6c_get_milli_time();
  round_start = start;

  /* Generate transaction ID */

  read(pdhcp6c->urandom_fd, trid, sizeof(trid));

  do
    {
      rto = (rto == 0) ? (retx->init_timeo * MSEC_PER_SEC +
              dhcp6c_rand_delay(handle, retx->init_timeo * MSEC_PER_SEC)) :
              (2 * rto + dhcp6c_rand_delay(handle, rto));

      if (rto >= retx->max_timeo * MSEC_PER_SEC)
        {
          rto = retx->max_timeo * MSEC_PER_SEC +
                dhcp6c_rand_delay(handle, retx->max_timeo * MSEC_PER_SEC);
        }

      /* Calculate end for this round and elapsed time */

      round_end = round_start + rto;
      elapsed = round_start - start;

      /* Don't wait too long */

      if (round_end - start > timeout * MSEC_PER_SEC)
        {
          round_end = timeout * MSEC_PER_SEC + start;
        }

      /* Built and send package */

      if (type != DHCPV6_MSG_UNKNOWN)
        {
          dhcp6c_send(handle, type, trid, elapsed / 10);
        }

      /* Receive rounds */

      for (; len < 0 && round_start < round_end;
              round_start = dhcp6c_get_milli_time())
        {
          /* Set timeout for receiving */

          uint64_t t = round_end - round_start;
          struct timeval retime =
          {
            t / MSEC_PER_SEC, (t % MSEC_PER_SEC) * MSEC_PER_SEC
          };

          /* check for dhcp6c_close */

          if (pdhcp6c->cancel)
            {
              len = -1;
              goto end;
            }

          setsockopt(pdhcp6c->sockfd, SOL_SOCKET, SO_RCVTIMEO,
                     &retime, sizeof(retime));

          /* Receive cycle */

          len = recv(pdhcp6c->sockfd, buf, buf_length, 0);
          if (type != DHCPV6_MSG_UNKNOWN)
            {
              ninfo("%s[type:%d] recv len[%d]\n", __func__, type, len);
            }

          if (!dhcp6c_response_is_valid(handle, buf, len, trid, type))
            {
              len = -1;
            }

          if (len > 0)
            {
              FAR uint8_t *opt = &buf[4];
              FAR uint8_t *opt_end = opt + len - 4;

              round_start = dhcp6c_get_milli_time();
              elapsed = round_start - start;
              ninfo("Got a valid reply after %ums\n", (unsigned)elapsed);

              if (retx->handler_reply != NULL)
                {
                  len = retx->handler_reply(handle, type, opt,
                                            opt_end, elapsed / MSEC_PER_SEC);
                }
            }
        }

      if (retx->handler_finish != NULL)
        {
          len = retx->handler_finish(handle, elapsed / MSEC_PER_SEC);
        }
    }
  while (len < 0 && elapsed / MSEC_PER_SEC < timeout);

end:
  free(buf);
  return len;
}

static int dhcp6c_poll_reconfigure(FAR void *handle)
{
  int ret = dhcp6c_command(handle, DHCPV6_MSG_UNKNOWN);
  if (ret != -1)
    {
      ret = dhcp6c_command(handle, ret);
    }

  return ret;
}

/* Collect all advertised servers */

static int dhcp6c_handle_advert(FAR void *handle, enum dhcpv6_msg_e orig,
                                FAR const void *opt, FAR const void *end,
                                uint32_t elapsed)
{
  FAR struct dhcp6c_state_s *pdhcp6c = (FAR struct dhcp6c_state_s *)handle;
  uint16_t olen;
  uint16_t otype;
  FAR uint8_t *ite0;
  FAR uint8_t *odata;
  struct dhcpv6_server_cand_s cand;

  memset(&cand, 0, sizeof(cand));
  dhcpv6_for_each_option(ite0, opt, end, otype, olen, odata)
    {
      if (otype == DHCPV6_OPT_SERVERID && olen <= 130)
        {
          memcpy(cand.duid, odata, olen);
          cand.duid_len = olen;
        }
      else if (otype == DHCPV6_OPT_STATUS && olen >= 2 && !odata[0]
               && odata[1] == DHCPV6_NOADDRSAVAIL)
        {
          if (pdhcp6c->ia_mode == IA_MODE_FORCE)
            {
              return -1;
            }
          else
            {
              cand.has_noaddravail = true;
              cand.preference -= 1000;
            }
        }
      else if (otype == DHCPV6_OPT_STATUS && olen >= 2 && !odata[0]
               && odata[1] == DHCPV6_NOPREFIXAVAIL)
        {
          cand.preference -= 2000;
        }
      else if (otype == DHCPV6_OPT_PREF && olen >= 1 &&
               cand.preference >= 0)
        {
          cand.preference = odata[1];
        }
      else if (otype == DHCPV6_OPT_RECONF_ACCEPT)
        {
          cand.wants_reconfigure = true;
        }
      else if (otype == DHCPV6_OPT_IA_PD && pdhcp6c->request_prefix)
        {
          FAR struct dhcpv6_ia_hdr_s *h = (FAR void *)odata;
          FAR uint8_t *oend = odata + olen;
          FAR uint8_t *ite1;
          FAR uint8_t *d;

          dhcpv6_for_each_option(ite1, &h[1], oend, otype, olen, d)
            {
              if (otype == DHCPV6_OPT_IA_PREFIX)
                {
                  cand.preference += 2000;
                }
              else if (otype == DHCPV6_OPT_STATUS &&
                       olen >= 2 && d[0] == 0 &&
                       d[1] == DHCPV6_NOPREFIXAVAIL)
                {
                  cand.preference -= 2000;
                }
            }
        }
    }

  if (cand.duid_len > 0)
    {
      dhcp6c_add_state(handle, STATE_SERVER_CAND, &cand, sizeof(cand));
    }

  return 0;
}

static int dhcp6c_commit_advert(FAR void *handle, uint32_t elapsed)
{
  FAR struct dhcp6c_state_s *pdhcp6c = (FAR struct dhcp6c_state_s *)handle;
  size_t cand_len;
  FAR struct dhcpv6_server_cand_s *c = NULL;
  FAR struct dhcpv6_server_cand_s *cand = dhcp6c_get_state(handle,
                                                           STATE_SERVER_CAND,
                                                           &cand_len);
  bool retry = false;

  for (size_t i = 0; i < cand_len / sizeof(*c); ++i)
    {
      if (cand[i].has_noaddravail)
        {
          retry = true;
        }

      if (c == NULL || c->preference < cand[i].preference)
        {
          c = &cand[i];
        }
    }

  if (retry && pdhcp6c->ia_mode == IA_MODE_TRY)
    {
      /* We give it a second try without the IA_NA */

      pdhcp6c->ia_mode = IA_MODE_NONE;
      return dhcp6c_command(handle, DHCPV6_MSG_SOLICIT);
    }

  if (c != NULL)
    {
      uint16_t hdr[2] =
      {
        htons(DHCPV6_OPT_SERVERID), htons(c->duid_len)
      };

      dhcp6c_add_state(handle, STATE_SERVER_ID, hdr, sizeof(hdr));
      dhcp6c_add_state(handle, STATE_SERVER_ID, c->duid, c->duid_len);
      pdhcp6c->accept_reconfig = c->wants_reconfigure;
    }

  dhcp6c_clear_state(handle, STATE_SERVER_CAND);

  if (c == NULL)
    {
      return -1;
    }
  else if (pdhcp6c->request_prefix || pdhcp6c->ia_mode != IA_MODE_NONE)
    {
      return DHCPV6_STATEFUL;
    }
  else
    {
      return DHCPV6_STATELESS;
    }
}

static time_t dhcp6c_parse_ia(FAR void *handle, FAR void *opt, FAR void *end)
{
  uint32_t timeout = UINT32_MAX;
  uint16_t otype;
  uint16_t olen;
  uint16_t stype;
  uint16_t slen;
  FAR uint8_t *ite0;
  FAR uint8_t *odata;
  FAR uint8_t *sdata;

  /* Update address IA */

  dhcpv6_for_each_option(ite0, opt, end, otype, olen, odata)
    {
      if (otype == DHCPV6_OPT_IA_PREFIX)
        {
          FAR struct dhcpv6_ia_prefix_s *prefix = (FAR void *)(odata - 4);
          FAR struct dhcpv6_ia_prefix_s *local = NULL;
          uint32_t valid;
          uint32_t pref;
          size_t pd_len;
          FAR uint8_t *pd;
          FAR uint8_t *ite1;

          if (olen + 4u < sizeof(*prefix))
            {
              continue;
            }

          olen = sizeof(*prefix);
          valid = ntohl(prefix->valid);
          pref = ntohl(prefix->preferred);

          if (pref > valid)
            {
              continue;
            }

          /* Search matching IA */

          pd = dhcp6c_get_state(handle, STATE_IA_PD, &pd_len);
          dhcpv6_for_each_option(ite1, pd, pd + pd_len,
                                 stype, slen, sdata)
            {
              if (memcmp(sdata + 8, odata + 8,
                         sizeof(local->addr) + 1) == 0)
                {
                  local = (FAR void *)(sdata - 4);
                }
            }

          if (local != NULL)
            {
              local->preferred = prefix->preferred;
              local->valid = prefix->valid;
            }
          else
            {
              dhcp6c_add_state(handle, STATE_IA_PD, prefix, olen);
            }

          if (timeout > valid)
            {
              timeout = valid;
            }
        }
      else if (otype == DHCPV6_OPT_IA_ADDR)
        {
          FAR struct dhcpv6_ia_addr_s *addr = (FAR void *)(odata - 4);
          FAR struct dhcpv6_ia_addr_s *local = NULL;
          uint32_t pref;
          uint32_t valid;
          size_t na_len;
          FAR uint8_t *na;
          FAR uint8_t *ite1;

          if (olen + 4u < sizeof(*addr))
            {
              continue;
            }

          olen = sizeof(*addr);
          pref = ntohl(addr->preferred);
          valid = ntohl(addr->valid);

          if (pref > valid)
            {
              continue;
            }

          /* Search matching IA */

          na = dhcp6c_get_state(handle, STATE_IA_NA, &na_len);
          dhcpv6_for_each_option(ite1, na, na + na_len,
                                 stype, slen, sdata)
            {
              if (memcmp(sdata, odata, sizeof(local->addr)) == 0)
                {
                  local = (FAR void *)(sdata - 4);
                }
            }

          if (local != NULL)
            {
              local->preferred = addr->preferred;
              local->valid = addr->valid;
            }
          else
            {
              dhcp6c_add_state(handle, STATE_IA_NA, addr, olen);
            }

          if (timeout > valid)
            {
              timeout = valid;
            }
        }
    }

  return timeout;
}

static int dhcp6c_handle_reply(FAR void *handle, enum dhcpv6_msg_e orig,
                               FAR const void *opt, FAR const void *end,
                               uint32_t elapsed)
{
  FAR struct dhcp6c_state_s *pdhcp6c = (FAR struct dhcp6c_state_s *)handle;
  uint16_t otype;
  uint16_t olen;
  FAR uint8_t *ite0;
  FAR uint8_t *odata;
  bool have_update = false;
  size_t ia_na_len;
  size_t ia_pd_len;
  size_t dns_len;
  size_t search_len;
  size_t sntp_ip_len;
  size_t sntp_dns_len;
  size_t sip_ip_len;
  size_t sip_fqdn_len;
  FAR uint8_t *ia_na = dhcp6c_get_state(handle, STATE_IA_NA, &ia_na_len);
  FAR uint8_t *ia_pd = dhcp6c_get_state(handle, STATE_IA_PD, &ia_pd_len);
  FAR uint8_t *ia_end;
  pdhcp6c->t1 = UINT32_MAX;
  pdhcp6c->t2 = UINT32_MAX;
  pdhcp6c->t3 = UINT32_MAX;

  dhcp6c_get_state(handle, STATE_DNS, &dns_len);
  dhcp6c_get_state(handle, STATE_SEARCH, &search_len);
  dhcp6c_get_state(handle, STATE_SNTP_IP, &sntp_ip_len);
  dhcp6c_get_state(handle, STATE_SNTP_FQDN, &sntp_dns_len);
  dhcp6c_get_state(handle, STATE_SIP_IP, &sip_ip_len);
  dhcp6c_get_state(handle, STATE_SIP_FQDN, &sip_fqdn_len);

  /* Decrease valid and preferred lifetime of prefixes */

  dhcpv6_for_each_option(ite0, ia_pd, ia_pd + ia_pd_len, otype, olen, odata)
    {
      FAR struct dhcpv6_ia_prefix_s *p = (FAR void *)(odata - 4);
      uint32_t valid = ntohl(p->valid);
      uint32_t pref = ntohl(p->preferred);

      if (valid != UINT32_MAX)
        {
          p->valid = (valid < elapsed) ? 0 : htonl(valid - elapsed);
        }

      if (pref != UINT32_MAX)
        {
          p->preferred = (pref < elapsed) ? 0 : htonl(pref - elapsed);
        }
    }

  /* Decrease valid and preferred lifetime of addresses */

  dhcpv6_for_each_option(ite0, ia_na, ia_na + ia_na_len, otype, olen, odata)
    {
      FAR struct dhcpv6_ia_addr_s *p = (FAR void *)(odata - 4);
      uint32_t valid = ntohl(p->valid);
      uint32_t pref = ntohl(p->preferred);

      if (valid != UINT32_MAX)
        {
          p->valid = (valid < elapsed) ? 0 : htonl(valid - elapsed);
        }

      if (pref != UINT32_MAX)
        {
          p->preferred = (pref < elapsed) ? 0 : htonl(pref - elapsed);
        }
    }

  /* Parse and find all matching IAs */

  dhcpv6_for_each_option(ite0, opt, end, otype, olen, odata)
    {
      if ((otype == DHCPV6_OPT_IA_PD || otype == DHCPV6_OPT_IA_NA)
           && olen > sizeof(struct dhcpv6_ia_hdr_s))
        {
          FAR struct dhcpv6_ia_hdr_s *ia_hdr = (FAR void *)(odata - 4);
          time_t l_t1 = ntohl(ia_hdr->t1);
          time_t l_t2 = ntohl(ia_hdr->t2);
          uint16_t stype;
          uint16_t slen;
          FAR uint8_t *ite1;
          FAR uint8_t *sdata;
          time_t n;

          /* Test ID and T1-T2 validity */

          if (ia_hdr->iaid != 1 || l_t2 < l_t1)
            {
              continue;
            }

          /* Test status and bail if error */

          dhcpv6_for_each_option(ite1, &ia_hdr[1], odata + olen,
                                 stype, slen, sdata)
            {
              if (stype == DHCPV6_OPT_STATUS && slen >= 2 &&
                  (sdata[0] || sdata[1]))
                {
                  continue;
                }
            }

          /* Update times */

          if (l_t1 > 0 && pdhcp6c->t1 > l_t1)
            {
              pdhcp6c->t1 = l_t1;
            }

          if (l_t2 > 0 && pdhcp6c->t2 > l_t2)
            {
              pdhcp6c->t2 = l_t2;
            }

          /* Always report update in case we have IA_PDs so that
           * the state-script is called with updated times
           */

          if (otype == DHCPV6_OPT_IA_PD && pdhcp6c->request_prefix)
            {
              have_update = true;
            }

          n = dhcp6c_parse_ia(handle, &ia_hdr[1], odata + olen);
          if (n < pdhcp6c->t1)
            {
              pdhcp6c->t1 = n;
            }

          if (n < pdhcp6c->t2)
            {
              pdhcp6c->t2 = n;
            }

          if (n < pdhcp6c->t3)
            {
              pdhcp6c->t3 = n;
            }
        }
      else if (otype == DHCPV6_OPT_DNS_SERVERS)
        {
          if (olen % 16 == 0)
            {
              dhcp6c_add_state(handle, STATE_DNS, odata, olen);
            }
        }
      else if (otype == DHCPV6_OPT_DNS_DOMAIN)
        {
          dhcp6c_add_state(handle, STATE_SEARCH, odata, olen);
        }
      else if (otype == DHCPV6_OPT_NTP_SERVER)
        {
          uint16_t stype;
          uint16_t slen;
          FAR uint8_t *sdata;
          FAR uint8_t *ite1;

          /* Test status and bail if error */

          dhcpv6_for_each_option(ite1, odata, odata + olen,
                                 stype, slen, sdata)
            {
              if (slen == 16 &&
                  (stype == NTP_MC_ADDR || stype == NTP_SRV_ADDR))
                {
                  dhcp6c_add_state(handle, STATE_SNTP_IP, sdata, slen);
                }
              else if (slen > 0 && stype == NTP_SRV_FQDN)
                {
                  dhcp6c_add_state(handle, STATE_SNTP_FQDN, sdata, slen);
                }
            }
        }
      else if (otype == DHCPV6_OPT_SIP_SERVER_A)
        {
          if (olen == 16)
            {
              dhcp6c_add_state(handle, STATE_SIP_IP, odata, olen);
            }
        }
      else if (otype == DHCPV6_OPT_SIP_SERVER_D)
        {
          dhcp6c_add_state(handle, STATE_SIP_FQDN, odata, olen);
        }
      else if (otype == DHCPV6_OPT_INFO_REFRESH && olen >= 4)
        {
          uint32_t refresh = ntohl(*((FAR uint32_t *)odata));
          if (refresh < (uint32_t)pdhcp6c->t1)
            {
              pdhcp6c->t1 = refresh;
            }
        }
      else if (otype != DHCPV6_OPT_CLIENTID && otype != DHCPV6_OPT_SERVERID)
        {
          dhcp6c_add_state(handle, STATE_CUSTOM_OPTS, (odata - 4), olen + 4);
        }
    }

  if (opt != NULL)
    {
      size_t new_ia_pd_len;
      size_t new_ia_na_len;
      have_update |= dhcp6c_commit_state(handle, STATE_DNS, dns_len);
      have_update |= dhcp6c_commit_state(handle, STATE_SEARCH, search_len);
      have_update |= dhcp6c_commit_state(handle, STATE_SNTP_IP,
                                         sntp_ip_len);
      have_update |= dhcp6c_commit_state(handle, STATE_SNTP_FQDN,
                                         sntp_dns_len);
      have_update |= dhcp6c_commit_state(handle, STATE_SIP_IP, sip_ip_len);
      have_update |= dhcp6c_commit_state(handle, STATE_SIP_FQDN,
                                         sip_fqdn_len);
      dhcp6c_get_state(handle, STATE_IA_PD, &new_ia_pd_len);
      dhcp6c_get_state(handle, STATE_IA_NA, &new_ia_na_len);
      have_update |= (new_ia_pd_len != ia_pd_len) ||
                     (new_ia_na_len != ia_na_len);
    }

  /* Delete prefixes with 0 valid-time */

  ia_pd = dhcp6c_get_state(handle, STATE_IA_PD, &ia_pd_len);
  ia_end = ia_pd + ia_pd_len;
  dhcpv6_for_each_option(ite0, ia_pd, ia_end, otype, olen, odata)
    {
      FAR struct dhcpv6_ia_prefix_s *p = (FAR void *)(odata - 4);
      while (!p->valid)
        {
          ia_end = ia_pd + dhcp6c_remove_state(handle, STATE_IA_PD,
                   (FAR uint8_t *)p - ia_pd, olen + 4);
          have_update = true;
        }
    }

  /* Delete addresses with 0 valid-time */

  ia_na = dhcp6c_get_state(handle, STATE_IA_NA, &ia_na_len);
  ia_end = ia_na + ia_na_len;
  dhcpv6_for_each_option(ite0, ia_na, ia_end, otype, olen, odata)
    {
      FAR struct dhcpv6_ia_addr_s *p = (FAR void *)(odata - 4);
      while (!p->valid)
        {
          ia_end = ia_na + dhcp6c_remove_state(handle, STATE_IA_NA,
                   (FAR uint8_t *)p - ia_na, olen + 4);
          have_update = true;
        }
    }

  return have_update;
}

static int dhcp6c_handle_reconfigure(FAR void *handle,
                                     enum dhcpv6_msg_e orig,
                                     FAR const void *opt,
                                     FAR const void *end,
                                     uint32_t elapsed)
{
  FAR struct dhcp6c_state_s *pdhcp6c = (FAR struct dhcp6c_state_s *)handle;
  uint16_t otype;
  uint16_t olen;
  FAR uint8_t *odata;
  FAR uint8_t *ite;
  uint8_t msg = DHCPV6_MSG_RENEW;

  /* TODO: should verify the reconfigure message */

  dhcpv6_for_each_option(ite, opt, end, otype, olen, odata)
    {
      if (otype == DHCPV6_OPT_RECONF_MESSAGE && olen == 1 &&
          (odata[0] == DHCPV6_MSG_RENEW ||
           odata[0] == DHCPV6_MSG_INFO_REQ))
        {
          msg = odata[0];
        }
    }

  pdhcp6c->t1 -= elapsed;
  pdhcp6c->t2 -= elapsed;
  pdhcp6c->t3 -= elapsed;

  if (pdhcp6c->t1 < 0)
    {
      pdhcp6c->t1 = 0;
    }

  if (pdhcp6c->t2 < 0)
    {
      pdhcp6c->t2 = 0;
    }

  if (pdhcp6c->t3 < 0)
    {
      pdhcp6c->t3 = 0;
    }

  dhcp6c_handle_reply(handle, DHCPV6_MSG_UNKNOWN, NULL, NULL, elapsed);

  return msg;
}

static int dhcp6c_handle_rebind_reply(FAR void *handle,
                                      enum dhcpv6_msg_e orig,
                                      FAR const void *opt,
                                      FAR const void *end,
                                      uint32_t elapsed)
{
  dhcp6c_handle_advert(handle, orig, opt, end, elapsed);
  if (dhcp6c_commit_advert(handle, elapsed) < 0)
    {
      return -1;
    }

  return dhcp6c_handle_reply(handle, orig, opt, end, elapsed);
}

static int dhcp6c_single_request(FAR void *args)
{
  FAR struct dhcp6c_state_s *pdhcp6c = (FAR struct dhcp6c_state_s *)args;
  FAR const char *process = NULL;
  enum dhcpv6_mode_e mode;
  enum dhcpv6_msg_e type;
  int ret = -1;

  dhcp6c_clear_state(pdhcp6c, STATE_SERVER_ID);
  dhcp6c_clear_state(pdhcp6c, STATE_SERVER_CAND);
  dhcp6c_clear_state(pdhcp6c, STATE_IA_PD);
  dhcp6c_clear_state(pdhcp6c, STATE_SNTP_IP);
  dhcp6c_clear_state(pdhcp6c, STATE_SNTP_FQDN);
  dhcp6c_clear_state(pdhcp6c, STATE_SIP_IP);
  dhcp6c_clear_state(pdhcp6c, STATE_SIP_FQDN);
  dhcp6c_clear_state(pdhcp6c, STATE_CUSTOM_OPTS);
  ret = dhcp6c_command(pdhcp6c, DHCPV6_MSG_SOLICIT);
  if (ret < 0)
    {
      return -1;
    }
  else if (ret == DHCPV6_STATELESS)
    {
      mode = DHCPV6_STATELESS;
      type = DHCPV6_MSG_INFO_REQ;
      process = "informed";
    }
  else
    {
      mode = DHCPV6_STATEFUL;
      type = DHCPV6_MSG_REQUEST;
      process = "bound";
    }

  ret = dhcp6c_command(pdhcp6c, type);
  if (ret >= 0)
    {
      ret = mode;
      dhcp6c_switch_process(pdhcp6c, process);
    }

  return ret;
}

static int dhcp6c_lease(FAR void *args, uint8_t type)
{
  FAR struct dhcp6c_state_s *pdhcp6c = (FAR struct dhcp6c_state_s *)args;
  enum dhcpv6_mode_e mode = (enum dhcpv6_mode_e)type;
  size_t ia_pd_len;
  size_t ia_na_len;
  size_t ia_pd_new;
  size_t ia_na_new;
  size_t server_id_len;
  int ret = -1;

  if (mode == DHCPV6_STATELESS)
    {
      /* Stateless mode */

      while (!pdhcp6c->cancel)
        {
          /* Wait for T1 to expire or until we get a reconfigure */

          ret = dhcp6c_poll_reconfigure(pdhcp6c);
          if (ret >= 0)
            {
              dhcp6c_switch_process(pdhcp6c, "informed");
            }

          if (pdhcp6c->cancel)
            {
              break;
            }

          /* Information-Request */

          ret = dhcp6c_command(pdhcp6c, DHCPV6_MSG_INFO_REQ);
          if (ret < 0)
            {
              nerr("DHCPV6_MSG_INFO_REQ error\n");
              break;
            }
          else
            {
              dhcp6c_switch_process(pdhcp6c, "informed");
            }
        }
    }
  else
    {
      /* Stateful mode */

      while (!pdhcp6c->cancel)
        {
          /* Renew Cycle
           * Wait for T1 to expire or until we get a reconfigure
           */

          ret = dhcp6c_poll_reconfigure(pdhcp6c);
          if (ret >= 0)
            {
              dhcp6c_switch_process(pdhcp6c, "updated");
            }

          if (pdhcp6c->cancel)
            {
              break;
            }

          dhcp6c_get_state(pdhcp6c, STATE_IA_PD, &ia_pd_len);
          dhcp6c_get_state(pdhcp6c, STATE_IA_NA, &ia_na_len);

          /* If we have any IAs, send renew, otherwise request */

          if (ia_pd_len == 0 && ia_na_len == 0)
            ret = dhcp6c_command(pdhcp6c, DHCPV6_MSG_REQUEST);
          else
            ret = dhcp6c_command(pdhcp6c, DHCPV6_MSG_RENEW);

          if (pdhcp6c->cancel)
            {
              break;
            }

          if (ret >= 0)
            {
              /* Publish updates */

              dhcp6c_switch_process(pdhcp6c, "updated");
            }
          else
            {
              /* Remove binding */

              dhcp6c_clear_state(pdhcp6c, STATE_SERVER_ID);

              /* If we have IAs, try rebind otherwise restart */

              ret = dhcp6c_command(pdhcp6c, DHCPV6_MSG_REBIND);
              dhcp6c_get_state(pdhcp6c, STATE_IA_PD, &ia_pd_new);
              dhcp6c_get_state(pdhcp6c, STATE_IA_NA, &ia_na_new);

              /* We lost all our IAs, restart */

              if (ret < 0 || (ia_pd_new == 0 && ia_pd_len) ||
                  (ia_na_new == 0 && ia_na_len))
                {
                  break;
                }
              else if (ret >= 0)
                {
                  dhcp6c_switch_process(pdhcp6c, "rebound");
                }
            }
        }
    }

  dhcp6c_get_state(pdhcp6c, STATE_IA_PD, &ia_pd_len);
  dhcp6c_get_state(pdhcp6c, STATE_IA_NA, &ia_na_len);
  dhcp6c_get_state(pdhcp6c, STATE_SERVER_ID, &server_id_len);

  /* Add all prefixes to lost prefixes */

  dhcp6c_clear_state(pdhcp6c, STATE_IA_PD);
  dhcp6c_switch_process(pdhcp6c, "unbound");

  /* Remove assigned addresses */

  if (ia_na_len > 0)
    {
      dhcp6c_remove_addrs(pdhcp6c);
    }

  if (server_id_len > 0 && (ia_pd_len > 0 || ia_na_len > 0))
    {
      dhcp6c_command(pdhcp6c, DHCPV6_MSG_RELEASE);
    }

  return ret;
}

static FAR void *dhcp6c_run(FAR void *args)
{
  FAR struct dhcp6c_state_s *pdhcp6c = (FAR struct dhcp6c_state_s *)args;
  int ret;

  while (!pdhcp6c->cancel)
    {
      ret = dhcp6c_single_request(pdhcp6c);
      if (ret > 0)
        {
          dhcp6c_lease(pdhcp6c, ret);
        }
    }

  return 0;
}

static FAR void *dhcp6c_precise_open(FAR const char *ifname,
                                     enum dhcp6c_ia_mode_e ia_mode,
                                     bool request_pd,
                                     uint16_t opt[], int cnt)
{
  FAR struct dhcp6c_state_s *pdhcp6c;
  struct sockaddr_in6 client_addr;
  struct ifreq ifr;
  size_t client_id_len;
  int val = 1;
  uint16_t oro[] =
  {
    htons(DHCPV6_OPT_DNS_SERVERS),
    htons(DHCPV6_OPT_DNS_DOMAIN),
    htons(DHCPV6_OPT_NTP_SERVER),
    htons(DHCPV6_OPT_SIP_SERVER_A),
    htons(DHCPV6_OPT_SIP_SERVER_D)
  };

  pdhcp6c = malloc(sizeof(struct dhcp6c_state_s));
  if (pdhcp6c == NULL)
    {
      return NULL;
    }

  memset(pdhcp6c, 0, sizeof(*pdhcp6c));
  pdhcp6c->urandom_fd = open("/dev/urandom", O_CLOEXEC | O_RDONLY);
  if (pdhcp6c->urandom_fd < 0)
    {
      free(pdhcp6c);
      return NULL;
    }

  pdhcp6c->sockfd = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, IPPROTO_UDP);
  if (pdhcp6c->sockfd < 0)
    {
      close(pdhcp6c->urandom_fd);
      free(pdhcp6c);
      return NULL;
    }

  /* Detect interface */

  strncpy(ifr.ifr_name, ifname, sizeof(ifr.ifr_name));
  if (ioctl(pdhcp6c->sockfd, SIOCGIFINDEX, &ifr))
    {
      close(pdhcp6c->urandom_fd);
      close(pdhcp6c->sockfd);
      free(pdhcp6c);
      return NULL;
    }

  pdhcp6c->ifindex = ifr.ifr_ifindex;

  /* Create client DUID */

  dhcp6c_get_state(pdhcp6c, STATE_CLIENT_ID, &client_id_len);
  if (client_id_len == 0)
    {
      uint8_t duid[14] =
      {
        0, DHCPV6_OPT_CLIENTID, 0, 10, 0,
        DHCPV6_DUID_LLADDR, 0, 1
      };

      uint8_t zero[ETHER_ADDR_LEN];
      struct ifreq ifs[100];
      FAR struct ifreq *ifp;
      FAR struct ifreq *ifend;
      struct ifconf ifc;

      ioctl(pdhcp6c->sockfd, SIOCGIFHWADDR, &ifr);
      memcpy(&duid[8], ifr.ifr_hwaddr.sa_data, ETHER_ADDR_LEN);
      memset(zero, 0, sizeof(zero));
      ifc.ifc_req = ifs;
      ifc.ifc_len = sizeof(ifs);

      if (memcmp(&duid[8], zero, ETHER_ADDR_LEN) == 0 &&
          ioctl(pdhcp6c->sockfd, SIOCGIFCONF, &ifc) >= 0)
        {
          /* If our interface doesn't have an address... */

          ifend = ifs + (ifc.ifc_len / sizeof(struct ifreq));
          for (ifp = ifc.ifc_req; ifp < ifend &&
               memcmp(&duid[8], zero, 6) == 0; ifp++)
            {
              memcpy(ifr.ifr_name, ifp->ifr_name,
                     sizeof(ifr.ifr_name));
              ioctl(pdhcp6c->sockfd, SIOCGIFHWADDR, &ifr);
              memcpy(&duid[8], ifr.ifr_hwaddr.sa_data,
                     ETHER_ADDR_LEN);
            }
        }

      dhcp6c_add_state(pdhcp6c, STATE_CLIENT_ID, duid, sizeof(duid));
    }

  /* Create ORO */

  dhcp6c_add_state(pdhcp6c, STATE_ORO, oro, sizeof(oro));

  /* Configure IPv6-options */

  setsockopt(pdhcp6c->sockfd, IPPROTO_IPV6, IPV6_V6ONLY, &val, sizeof(val));
  setsockopt(pdhcp6c->sockfd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val));
  setsockopt(pdhcp6c->sockfd, SOL_SOCKET, SO_BINDTODEVICE, ifname,
             strlen(ifname));

  memset(&client_addr, 0, sizeof(client_addr));
  client_addr.sin6_family = AF_INET6;
  client_addr.sin6_port = htons(DHCPV6_CLIENT_PORT);

  if (bind(pdhcp6c->sockfd, (struct sockaddr *)&client_addr,
           sizeof(client_addr)) != 0)
    {
      close(pdhcp6c->urandom_fd);
      close(pdhcp6c->sockfd);
      free(pdhcp6c);
      return NULL;
    }

  pdhcp6c->thread = 0;
  pdhcp6c->cancel = false;
  pdhcp6c->t1 = 0;
  pdhcp6c->t2 = 0;
  pdhcp6c->t3 = 0;
  pdhcp6c->request_prefix = request_pd;
  switch (ia_mode)
    {
      case IA_MODE_NONE:
      case IA_MODE_TRY:
      case IA_MODE_FORCE:
        break;
      default:
        ia_mode = IA_MODE_TRY;
        break;
    }

  pdhcp6c->ia_mode = ia_mode;
  pdhcp6c->accept_reconfig = false;
  if (opt != NULL && cnt > 0)
    {
      uint16_t opttype;
      for (int i = 0; i < cnt; i++)
        {
          opttype = htons(opt[i]);
          dhcp6c_add_state(pdhcp6c, STATE_ORO, &opttype, 2);
        }
    }

  return pdhcp6c;
}

/****************************************************************************
 * Public Functions
 ****************************************************************************/

FAR void *dhcp6c_open(FAR const char *interface)
{
  return dhcp6c_precise_open(interface, IA_MODE_TRY, true, NULL, 0);
}

int dhcp6c_request(FAR void *handle, FAR struct dhcp6c_state *presult)
{
  int ret;

  if (handle == NULL)
    {
      return ERROR;
    }

  ret = dhcp6c_single_request(handle);
  if (ret >= 0)
    {
      dhcp6c_get_result(handle, presult);
      return OK;
    }
  else
    {
      return ERROR;
    }
}

int dhcp6c_request_async(FAR void *handle, dhcp6c_callback_t callback)
{
  FAR struct dhcp6c_state_s *pdhcp6c = (FAR struct dhcp6c_state_s *)handle;
  int ret;

  if (handle == NULL)
    {
      return ERROR;
    }

  if (pdhcp6c->thread)
    {
      nerr("DHCP6C thread already running\n");
      return ERROR;
    }

  pdhcp6c->callback = callback;
  ret = pthread_create(&pdhcp6c->thread, NULL, dhcp6c_run, pdhcp6c);
  if (ret != 0)
    {
      nerr("Failed to start the DHCP6C thread\n");
      return ERROR;
    }

  return OK;
}

void dhcp6c_cancel(FAR void *handle)
{
  FAR struct dhcp6c_state_s *pdhcp6c = (FAR struct dhcp6c_state_s *)handle;
  sighandler_t old;
  int ret;

  if (pdhcp6c != NULL)
    {
      pdhcp6c->cancel = true;
      if (pdhcp6c->thread)
        {
          old = signal(SIGQUIT, SIG_IGN);

          /* Signal the dhcp6c_run */

          ret = pthread_kill(pdhcp6c->thread, SIGQUIT);
          if (ret != 0)
            {
              nerr("pthread_kill DHCP6C thread\n");
            }

          /* Wait for the end of dhcp6c_run */

          ret = pthread_join(pdhcp6c->thread, NULL);
          if (ret != 0)
            {
              nerr("pthread_join DHCP6C thread\n");
            }

          pdhcp6c->thread = 0;
          signal(SIGQUIT, old);
        }
    }
}

void dhcp6c_close(FAR void *handle)
{
  FAR struct dhcp6c_state_s *pdhcp6c = (FAR struct dhcp6c_state_s *)handle;

  if (pdhcp6c != NULL)
    {
      dhcp6c_cancel(pdhcp6c);
      if (pdhcp6c->urandom_fd > 0)
        {
          close(pdhcp6c->urandom_fd);
        }

      if (pdhcp6c->sockfd > 0)
        {
          close(pdhcp6c->sockfd);
        }

      for (int i = 0; i < STATE_MAX; i++)
        {
          if (pdhcp6c->state_data[i] != NULL)
            {
              free(pdhcp6c->state_data[i]);
            }
        }

      free(pdhcp6c);
    }
}