#include "net_builtin.h"
#include "profile.h"

#if MG_ENABLE_TCPIP
#define MG_EPHEMERAL_PORT_BASE 32768
#define PDIFF(a, b) ((size_t) (((char *) (b)) - ((char *) (a))))

#ifndef MG_TCPIP_KEEPALIVE_MS
#define MG_TCPIP_KEEPALIVE_MS 45000  // TCP keep-alive period, ms
#endif

#define MG_TCPIP_ACK_MS 150    // Timeout for ACKing
#define MG_TCPIP_ARP_MS 100    // Timeout for ARP response
#define MG_TCPIP_SYN_MS 15000  // Timeout for connection establishment
#define MG_TCPIP_FIN_MS 1000   // Timeout for closing connection

#ifndef MG_TCPIP_WIN
#define MG_TCPIP_WIN 6000  // TCP window size
#endif

struct connstate {
  uint32_t seq, ack;                      // TCP seq/ack counters
  uint64_t timer;                         // TCP timer (see 'ttype' below)
  uint32_t acked;                         // Last ACK-ed number
  size_t unacked;                         // Not acked bytes
  uint32_t maxseq;                        // Max send seq (ack + window)
  uint16_t win;                           // destination current window size
  uint16_t dmss;                          // destination MSS (from TCP opts)
  uint8_t mac[sizeof(struct mg_l2addr)];  // Peer hw address
  uint8_t ttype;                          // Timer type:
#define MIP_TTYPE_KEEPALIVE 0  // Connection is idle for long, send keepalive
#define MIP_TTYPE_ACK 1        // Peer sent us data, we have to ack it soon
#define MIP_TTYPE_ARP 2        // ARP resolve sent, waiting for response
#define MIP_TTYPE_SYN 3        // SYN sent, waiting for response
#define MIP_TTYPE_FIN 4  // FIN sent, waiting until terminating the connection
  uint8_t tmiss;         // Number of keep-alive misses
  bool fin_rcvd;         // We have received FIN from the peer
  bool twclosure;        // 3-way closure done
};

#if defined(__DCC__)
#pragma pack(1)
#else
#pragma pack(push, 1)
#endif

struct ip {
  uint8_t ver;    // Version
  uint8_t tos;    // DS field
  uint16_t len;   // Datagram length
  uint16_t id;    // Unused
  uint16_t frag;  // Fragmentation
#define IP_FRAG_OFFSET_MSK 0x1fff
#define IP_MORE_FRAGS_MSK 0x2000
  uint8_t ttl;    // Time to live
  uint8_t proto;  // Upper level protocol
  uint16_t csum;  // Checksum
  uint32_t src;   // Source IP
  uint32_t dst;   // Destination IP
};

struct ip6 {
  uint8_t ver;       // Version
  uint8_t label[3];  // Flow label
  uint16_t plen;     // Payload length
  uint8_t next;      // Upper level protocol
  uint8_t hops;      // Hop limit
  uint64_t src[2];   // Source IP
  uint64_t dst[2];   // Destination IP
};

struct icmp {
  uint8_t type;
  uint8_t code;
  uint16_t csum;
};

struct icmp6 {
  uint8_t type;
  uint8_t code;
  uint16_t csum;
};

struct ndp_na {
  uint8_t res[4];    // R S O, reserved
  uint64_t addr[2];  // Target address
};

struct ndp_ra {
  uint8_t cur_hop_limit;
  uint8_t flags;  // M,O,Prf,Resvd
  uint16_t router_lifetime;
  uint32_t reachable_time;
  uint32_t retrans_timer;
};

struct arp {
  uint16_t fmt;    // Format of hardware address
  uint16_t pro;    // Format of protocol address
  uint8_t hlen;    // Length of hardware address
  uint8_t plen;    // Length of protocol address
  uint16_t op;     // Operation
  uint8_t sha[6];  // Sender hardware address
  uint32_t spa;    // Sender protocol address
  uint8_t tha[6];  // Target hardware address
  uint32_t tpa;    // Target protocol address
};

struct tcp {
  uint16_t sport;  // Source port
  uint16_t dport;  // Destination port
  uint32_t seq;    // Sequence number
  uint32_t ack;    // Acknowledgement number
  uint8_t off;     // Data offset
  uint8_t flags;   // TCP flags
#define TH_FIN 0x01
#define TH_SYN 0x02
#define TH_RST 0x04
#define TH_PUSH 0x08
#define TH_ACK 0x10
#define TH_URG 0x20
#define TH_STDFLAGS 0x3f
  // #define TH_ECE 0x40 // not part of TCP but RFC-3168 (ECN)
  // #define TH_CWR 0x80
  uint16_t win;   // Window
  uint16_t csum;  // Checksum
  uint16_t urp;   // Urgent pointer
};

struct udp {
  uint16_t sport;  // Source port
  uint16_t dport;  // Destination port
  uint16_t len;    // UDP length
  uint16_t csum;   // UDP checksum
};

struct dhcp {
  uint8_t op, htype, hlen, hops;
  uint32_t xid;
  uint16_t secs, flags;
  uint32_t ciaddr, yiaddr, siaddr, giaddr;
  uint8_t hwaddr[208];
  uint32_t magic;
  uint8_t options[30 + sizeof(((struct mg_tcpip_if *) 0)->dhcp_name)];
};

struct dhcp6 {
  union {
    uint8_t type;
    uint32_t xid;
  };
  uint8_t options[30 + sizeof(((struct mg_tcpip_if *) 0)->dhcp_name)];
};

struct pseudoip {
  uint32_t src;  // Source IP
  uint32_t dst;  // Destination IP
  uint8_t zero;
  uint8_t proto;  // Upper level protocol
  uint16_t len;   // Datagram length
};

struct pseudoip6 {
  uint64_t src[2];  // Source IP
  uint64_t dst[2];  // Destination IP
  uint32_t plen;    // Payload length
  uint8_t zero[3];
  uint8_t next;  // Upper level protocol
};

#if defined(__DCC__)
#pragma pack(0)
#else
#pragma pack(pop)
#endif

// pkt is 8-bit aligned, pointers to headers hint compilers to generate
// byte-copy code for micros with alignment constraints
struct pkt {
  struct mg_str raw;  // Raw packet data
  struct mg_str pay;  // Payload data
  uint8_t *l2;        // Ethernet, PPP [, etc] frame data
  struct arp *arp;
  struct ip *ip;
  struct ip6 *ip6;
  struct icmp *icmp;
  struct icmp6 *icmp6;
  struct tcp *tcp;
  struct udp *udp;
  struct dhcp *dhcp;
  struct dhcp6 *dhcp6;
};

// L2 API
void mg_l2_init(struct mg_tcpip_if *ifp);
uint8_t *mg_l2_header(struct mg_tcpip_if *ifp, enum mg_l2proto proto,
                      uint8_t *src, uint8_t *dst, uint8_t *frame);
size_t mg_l2_trailer(struct mg_tcpip_if *ifp, size_t len, uint8_t *cur);
bool mg_l2_rx(struct mg_tcpip_if *ifp, enum mg_l2proto *proto,
              struct mg_str *pay, struct mg_str *raw);
uint8_t *mg_l2_getaddr(struct mg_tcpip_if *ifp, uint8_t *frame);
uint8_t *mg_l2_mapip(enum mg_l2type type, enum mg_l2addrtype addrtype,
                     struct mg_addr *ip);
#if MG_ENABLE_IPV6
bool mg_l2_genip6(enum mg_l2type type, uint64_t *ip6, uint8_t prefix_len,
                  uint8_t *addr);
bool mg_l2_ip6get(enum mg_l2type type, uint8_t *addr, uint8_t *opts,
                  uint8_t len);
uint8_t mg_l2_ip6put(enum mg_l2type type, uint8_t *addr, uint8_t *opts);
#endif
bool mg_l2_poll(struct mg_tcpip_if *ifp, bool expired_1000ms);

static void mg_tcpip_call(struct mg_tcpip_if *ifp, int ev, void *ev_data) {
#if 0 && MG_ENABLE_PROFILE
  const char *names[] = {"TCPIP_EV_STATE_CHANGE",  "TCPIP_EV_DHCP_DNS",
                         "TCPIP_EV_DHCP_SNTP",     "TCPIP_EV_ARP",
                         "TCPIP_EV_TIMER_1S",      "TCPIP_EV_WIFI_SCAN_RESULT",
                         "TCPIP_EV_WIFI_SCAN_END", "TCPIP_EV_WIFI_CONNECT_ERR",
                         "TCPIP_EV_DRIVER",        "TCPIP_EV_STATE6_CHANGE",
                         "TCPIP_EV_USER"};
  if (ev != MG_TCPIP_EV_TIMER_1S && ev < (int) (sizeof(names) / sizeof(names[0]))) {
    MG_PROF_ADD(ifp, names[ev]); // TODO(): call MG_PROF_DUMP() MG_PROF_FREE()
  }
#endif
  // Fire protocol handler first, user handler second. See #2559
  if (ifp->pfn != NULL) ifp->pfn(ifp, ev, ev_data);
  if (ifp->fn != NULL) ifp->fn(ifp, ev, ev_data);
}

static void send_syn(struct mg_connection *c);

static void mkpay(struct pkt *pkt, void *p) {
  pkt->pay =
      mg_str_n((char *) p, (size_t) (&pkt->pay.buf[pkt->pay.len] - (char *) p));
}

// NOTE(): DOES NOT handle reentries after odd length, use last
static uint32_t csumup(uint32_t sum, const void *buf, size_t len) {
  size_t i;
  const uint8_t *p = (const uint8_t *) buf;
  for (i = 0; i < len; i++) sum += i & 1 ? p[i] : ((uint32_t) p[i]) << 8;
  return sum;
}

static uint16_t csumfin(uint32_t sum) {
  while (sum >> 16) sum = (sum & 0xffff) + (sum >> 16);
  return mg_htons((uint16_t) ((uint16_t) ~sum & 0xffff));
}

static uint16_t ipcsum(const void *buf, size_t len) {
  uint32_t sum = csumup(0, buf, len);
  return csumfin(sum);
}

static bool ipcsum_ok(const void *d) {
  struct ip *ip = (struct ip *) d;
  return (ipcsum(d, (ip->ver & 0x0F) * 4) == 0);
}

static bool icmpcsum_ok(const void *d, size_t len) {
  return (ipcsum(d, len) == 0);
}

static uint16_t pcsum(void *d, void *p, size_t plen) {
  uint32_t sum;
  struct ip *ip = (struct ip *) d;
#if defined(__DCC__)
  volatile /* Makes PPC & Diab4.3 happy */
#endif
      struct pseudoip pip;
  pip.src = ip->src;
  pip.dst = ip->dst;
  pip.zero = 0;
  pip.proto = ip->proto;
  pip.len = mg_htons((uint16_t) plen);
  sum = csumup(0, &pip, sizeof(pip));  // even length
  sum = csumup(sum, p, plen);          // possibly odd length: last
  return csumfin(sum);
}

static bool udpcsum_ok(void *d, void *u) {
  struct udp *udp = (struct udp *) u;
  if (udp->csum == 0) return true;
  if (udp->csum == 0xFFFF) udp->csum = 0;
  return (pcsum(d, u, (size_t) mg_ntohs(udp->len)) == 0);
}

static bool tcpcsum_ok(void *d, void *t) {
  struct ip *ip = (struct ip *) d;
  return (pcsum(d, t, (size_t) (mg_ntohs(ip->len) - (ip->ver & 0x0F) * 4)) ==
          0);
}

#if MG_ENABLE_IPV6
static uint16_t p6csum(void *d, void *p, size_t plen) {
  uint32_t sum;
  struct ip6 *ip6 = (struct ip6 *) d;
#if defined(__DCC__)
  volatile /* Makes PPC & Diab4.3 happy */
#endif
      struct pseudoip6 pip6;
  pip6.src[0] = ip6->src[0], pip6.src[1] = ip6->src[1];
  pip6.dst[0] = ip6->dst[0], pip6.dst[1] = ip6->dst[1];
  pip6.zero[0] = 0, pip6.zero[1] = 0, pip6.zero[2] = 0;
  pip6.plen = mg_htonl((uint32_t) plen);
  pip6.next = ip6->next;
  sum = csumup(0, &pip6, sizeof(pip6));  // even length
  sum = csumup(sum, p, plen);            // possibly odd length: last
  return csumfin(sum);
}

static bool udp6csum_ok(void *d, void *u) {
  struct udp *udp = (struct udp *) u;
  if (udp->csum == 0) return false;  // mandatory in IPv6
  if (udp->csum == 0xFFFF) udp->csum = 0;
  return (p6csum(d, u, (size_t) mg_ntohs(udp->len)) == 0);
}
static bool tcp6csum_ok(void *d, void *t) {
  struct ip6 *ip6 = (struct ip6 *) d;
  return (p6csum(d, t, (size_t) mg_ntohs(ip6->plen)) == 0);
}
static bool icmp6csum_ok(void *d, void *i) {
  struct ip6 *ip6 = (struct ip6 *) d;
  return (p6csum(d, i, (size_t) mg_ntohs(ip6->plen)) == 0);
}

static void ip6sn(uint64_t *addr, uint64_t *sn_addr) {
  // Build solicited-node multicast address from a given unicast IP
  // RFC-4291 2.7
  uint8_t *sn = (uint8_t *) sn_addr;
  memset(sn_addr, 0, 16);
  sn[0] = 0xff;
  sn[1] = 0x02;
  sn[11] = 0x01;
  sn[12] = 0xff;
  sn[13] = ((uint8_t *) addr)[13];
  sn[14] = ((uint8_t *) addr)[14];
  sn[15] = ((uint8_t *) addr)[15];
}

static const struct mg_addr ip6_allrouters = {
    {{0xFF, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x02}}, 0, 0, true};
static const struct mg_addr ip6_allnodes = {
    {{0xFF, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01}}, 0, 0, true};

#define MG_IP6MATCH(a, b) (a[0] == b[0] && a[1] == b[1])
#endif

static void settmout(struct mg_connection *c, uint8_t type) {
  struct mg_tcpip_if *ifp = c->mgr->ifp;
  struct connstate *s = (struct connstate *) (c + 1);
  unsigned n = type == MIP_TTYPE_ACK   ? MG_TCPIP_ACK_MS
               : type == MIP_TTYPE_ARP ? MG_TCPIP_ARP_MS
               : type == MIP_TTYPE_SYN ? MG_TCPIP_SYN_MS
               : type == MIP_TTYPE_FIN ? MG_TCPIP_FIN_MS
                                       : MG_TCPIP_KEEPALIVE_MS;
  if (s->ttype == MIP_TTYPE_FIN) return;  // skip if 3-way closing
  s->timer = ifp->now + n;
  s->ttype = type;
  MG_VERBOSE(("%lu %d -> %llx", c->id, type, s->timer));
}

static size_t driver_output(struct mg_tcpip_if *ifp, size_t len) {
  size_t n = ifp->driver->tx(ifp->tx.buf, len, ifp);
  if (n == len) ifp->nsent++;
  return n;
}

// RFC826, ARP assumes Ethernet MAC addresses
void mg_tcpip_arp_request(struct mg_tcpip_if *ifp, uint32_t ip, uint8_t *mac) {
  uint8_t *l2p = (uint8_t *) ifp->tx.buf;
  struct arp *arp = (struct arp *) mg_l2_header(
      ifp, MG_TCPIP_L2PROTO_ARP, ifp->mac,
      mg_l2_mapip(ifp->l2type, MG_TCPIP_L2ADDR_BCAST, NULL), l2p);
  memset(arp, 0, sizeof(*arp));
  arp->fmt = mg_htons(1), arp->pro = mg_htons(0x800), arp->hlen = 6,
  arp->plen = 4;
  arp->op = mg_htons(1), arp->tpa = ip, arp->spa = ifp->ip;
  memcpy(arp->sha, ifp->mac, sizeof(arp->sha));
  if (mac != NULL) memcpy(arp->tha, mac, sizeof(arp->tha));
  driver_output(ifp, mg_l2_trailer(ifp, sizeof(*arp), (uint8_t *) (arp + 1)));
}

static void onstatechange(struct mg_tcpip_if *ifp) {
  if (ifp->state == MG_TCPIP_STATE_IP &&
      (ifp->l2type == MG_TCPIP_L2_PPP || ifp->l2type == MG_TCPIP_L2_PPPoE))
    ifp->state = MG_TCPIP_STATE_READY;
  if (ifp->state == MG_TCPIP_STATE_READY) {
    MG_INFO(("READY, IP: %M", mg_print_ip4, &ifp->ip));
    MG_INFO(("       GW: %M", mg_print_ip4, &ifp->gw));
    if (ifp->l2type == MG_TCPIP_L2_ETH ||
        ifp->l2type == MG_TCPIP_L2_PPPoE)  // TODO(): print other l2
      MG_INFO(("      MAC: %M", mg_print_mac, ifp->mac));
    if (ifp->l2type == MG_TCPIP_L2_ETH)
      mg_tcpip_arp_request(ifp, ifp->ip, ifp->mac);  // gratuitous ARP annc
    if (ifp->is_ip_changed) {
      struct mg_connection *c;
      for (c = ifp->mgr->conns; c != NULL; c = c->next) {
        if (!c->is_listening && !c->is_udp) c->is_closing = 1;
      }
      ifp->is_ip_changed = false;
    }
  } else if (ifp->state == MG_TCPIP_STATE_IP) {
    if (ifp->gw != 0 && ifp->l2type == MG_TCPIP_L2_ETH)
      mg_tcpip_arp_request(ifp, ifp->gw, NULL);  // unsolicited GW ARP request
  } else if (ifp->state == MG_TCPIP_STATE_UP) {
    srand((unsigned int) mg_millis());
  } else if (ifp->state == MG_TCPIP_STATE_DOWN) {
    MG_ERROR(("Link down"));
  }
  mg_tcpip_call(ifp, MG_TCPIP_EV_STATE_CHANGE, &ifp->state);
}

static struct ip *tx_ip(struct mg_tcpip_if *ifp, uint8_t *l2_dst, uint8_t proto,
                        uint8_t dscp, uint32_t ip_src, uint32_t ip_dst,
                        size_t plen) {
  // ifp->tx.buf is 8-bit aligned, keep other headers as pointers, see pkt
  uint8_t *l2p = (uint8_t *) ifp->tx.buf;
  struct ip *ip = (struct ip *) mg_l2_header(ifp, MG_TCPIP_L2PROTO_IPV4,
                                             ifp->mac, l2_dst, l2p);
  memset(ip, 0, sizeof(*ip));
  ip->ver = 0x45;  // Version 4, header length 5 words
  ip->tos = (uint8_t) (dscp << 2);
  ip->frag = mg_htons(0x4000);  // Don't fragment
  ip->len = mg_htons((uint16_t) (sizeof(*ip) + plen));
  ip->ttl = 64;
  ip->proto = proto;
  ip->src = ip_src;
  ip->dst = ip_dst;
  ip->csum = ipcsum(ip, sizeof(*ip));
  return ip;
}

#if MG_ENABLE_IPV6
static struct ip6 *tx_ip6(struct mg_tcpip_if *ifp, uint8_t *l2_dst,
                          uint8_t next, uint8_t dscp, uint64_t *ip_src,
                          uint64_t *ip_dst, size_t plen);
#endif

static bool tx_udp(struct mg_tcpip_if *ifp, uint8_t *l2_dst,
                   struct mg_addr *ip_src, struct mg_addr *ip_dst, uint8_t dscp,
                   const void *buf, size_t len) {
  uint8_t *l3p;
  size_t l2_len;
  struct ip *ip = NULL;
  struct udp *udp;
#if MG_ENABLE_IPV6
  struct ip6 *ip6 = NULL;
  if (ip_dst->is_ip6) {
    ip6 = tx_ip6(ifp, l2_dst, 17, dscp, ip_src->addr.ip6, ip_dst->addr.ip6,
                 len + sizeof(struct udp));
    udp = (struct udp *) (ip6 + 1);
    l2_len = sizeof(*ip6) + sizeof(*udp) + len;
    l3p = (uint8_t *) ip6;
  } else
#endif
  {
    ip = tx_ip(ifp, l2_dst, 17, dscp, ip_src->addr.ip4, ip_dst->addr.ip4,
               len + sizeof(struct udp));
    udp = (struct udp *) (ip + 1);
    l2_len = sizeof(*ip) + sizeof(*udp) + len;
    l3p = (uint8_t *) ip;
  }
  udp->sport = ip_src->port;
  udp->dport = ip_dst->port;
  udp->len = mg_htons((uint16_t) (sizeof(*udp) + len));
  udp->csum = 0;
  memmove(udp + 1, buf, len);
#if MG_ENABLE_IPV6
  if (ip_dst->is_ip6) {
    udp->csum = p6csum(ip6, udp, sizeof(*udp) + len);
  } else
#endif
  {
    udp->csum = pcsum(ip, udp, sizeof(*udp) + len);
  }
  l2_len = mg_l2_trailer(ifp, l2_len, l3p + l2_len);
  return (driver_output(ifp, l2_len) == l2_len);
}

static bool tx_udp4(struct mg_tcpip_if *ifp, uint8_t *l2_dst, uint32_t ip_src,
                    uint16_t sport, uint32_t ip_dst, uint16_t dport,
                    const void *buf, size_t len) {
  struct mg_addr ips, ipd;
  memset(&ips, 0, sizeof(ips));
  ips.addr.ip4 = ip_src;
  ips.port = sport;
  memset(&ipd, 0, sizeof(ipd));
  ipd.addr.ip4 = ip_dst;
  ipd.port = dport;
  return tx_udp(ifp, l2_dst, &ips, &ipd, 0, buf, len);
}

static void tx_dhcp(struct mg_tcpip_if *ifp, uint8_t *l2_dst, uint32_t ip_src,
                    uint32_t ip_dst, uint8_t *opts, size_t optslen,
                    bool ciaddr) {
  // https://datatracker.ietf.org/doc/html/rfc2132#section-9.6
  // NOTE(): assumes Ethernet: htype=1 hlen=6, copy 6 bytes
  struct dhcp dhcp = {1, 1, 6, 0, 0, 0, 0, 0, 0, 0, 0, {0}, 0, {0}};
  dhcp.magic = mg_htonl(0x63825363);
  memcpy(&dhcp.hwaddr, ifp->mac, 6);
  memcpy(&dhcp.xid, ifp->mac + 2, sizeof(dhcp.xid));
  memcpy(&dhcp.options, opts, optslen);
  if (ciaddr) dhcp.ciaddr = ip_src;
  tx_udp4(ifp, l2_dst, ip_src, mg_htons(68), ip_dst, mg_htons(67), &dhcp,
          sizeof(dhcp));
}

// RFC-2131 #4.3.6, #4.4.1; RFC-2132 #9.8
static void tx_dhcp_request_sel(struct mg_tcpip_if *ifp, uint32_t ip_req,
                                uint32_t ip_srv) {
  uint8_t extra = (uint8_t) ((ifp->enable_req_dns ? 1 : 0) +
                             (ifp->enable_req_sntp ? 1 : 0));
  size_t len = strlen(ifp->dhcp_name);
  size_t olen = 21 + len + extra + 2 + 1;  // Total length of options
#define OPTS_MAXLEN (21 + sizeof(ifp->dhcp_name) + 2 + 2 + 1)
  uint8_t opts[OPTS_MAXLEN];  // Allocate options (max size possible)
  uint8_t *p = opts;
  assert(olen <= sizeof(opts));
  memset(opts, 0, sizeof(opts));
  *p++ = 53, *p++ = 1, *p++ = 3;                       // Type: DHCP request
  *p++ = 54, *p++ = 4, memcpy(p, &ip_srv, 4), p += 4;  // DHCP server ID
  *p++ = 50, *p++ = 4, memcpy(p, &ip_req, 4), p += 4;  // Requested IP
  *p++ = 12, *p++ = (uint8_t) (len & 255);             // DHCP host
  memcpy(p, ifp->dhcp_name, len), p += len;            // name
  *p++ = 55, *p++ = 2 + extra, *p++ = 1, *p++ = 3;     // GW, MASK
  if (ifp->enable_req_dns) *p++ = 6;                   // DNS
  if (ifp->enable_req_sntp) *p++ = 42;                 // SNTP
  *p++ = 255;                                          // End of options
  // assert((size_t) (p - opts) < olen);
  tx_dhcp(ifp, mg_l2_mapip(ifp->l2type, MG_TCPIP_L2ADDR_BCAST, NULL), 0,
          0xffffffff, opts, olen, 0);
  MG_DEBUG(("DHCP req sent"));
}

// RFC-2131 #4.3.6, #4.4.5 (renewing: unicast, rebinding: bcast)
static void tx_dhcp_request_re(struct mg_tcpip_if *ifp, uint8_t *l2_dst,
                               uint32_t ip_src, uint32_t ip_dst) {
  uint8_t opts[] = {
      53, 1, 3,  // Type: DHCP request
      255        // End of options
  };
  tx_dhcp(ifp, l2_dst, ip_src, ip_dst, opts, sizeof(opts), true);
  MG_DEBUG(("DHCP req sent"));
}

static void tx_dhcp_discover(struct mg_tcpip_if *ifp) {
  uint8_t opts[] = {
      53, 1, 1,     // Type: DHCP discover
      55, 2, 1, 3,  // Parameters: ip, mask
      255           // End of options
  };
  tx_dhcp(ifp, mg_l2_mapip(ifp->l2type, MG_TCPIP_L2ADDR_BCAST, NULL), 0,
          0xffffffff, opts, sizeof(opts), false);
  MG_DEBUG(("DHCP discover sent. Our MAC: %M", mg_print_mac, ifp->mac));
}

static struct mg_connection *getpeer(struct mg_mgr *mgr, struct pkt *pkt,
                                     bool lsn) {
  struct mg_connection *c = NULL;
  for (c = mgr->conns; c != NULL; c = c->next) {
    if (c->is_arplooking && pkt->arp && pkt->arp->spa == c->rem.addr.ip4) break;
#if MG_ENABLE_IPV6
    if (c->is_arplooking && pkt->icmp6 && pkt->icmp6->type == 136) {
      struct ndp_na *na = (struct ndp_na *) (pkt->icmp6 + 1);
      if (MG_IP6MATCH(na->addr, c->rem.addr.ip6)) break;
    }
#endif
    if (c->is_udp && pkt->udp && c->loc.port == pkt->udp->dport &&
        !(c->loc.is_ip6 ^ (pkt->ip6 != NULL)))  // IP or IPv6 to same dest
      break;
    if (!c->is_udp && pkt->tcp && c->loc.port == pkt->tcp->dport &&
        ((lsn && c->is_listening && !(c->loc.is_ip6 ^ (pkt->ip6 != NULL))) ||
         (!lsn && !c->is_listening && c->rem.port == pkt->tcp->sport &&
          ((!c->loc.is_ip6 && c->rem.addr.ip4 == pkt->ip->src)
#if MG_ENABLE_IPV6
           || (c->loc.is_ip6 && MG_IP6MATCH(c->rem.addr.ip6, pkt->ip6->src))
#endif
               ))))  // validate addr for established (not listening) conns
      break;
  }
  return c;
}

static void l2addr_resolved(struct mg_connection *c);
static uint8_t *get_return_l2addr(struct mg_tcpip_if *ifp, struct mg_addr *rem,
                                  bool is_udp, struct pkt *pkt);

// RFC826, ARP assumes Ethernet MAC addresses
static void rx_arp(struct mg_tcpip_if *ifp, struct pkt *pkt) {
  if (pkt->arp->op == mg_htons(1) && pkt->arp->tpa == ifp->ip) {
    // ARP request. Make a response, then send
    // MG_VERBOSE(("ARP req from %M", mg_print_ip4, &pkt->arp->spa));
    uint8_t *l2p = (uint8_t *) ifp->tx.buf;
    struct arp *arp = (struct arp *) mg_l2_header(
        ifp, MG_TCPIP_L2PROTO_ARP, ifp->mac, mg_l2_getaddr(ifp, pkt->l2), l2p);
    *arp = *pkt->arp;
    arp->op = mg_htons(2);
    memcpy(arp->tha, pkt->arp->sha, sizeof(pkt->arp->tha));
    memcpy(arp->sha, ifp->mac, sizeof(pkt->arp->sha));
    arp->tpa = pkt->arp->spa;
    arp->spa = ifp->ip;
    MG_DEBUG(("ARP: tell %M we're %M", mg_print_ip4, &arp->tpa, mg_print_mac,
              ifp->mac));
    driver_output(ifp, mg_l2_trailer(ifp, sizeof(*arp), (uint8_t *) (arp + 1)));
  } else if (pkt->arp->op == mg_htons(2)) {
    if (memcmp(pkt->arp->tha, ifp->mac, sizeof(pkt->arp->tha)) != 0) return;
    // MG_VERBOSE(("ARP resp from %M", mg_print_ip4, &pkt->arp->spa));
    if (pkt->arp->spa == ifp->gw) {
      // Got response for the GW ARP request. Set ifp->gwmac and IP -> READY
      memcpy(ifp->gwmac, pkt->arp->sha, sizeof(ifp->gwmac));
      ifp->gw_ready = true;
      if (ifp->state == MG_TCPIP_STATE_IP) {
        ifp->state = MG_TCPIP_STATE_READY;
        onstatechange(ifp);
      }
    } else {
      struct mg_connection *c = getpeer(ifp->mgr, pkt, false);
      if (c != NULL && c->is_arplooking) {
        struct connstate *s = (struct connstate *) (c + 1);
        memcpy(s->mac, pkt->arp->sha, sizeof(s->mac));
        MG_DEBUG(("%lu ARP resolved %M -> %M", c->id, mg_print_ip4,
                  &c->rem.addr.ip4, mg_print_mac, s->mac));
        c->is_arplooking = 0;
        l2addr_resolved(c);
      }
    }
  }
}

static void rx_icmp(struct mg_tcpip_if *ifp, struct pkt *pkt) {
  size_t plen = pkt->pay.len;
  if (!icmpcsum_ok(pkt->icmp, sizeof(struct icmp) + plen)) return;
  if (pkt->icmp->type == 8 && pkt->ip != NULL && pkt->ip->dst == ifp->ip) {
    size_t l2_max_overhead = ifp->framesize - ifp->l2mtu;
    size_t hlen = sizeof(struct ip) + sizeof(struct icmp);
    size_t room = ifp->tx.len - hlen - l2_max_overhead;
    uint8_t *l2addr;
    struct ip *ip;
    struct icmp *icmp;
    struct mg_addr ips;
    ips.addr.ip4 = pkt->ip->src;
    ips.is_ip6 = false;
    if ((l2addr = get_return_l2addr(ifp, &ips, false, pkt)) == NULL)
      return;  // safety net for lousy networks
    if (plen > room) plen = room;
    ip = tx_ip(ifp, l2addr, 1, 0, ifp->ip, pkt->ip->src, sizeof(*icmp) + plen);
    icmp = (struct icmp *) (ip + 1);
    memset(icmp, 0, sizeof(*icmp));        // Set csum, type, code to 0
    memcpy(icmp + 1, pkt->pay.buf, plen);  // Copy RX payload to TX
    icmp->csum = ipcsum(icmp, sizeof(*icmp) + plen);
    driver_output(
        ifp, mg_l2_trailer(ifp, hlen + plen, (uint8_t *) ip + hlen + plen));
  }
}

static void setdns4(struct mg_tcpip_if *ifp, uint32_t *ip);

static bool dhcp_opt_len_ok(uint8_t len, uint8_t *p, uint8_t *end) {
  return (len >= 4 && (len & 3) == 0 && p + 6 < end);
}

static void rx_dhcp_client(struct mg_tcpip_if *ifp, struct pkt *pkt) {
  uint32_t ip = 0, gw = 0, mask = 0, lease = 0, dns = 0, sntp = 0, owner = 0;
  uint8_t msgtype = 0, state = ifp->state;
  // perform size check first, then access fields
  uint8_t *p = (uint8_t *) pkt->pay.buf,
          *end = (uint8_t *) &pkt->pay.buf[pkt->pay.len];
  // min header length checked at payload calculation, options are optional
  if (memcmp(&pkt->dhcp->xid, ifp->mac + 2, sizeof(pkt->dhcp->xid))) return;
  while (p < end && p[0] != 255) {  // RFC-2132 9
    if (p[0] == 0) { p++; continue; }  // Pad
    if ((size_t) (end - p) < 2 || (size_t) (end - p) < 2U + p[1]) break;
    if (p[0] == 1) {  // Mask, 3.3
      if (p[1] != 4) break;
      memcpy(&mask, p + 2, sizeof(mask));
    } else if (p[0] == 3) {  // GW, 3.5
      if (!dhcp_opt_len_ok(p[1], p, end)) break;
      memcpy(&gw, p + 2, sizeof(gw));
      ip = pkt->dhcp->yiaddr;
    } else if (ifp->enable_req_dns && p[0] == 6) {  // DNS, 3.8
      if (!dhcp_opt_len_ok(p[1], p, end)) break;
      memcpy(&dns, p + 2, sizeof(dns));
    } else if (ifp->enable_req_sntp && p[0] == 42) {  // SNTP, 8.3
      if (!dhcp_opt_len_ok(p[1], p, end)) break;
      memcpy(&sntp, p + 2, sizeof(sntp));
    } else if (p[0] == 51) {  // Lease
      if (p[1] != 4) break;
      memcpy(&lease, p + 2, sizeof(lease));
      lease = mg_ntohl(lease);
    } else if (p[0] == 53) {  // Msg Type
      if (p[1] != 1) break;
      msgtype = p[2];
    } else if (p[0] == 54) {  // Server id 9.7
      if (p[1] != 4) break;
      memcpy(&owner, p + 2, sizeof(sntp));  // This is the lease owner
    }
    p += p[1] + 2;
  }
  // Process message type, RFC-1533 (9.4); RFC-2131 (3.1, 4)
  if (msgtype == 6 && ifp->ip == ip) {  // DHCPNACK, release IP
    ifp->state = MG_TCPIP_STATE_UP, ifp->ip = 0;
  } else if (msgtype == 2 && ifp->state == MG_TCPIP_STATE_UP && ip && gw &&
             lease) {  // DHCPOFFER
    // select a server (2131 4.4.1, 2132 9.7): lease owner takes precedence,
    // otherwise use siaddr (fallback to IP source addr on foul play).
    // This is broadcast, otherwise siaddr would be the destination.
    tx_dhcp_request_sel(ifp, ip,
                        owner               ? owner
                        : pkt->dhcp->siaddr ? pkt->dhcp->siaddr
                                            : pkt->ip->src);
    ifp->state = MG_TCPIP_STATE_REQ;  // REQUESTING state
  } else if (msgtype == 5) {          // DHCPACK
    if (ifp->state == MG_TCPIP_STATE_REQ && ip && gw && lease) {  // got an IP
      uint64_t rand;
      ifp->lease_expire = ifp->now + (uint64_t) lease * 1000;
      MG_INFO(("Lease: %u sec (%lld)", lease, ifp->lease_expire / 1000));
      // assume DHCP server = router until ARP resolves
      memcpy(ifp->gwmac, mg_l2_getaddr(ifp, pkt->l2), sizeof(ifp->gwmac));
      ifp->gw_ready = true;  // NOTE(): actual gw ARP won't retry now
      if (ifp->ip != ip) ifp->is_ip_changed = true;
      ifp->ip = ip, ifp->gw = gw, ifp->mask = mask;
      ifp->state = MG_TCPIP_STATE_IP;  // BOUND state
      mg_random(&rand, sizeof(rand));
      srand((unsigned int) (rand + mg_millis()));
      if (ifp->enable_req_dns && dns != 0) {
        setdns4(ifp, &dns);
        mg_tcpip_call(ifp, MG_TCPIP_EV_DHCP_DNS, &dns);
      }
      if (ifp->enable_req_sntp && sntp != 0)
        mg_tcpip_call(ifp, MG_TCPIP_EV_DHCP_SNTP, &sntp);
    } else if (ifp->state == MG_TCPIP_STATE_READY && ifp->ip == ip) {  // renew
      ifp->lease_expire = ifp->now + (uint64_t) lease * 1000;
      MG_INFO(("Lease: %u sec (%lld)", lease, ifp->lease_expire / 1000));
    }  // TODO(): accept provided T1/T2 and store server IP for renewal (4.4)
  }
  if (ifp->state != state) onstatechange(ifp);
}

// Simple DHCP server that assigns a next IP address: ifp->ip + 1
static void rx_dhcp_server(struct mg_tcpip_if *ifp, struct pkt *pkt) {
  uint8_t *mac;
  uint8_t op = 0, *p = (uint8_t *) pkt->pay.buf,
          *end = (uint8_t *) &pkt->pay.buf[pkt->pay.len];
  // NOTE(): assumes Ethernet: htype=1 hlen=6, copy 6 bytes
  struct dhcp res = {2, 1, 6, 0, 0, 0, 0, 0, 0, 0, 0, {0}, 0, {0}};
  // min header length checked at payload calculation, options are optional
  res.yiaddr = ifp->ip;
  ((uint8_t *) (&res.yiaddr))[3]++;                // Offer our IP + 1
  while (p < end && p[0] != 255) {                 // Parse options
    if (p[0] == 0) { p++; continue; }              // Pad
    if ((size_t) (end - p) < 2 || (size_t) (end - p) < 2U + p[1]) break;
    if (p[0] == 53) {  // Message type
      if (p[1] != 1) break;
      op = p[2];
    }
    p += p[1] + 2;
  }
  if (op == 1 || op == 3) {         // DHCP Discover or DHCP Request
    uint8_t msg = op == 1 ? 2 : 5;  // Message type: DHCP OFFER or DHCP ACK
    uint8_t opts[] = {
        53, 1, 0,                   // Message type
        1,  4, 0,   0,   0,   0,    // Subnet mask
        54, 4, 0,   0,   0,   0,    // Server ID
        12, 3, 'm', 'i', 'p',       // Host name: "mip"
        51, 4, 255, 255, 255, 255,  // Lease time
        255                         // End of options
    };
    opts[2] = msg;
    memcpy(&res.hwaddr, pkt->dhcp->hwaddr, 6);
    memcpy(opts + 5, &ifp->mask, sizeof(ifp->mask));
    memcpy(opts + 11, &ifp->ip, sizeof(ifp->ip));
    memcpy(&res.options, opts, sizeof(opts));
    res.magic = pkt->dhcp->magic;
    res.xid = pkt->dhcp->xid;
    mac = mg_l2_getaddr(ifp, pkt->l2);
    if (ifp->enable_get_gateway) {
      ifp->gw = res.yiaddr;  // set gw IP, best-effort gwmac as DHCP server's
      memcpy(ifp->gwmac, mac, sizeof(ifp->gwmac));
    }
    tx_udp4(ifp, mac, ifp->ip, mg_htons(67), op == 1 ? ~0U : res.yiaddr,
            mg_htons(68), &res, sizeof(res));
  }
}

#if MG_ENABLE_IPV6
static struct ip6 *tx_ip6(struct mg_tcpip_if *ifp, uint8_t *l2_dst,
                          uint8_t next, uint8_t dscp, uint64_t *ip_src,
                          uint64_t *ip_dst, size_t plen) {
  // ifp->tx.buf is 8-bit aligned, keep other headers as pointers, see pkt
  uint8_t *l2p = (uint8_t *) ifp->tx.buf;
  struct ip6 *ip6 = (struct ip6 *) mg_l2_header(ifp, MG_TCPIP_L2PROTO_IPV6,
                                                ifp->mac, l2_dst, l2p);
  memset(ip6, 0, sizeof(*ip6));
  ip6->ver = (uint8_t) (0x60 | (dscp >> 2)); // Version 6, traffic class
  ip6->label[0] = (uint8_t) (dscp << 6);
  ip6->plen = mg_htons((uint16_t) plen);
  ip6->next = next;
  ip6->hops = 255;  // NDP requires max
  ip6->src[0] = *ip_src++;
  ip6->src[1] = *ip_src;
  ip6->dst[0] = *ip_dst++;
  ip6->dst[1] = *ip_dst;
  return ip6;
}

static void tx_icmp6(struct mg_tcpip_if *ifp, uint8_t *l2_dst, uint64_t *ip_src,
                     uint64_t *ip_dst, uint8_t type, uint8_t code,
                     const void *buf, size_t len) {
  struct ip6 *ip6;
  struct icmp6 *icmp6;
  ip6 = tx_ip6(ifp, l2_dst, 58, 0, ip_src, ip_dst, sizeof(*icmp6) + len);
  icmp6 = (struct icmp6 *) (ip6 + 1);
  memset(icmp6, 0, sizeof(*icmp6));  // Set csum to 0
  icmp6->type = type;
  icmp6->code = code;
  memcpy(icmp6 + 1, buf, len);  // Copy payload
  icmp6->csum = 0;              // RFC-4443 2.3, RFC-8200 8.1
  icmp6->csum = p6csum(ip6, icmp6, sizeof(*icmp6) + len);
  driver_output(ifp,
                mg_l2_trailer(ifp, sizeof(*ip6) + sizeof(*icmp6) + len,
                              (uint8_t *) (ip6 + 1) + sizeof(*icmp6) + len));
}

// Neighbor Discovery Protocol, RFC-4861
// Neighbor Advertisement, 4.4
static void tx_ndp_na(struct mg_tcpip_if *ifp, uint8_t *l2_dst,
                      uint64_t *ip_src, uint64_t *ip_dst, bool solicited,
                      uint8_t *l2) {
  uint8_t data[20 + 16];  // NOTE(): optional len upto 2 hw addr
  memset(data, 0, sizeof(data));
  data[0] = solicited ? 0x60 : 0x20;                    // O + S
  memcpy(data + 4, ip_src, 16);                         // Target address
  data[20] = 2;                                         // 4.6.1, target hwaddr
  data[21] = mg_l2_ip6put(ifp->l2type, l2, data + 22);  // option length / 8
  tx_icmp6(ifp, l2_dst, ip_src, ip_dst, 136, 0, data,
           20 + (size_t) (8 * data[21]));
}

static void onstate6change(struct mg_tcpip_if *ifp);

static void rx_ndp_na(struct mg_tcpip_if *ifp, struct pkt *pkt) {
  struct ndp_na *na = (struct ndp_na *) pkt->pay.buf;
  uint8_t *opts = (uint8_t *) (na + 1),
          *endp = opts + pkt->pay.len - sizeof(*na);
  if (pkt->pay.len < (sizeof(*na) + 2)) return;  // first 2 bytes in opts
  if ((na->res[0] & 0x40) == 0) return;          // not "solicited"
  if (*opts++ != 2) return;                      // no target hwaddr, must have
  MG_VERBOSE(("NDP NA resp from %M", mg_print_ip6, (char *) &na->addr));
  if (MG_IP6MATCH(na->addr, ifp->gw6)) {
    // Got response for the GW NS request. Set ifp->gw6mac and IP6 -> READY
    uint8_t len = *opts++;                    // check valid hwaddr and get it
    if ((opts + 8 * len - 2) > endp) return;  // truncated
    if (!mg_l2_ip6get(ifp->l2type, ifp->gw6mac, opts, len)) return;
    ifp->gw6_ready = true;
    if (ifp->state6 == MG_TCPIP_STATE_IP) {
      ifp->state6 = MG_TCPIP_STATE_READY;
      onstate6change(ifp);
    }
  } else {
    struct mg_connection *c = getpeer(ifp->mgr, pkt, false);
    if (c != NULL && c->is_arplooking) {
      struct connstate *s = (struct connstate *) (c + 1);
      uint8_t len = *opts++;                    // check valid hwaddr and get it
      if ((opts + 8 * len - 2) > endp) return;  // truncated
      if (!mg_l2_ip6get(ifp->l2type, s->mac, opts, len)) return;
      MG_DEBUG(("%lu NDP resolved %M -> %M", c->id, mg_print_ip6,
                &c->rem.addr.ip6, mg_print_l2addr, ifp->l2type, s->mac));
      c->is_arplooking = 0;
      l2addr_resolved(c);
    }
  }
}

// Neighbor Solicitation, 4.3
static void rx_ndp_ns(struct mg_tcpip_if *ifp, struct pkt *pkt) {
  struct ndp_na *ns = (struct ndp_na *) pkt->pay.buf;  // struct ndp_ns = ndp_na
  uint64_t target[2];
  if (pkt->pay.len < (sizeof(*ns) + 2)) return;  // first 2 bytes in opts
  memcpy(target, ns->addr, sizeof(target));
  if (MG_IP6MATCH(target, ifp->ip6ll) || MG_IP6MATCH(target, ifp->ip6)) {
    uint64_t req[2];  // requester address
    uint8_t l2[sizeof(struct mg_l2addr)];
    uint8_t len, *opts = (uint8_t *) (ns + 1),
                 *endp = opts + pkt->pay.len - sizeof(*ns);
    if (*opts++ != 1) return;                 // no requester hwaddr (source)
    len = *opts++;                            // check valid hwaddr and get it
    if ((opts + 8 * len - 2) > endp) return;  // truncated
    if (!mg_l2_ip6get(ifp->l2type, l2, opts, len)) return;
    req[0] = pkt->ip6->src[0], req[1] = pkt->ip6->src[1];  // align to 64-bit
    tx_ndp_na(ifp, l2, target, req, true, ifp->mac);
  }
}

// - use solicited node multicast to resolve a l2 address (l2_addr = NULL)
// - use unicast to verify presence (l2_addr = neighbor l2 address)
static void tx_ndp_ns(struct mg_tcpip_if *ifp, uint64_t *ip_dst,
                      uint8_t *l2_addr) {
  uint8_t payload[4 + 16 + 16];  // NOTE(): 16 --> optional len upto 2 hw addr
  uint64_t ip_unspec[2] = {0, 0};
  size_t payload_len = 20;
  bool mcast = (l2_addr == NULL);
  uint64_t ip_mcast[2] = {0, 0};
  uint8_t *l2 = l2_addr;

  if (ifp->l2type == MG_TCPIP_L2_PPP || ifp->l2type == MG_TCPIP_L2_PPPoE) {
    MG_DEBUG(("SKIP NS for %M", mg_print_ip6, ip_dst));
    return;
  }
  memset(payload, 0, sizeof(payload));
  memcpy(payload + 4, ip_dst, 16);
  if (mcast) {
    struct mg_addr ipd;
    ip6sn(ip_dst, ip_mcast);
    ipd.addr.ip6[0] = ip_mcast[0], ipd.addr.ip6[1] = ip_mcast[1],
    ipd.is_ip6 = true;
    l2 = mg_l2_mapip(ifp->l2type, MG_TCPIP_L2ADDR_MCAST6, &ipd);
  }
  payload_len = 20;
  // TODO(robertc2000): using only link-local IP addr for now
  // We might consider to add an option to use either link-local or global IP
  if (!MG_IP6MATCH(ifp->ip6ll, ip_unspec)) {
    payload[20] = 1;  // 4.6.1, source hwaddr; option length in 8-byte units
    payload[21] = mg_l2_ip6put(ifp->l2type, ifp->mac, payload + 22);
    payload_len += 8 * payload[21];
  }
  tx_icmp6(ifp, l2, ifp->ip6ll, mcast ? ip_mcast : ip_dst, 135, 0, payload,
           payload_len);
}

// Router Solicitation, 4.1
static void tx_ndp_rs(struct mg_tcpip_if *ifp) {
  uint8_t payload[4 + 16];  // reserved + optional len upto 2 hw addr NOTE()
  size_t payload_len = 4;
  uint64_t ip_unspec[2] = {0, 0};

  memset(payload, 0, sizeof(payload));

  if (!MG_IP6MATCH(ifp->ip6ll, ip_unspec)) {
    payload[4] = 1;  // 4.6.1, source hwaddr; option length in 8-byte units
    payload[5] = mg_l2_ip6put(ifp->l2type, ifp->mac, payload + 6);
    payload_len += 8 * payload[5];
  }
  tx_icmp6(ifp,
           mg_l2_mapip(ifp->l2type, MG_TCPIP_L2ADDR_MCAST6,
                       (struct mg_addr *) &ip6_allrouters),
           ifp->ip6ll, (uint64_t *) ip6_allrouters.addr.ip6, 133, 0, payload,
           payload_len);
  MG_DEBUG(("NDP Router Solicitation sent"));
}

static void fill_prefix(uint8_t *dst, uint8_t *src, uint8_t len) {
  uint8_t full = len / 8;
  uint8_t rem = len % 8;
  if (full > 0) memcpy(dst, src, full);
  if (rem > 0) {
    uint8_t mask = (uint8_t) (0xFF << (8 - rem));
    dst[full] |= src[full] & mask;  // mg_l2_genip6() zeroes dst
  }
}

static bool match_prefix(uint8_t *newp, uint8_t *curp, uint8_t len) {
  uint8_t full = len / 8;
  uint8_t rem = len % 8;
  if (full > 0 && memcmp(curp, newp, full) != 0) return false;
  if (rem > 0) {
    uint8_t mask = (uint8_t) (0xFF << (8 - rem));
    if (curp[full] != (newp[full] & mask)) return false;
  }
  return true;
}

static bool fill_global(struct mg_tcpip_if *ifp, uint8_t *prefix,
                        uint8_t prefix_len) {
  if (!mg_l2_genip6(ifp->l2type, ifp->ip6, prefix_len, ifp->mac)) return false;
  fill_prefix((uint8_t *) ifp->ip6, prefix, prefix_len);
  fill_prefix(ifp->prefix, prefix, prefix_len);
  ifp->prefix_len = prefix_len;
  return true;
}

// Router Advertisement, 4.2
static void rx_ndp_ra(struct mg_tcpip_if *ifp, struct pkt *pkt) {
  struct ndp_ra *ra = (struct ndp_ra *) pkt->pay.buf;
  uint8_t *opts = (uint8_t *) (ra + 1);
  size_t opt_left = pkt->pay.len - sizeof(*ra);
  bool gotl2addr = false, gotprefix = false, changed = false;
  uint8_t l2[sizeof(struct mg_l2addr)];
  uint32_t mtu = 0;
  uint8_t *prefix = NULL, prefix_len = 0;

  if (pkt->pay.len < sizeof(*ra)) return;
  if (ifp->state6 == MG_TCPIP_STATE_UP) {
    MG_DEBUG(("Received NDP RA"));  // fill gw6 address
    // parse options
    while (opt_left >= 2) {
      uint8_t type = opts[0], len = opts[1];
      size_t length = (size_t) len * 8;
      if (length == 0 || length > opt_left) break;  // malformed
      if (type == 1 && length >= 8) {
        // Received router's L2 address
        if (!mg_l2_ip6get(ifp->l2type, l2, opts + 2, len)) break;
        gotl2addr = true;
      } else if (type == 5 && length >= 8) {
        // process MTU if available, ignore if it smells
        mtu = MG_LOAD_BE32(opts + 4);
        if (mtu < 1280 || mtu > ifp->l2mtu) mtu = 0;  // RFC-8200, minimum MTU
      } else if (type == 3 && length >= 32) {
        // process prefix, 4.6.2, ignore if it smells
        uint8_t pfx_flags = opts[3];  // L=0x80, A=0x40
        uint32_t valid = MG_LOAD_BE32(opts + 4);
        uint32_t pref_lifetime = MG_LOAD_BE32(opts + 8);
        prefix_len = opts[2];
        if (prefix_len >= 128) break;
        prefix = opts + 16;

        // TODO (robertc2000): handle prefix options if necessary
        (void) pfx_flags;
        (void) valid;
        (void) pref_lifetime;

        gotprefix = true;
        if (prefix_len != ifp->prefix_len ||
            !match_prefix(prefix, ifp->prefix, ifp->prefix_len))
          changed = true;
      }
      opts += length;
      opt_left -= length;
    }

    // fill prefix and global
    if (gotprefix && !fill_global(ifp, prefix, prefix_len)) return;
    if (changed) ifp->is_ip6_changed = true;
    ifp->gw6[0] = pkt->ip6->src[0], ifp->gw6[1] = pkt->ip6->src[1];
    if (gotl2addr) memcpy(ifp->gw6mac, l2, sizeof(ifp->gw6mac));
    if (gotl2addr || ifp->l2type == MG_TCPIP_L2_PPP ||
        ifp->l2type == MG_TCPIP_L2_PPPoE) {
      ifp->state6 = MG_TCPIP_STATE_READY;
      ifp->gw6_ready = true;
    }
    if (mtu != 0 && ifp->mtu != mtu) {
      MG_ERROR(
          ("got an MTU: %u, that differs from the configured one. "
           "All devices in an IPv6 network should have the same MTU, "
           "using the router's instead...",
           mtu));
      ifp->mtu = (uint16_t) mtu;
    }
    if (ifp->state6 != MG_TCPIP_STATE_READY) {
      tx_ndp_ns(ifp, ifp->gw6, NULL);  // unsolicited GW hwaddr resolution
      ifp->state6 = MG_TCPIP_STATE_IP;
    }
    onstate6change(ifp);
  }
}

static void rx_icmp6(struct mg_tcpip_if *ifp, struct pkt *pkt) {
  if (!icmp6csum_ok(pkt->ip6, pkt->icmp6)) return;
  switch (pkt->icmp6->type) {
    case 128: {  // Echo Request, RFC-4443 4.1
      uint64_t target[2];
      target[0] = pkt->ip6->dst[0], target[1] = pkt->ip6->dst[1];
      if (MG_IP6MATCH(target, ifp->ip6ll) || MG_IP6MATCH(target, ifp->ip6)) {
        size_t l2_max_overhead = ifp->framesize - ifp->l2mtu;
        size_t hlen = sizeof(struct ip6) + sizeof(struct icmp6);
        size_t room = ifp->tx.len - hlen - l2_max_overhead, plen = pkt->pay.len;
        struct mg_addr ips;
        uint8_t *l2addr;

        ips.addr.ip6[0] = pkt->ip6->src[0], ips.addr.ip6[1] = pkt->ip6->src[1];
        ips.is_ip6 = true;
        if ((l2addr = get_return_l2addr(ifp, &ips, false, pkt)) == NULL)
          return;                      // safety net for lousy networks
        if (plen > room) plen = room;  // Copy (truncated) RX payload to TX
        // Echo Reply, 4.2
        tx_icmp6(ifp, l2addr, target, ips.addr.ip6, 129, 0, pkt->pay.buf, plen);
      }
    } break;
    case 134:  // Router Advertisement
      rx_ndp_ra(ifp, pkt);
      break;
    case 135:  // Neighbor Solicitation
      rx_ndp_ns(ifp, pkt);
      break;
    case 136:  // Neighbor Advertisement
      rx_ndp_na(ifp, pkt);
      break;
    default:
      if (mg_log_level >= MG_LL_VERBOSE)
        mg_hexdump(pkt->icmp6, pkt->pay.len > 16 ? 16 : pkt->pay.len);
      break;
  }
}

static void onstate6change(struct mg_tcpip_if *ifp) {
  if (ifp->state6 == MG_TCPIP_STATE_READY) {
    MG_INFO(("READY, IP: %M", mg_print_ip6, &ifp->ip6));
    MG_INFO(("       GW: %M", mg_print_ip6, &ifp->gw6));
    if (ifp->l2type == MG_TCPIP_L2_ETH)  // TODO(): print other l2
      MG_INFO(("      MAC: %M", mg_print_mac, &ifp->mac));
    if (ifp->l2type == MG_TCPIP_L2_ETH) {  // gratuitous NA annc
      tx_ndp_na(ifp,                       // RFC-4861 7.2.6
                mg_l2_mapip(ifp->l2type, MG_TCPIP_L2ADDR_MCAST6,
                            (struct mg_addr *) &ip6_allnodes),
                ifp->ip6ll, (uint64_t *) ip6_allnodes.addr.ip6, false,
                ifp->mac);
      if (ifp->ip6[0] != 0 && ifp->ip6[1] != 0)
        tx_ndp_na(ifp,  // RFC-9131 4.1
                  mg_l2_mapip(ifp->l2type, MG_TCPIP_L2ADDR_MCAST6,
                              (struct mg_addr *) &ip6_allrouters),
                  ifp->ip6, (uint64_t *) ip6_allrouters.addr.ip6, false,
                  ifp->mac);
    }
    if (ifp->is_ip6_changed) {
      struct mg_connection *c;
      for (c = ifp->mgr->conns; c != NULL; c = c->next) {
        if (!c->is_listening && !c->is_udp) c->is_closing = 1;
      }
      ifp->is_ip6_changed = false;
    }
  } else if (ifp->state6 == MG_TCPIP_STATE_IP) {
    if ((ifp->gw6[0] != 0 || ifp->gw6[1] != 0) &&
        (ifp->l2type != MG_TCPIP_L2_PPP && ifp->l2type != MG_TCPIP_L2_PPPoE))
      tx_ndp_ns(ifp, ifp->gw6, NULL);  // unsolicited GW hwaddr resolution
  } else if (ifp->state6 == MG_TCPIP_STATE_UP) {
    MG_INFO(("IP: %M", mg_print_ip6, &ifp->ip6ll));
  }
  if (ifp->state6 > MG_TCPIP_STATE_UP)
    mg_tcpip_call(ifp, MG_TCPIP_EV_STATE6_CHANGE, &ifp->state6);
}
#endif

static uint8_t *tcpip_mapip(struct mg_tcpip_if *ifp, struct mg_addr *ip) {
#if MG_ENABLE_IPV6
  if (ip->is_ip6) {
    if (MG_IP6MATCH(ip->addr.ip6, ip6_allnodes.addr.ip6))  // local broadcast
      return mg_l2_mapip(ifp->l2type, MG_TCPIP_L2ADDR_MCAST6,
                         (struct mg_addr *) &ip6_allnodes);
    if (*ip->addr.ip == 0xFF)  // multicast
      return mg_l2_mapip(ifp->l2type, MG_TCPIP_L2ADDR_MCAST6, ip);
  } else
#endif
  {  // global/local broadcast
    if (ip->addr.ip4 == 0xffffffff || ip->addr.ip4 == (ifp->ip | ~ifp->mask))
      return mg_l2_mapip(ifp->l2type, MG_TCPIP_L2ADDR_BCAST, NULL);
    if ((*ip->addr.ip & 0xE0) == 0xE0)  // 224 ~ 239 = E0 ~ EF, multicast
      return mg_l2_mapip(ifp->l2type, MG_TCPIP_L2ADDR_MCAST, ip);
  }
  return NULL;
}

static uint8_t *get_return_l2addr(struct mg_tcpip_if *ifp, struct mg_addr *rem,
                                  bool is_udp, struct pkt *pkt) {
  uint8_t *l2addr;
  if (is_udp && (l2addr = tcpip_mapip(ifp, rem)) != NULL)
    return l2addr;  // broadcast or multicast
#if MG_ENABLE_IPV6
  if (rem->is_ip6) {
    if (rem->addr.ip6[0] == ifp->ip6ll[0] ||
        match_prefix((uint8_t *) rem->addr.ip6, ifp->prefix, ifp->prefix_len))
      return mg_l2_getaddr(ifp, pkt->l2);  // same LAN, get from frame
    if (ifp->gw6_ready)                    // use the router
      return ifp->gw6mac;                  // ignore source address in frame
  } else
#endif
  {
    if (ifp->ip != 0 && ((rem->addr.ip4 & ifp->mask) == (ifp->ip & ifp->mask)))
      return mg_l2_getaddr(ifp, pkt->l2);  // same LAN, get from frame
    if (ifp->gw_ready)                     // use the router
      return ifp->gwmac;                   // ignore source address in frame
  }
  MG_ERROR(("%M %s: No way back, can't respond", mg_print_ip_port, rem,
            is_udp ? "UDP" : "TCP"));
  return NULL;
}

static bool rx_udp(struct mg_tcpip_if *ifp, struct pkt *pkt) {
  struct mg_connection *c = getpeer(ifp->mgr, pkt, true);
  struct connstate *s;
  uint8_t *l2addr;
  if (c == NULL) return false;  // No UDP listener on this port
  s = (struct connstate *) (c + 1);
  c->rem.port = pkt->udp->sport;
#if MG_ENABLE_IPV6
  if (c->loc.is_ip6) {  // matching of v4/v6 to dest is done bt getpeer()
    if (!udp6csum_ok(pkt->ip6, pkt->udp)) return false;
    c->rem.addr.ip6[0] = pkt->ip6->src[0],
    c->rem.addr.ip6[1] = pkt->ip6->src[1], c->rem.is_ip6 = true;
  } else
#endif
  {
    if (!udpcsum_ok(pkt->ip, pkt->udp)) return false;
    c->rem.addr.ip4 = pkt->ip->src;
  }
  if ((l2addr = get_return_l2addr(ifp, &c->rem, true, pkt)) == NULL)
    return false;  // safety net for lousy networks
  memcpy(s->mac, l2addr, sizeof(s->mac));
  if (c->recv.len >= MG_MAX_RECV_SIZE) {
    mg_error(c, "max_recv_buf_size reached");
  } else if (c->recv.size - c->recv.len < pkt->pay.len &&
             !mg_iobuf_resize(&c->recv, c->recv.len + pkt->pay.len)) {
    mg_error(c, "oom");
  } else {
    memcpy(&c->recv.buf[c->recv.len], pkt->pay.buf, pkt->pay.len);
    c->recv.len += pkt->pay.len;
    mg_call(c, MG_EV_READ, &pkt->pay.len);
  }
  return true;
}

static size_t tx_tcp(struct mg_tcpip_if *ifp, uint8_t *l2_dst,
                     struct mg_addr *ip_src, struct mg_addr *ip_dst,
                     uint8_t dscp, uint8_t flags, uint32_t seq, uint32_t ack,
                     const void *buf, size_t len) {
  uint8_t *l3p;
  struct ip *ip = NULL;
  struct tcp *tcp;
  uint16_t opts[4 / 2];
  size_t hlen = sizeof(*tcp);
#if MG_ENABLE_IPV6
  struct ip6 *ip6 = NULL;
#endif

  // Handle any options first, here, to determine header size
  if (flags & TH_SYN) {  // Send MSS
    uint16_t mss;
#if MG_ENABLE_IPV6  // RFC-9293 3.7.1; RFC-6691 2
    mss = (uint16_t) (ifp->mtu - 60);
#else
    mss = (uint16_t) (ifp->mtu - 40);
#endif
    opts[0] = mg_htons(0x0204);  // RFC-9293 3.2
    opts[1] = mg_htons(mss);
    hlen += sizeof(opts);  // always whole number of 32-bit words
  }

#if MG_ENABLE_IPV6
  if (ip_dst->is_ip6) {
    ip6 = tx_ip6(ifp, l2_dst, 6, dscp, ip_src->addr.ip6, ip_dst->addr.ip6,
                 hlen + len);
    tcp = (struct tcp *) (ip6 + 1);
    l3p = (uint8_t *) ip6;
  } else
#endif
  {
    ip = tx_ip(ifp, l2_dst, 6, dscp, ip_src->addr.ip4, ip_dst->addr.ip4,
               hlen + len);
    tcp = (struct tcp *) (ip + 1);
    l3p = (uint8_t *) ip;
  }
  memset(tcp, 0, sizeof(*tcp));
  memmove(tcp + 1, opts, hlen - sizeof(*tcp));  // copy opts if any
  if (buf != NULL && len) memmove((uint8_t *) tcp + hlen, buf, len);
  tcp->sport = ip_src->port;
  tcp->dport = ip_dst->port;
  tcp->seq = seq;
  tcp->ack = ack;
  tcp->flags = flags;
  tcp->win = mg_htons(MG_TCPIP_WIN);
  tcp->off = (uint8_t) (hlen / 4 << 4);
#if MG_ENABLE_IPV6
  if (ip_dst->is_ip6) {
    tcp->csum = p6csum(ip6, tcp, hlen + len);
  } else
#endif
  {
    tcp->csum = pcsum(ip, tcp, hlen + len);
  }
  MG_VERBOSE(("TCP %M -> %M fl %x len %u", mg_print_ip_port, ip_src,
              mg_print_ip_port, ip_dst, tcp->flags, len));
  return driver_output(
      ifp, mg_l2_trailer(ifp, PDIFF(l3p, (uint8_t *) tcp + hlen + len),
                         (uint8_t *) tcp + hlen + len));
}

static size_t tx_tcp_ctrlresp(struct mg_tcpip_if *ifp, struct pkt *pkt,
                              uint8_t flags, uint32_t seqno) {
  uint32_t ackno = mg_htonl(mg_ntohl(pkt->tcp->seq) + (uint32_t) pkt->pay.len +
                            ((pkt->tcp->flags & (TH_SYN | TH_FIN)) ? 1 : 0));
  struct mg_addr ips, ipd;
  uint8_t *l2addr;
  memset(&ips, 0, sizeof(ips));
  memset(&ipd, 0, sizeof(ipd));
  if (pkt->ip != NULL) {
    ips.addr.ip4 = pkt->ip->dst;
    ipd.addr.ip4 = pkt->ip->src;
  } else {
    ips.addr.ip6[0] = pkt->ip6->dst[0], ips.addr.ip6[1] = pkt->ip6->dst[1];
    ipd.addr.ip6[0] = pkt->ip6->src[0], ipd.addr.ip6[1] = pkt->ip6->src[1];
    ips.is_ip6 = true;
    ipd.is_ip6 = true;
  }
  ips.port = pkt->tcp->dport;
  ipd.port = pkt->tcp->sport;
  if ((l2addr = get_return_l2addr(ifp, &ipd, false, pkt)) == NULL)
    return 0;  // safety net for lousy networks
  return tx_tcp(ifp, l2addr, &ips, &ipd, 0, flags, seqno, ackno, NULL, 0);
}

static size_t tx_tcp_rst(struct mg_tcpip_if *ifp, struct pkt *pkt, bool toack) {
  return tx_tcp_ctrlresp(ifp, pkt,
                         (uint8_t) (toack ? TH_RST : (TH_RST | TH_ACK)),
                         toack ? pkt->tcp->ack : 0);
}

static struct mg_connection *accept_conn(struct mg_connection *lsn,
                                         struct pkt *pkt, uint16_t mss) {
  struct connstate *s;
  uint8_t *l2addr;
  struct mg_connection *c = mg_alloc_conn(lsn->mgr);
  if (c == NULL) {
    MG_ERROR(("OOM"));
    return NULL;
  }
  s = (struct connstate *) (c + 1);
  s->dmss = mss;  // from options in client SYN
  s->seq = mg_ntohl(pkt->tcp->ack), s->ack = mg_ntohl(pkt->tcp->seq);
  s->win = mg_ntohs(pkt->tcp->win), s->maxseq = (uint32_t) (s->seq + s->win);
#if MG_ENABLE_IPV6
  if (lsn->loc.is_ip6) {
    c->rem.addr.ip6[0] = pkt->ip6->src[0],
    c->rem.addr.ip6[1] = pkt->ip6->src[1], c->rem.is_ip6 = true;
    c->loc.addr.ip6[0] = c->mgr->ifp->ip6[0],
    c->loc.addr.ip6[1] = c->mgr->ifp->ip6[1], c->loc.is_ip6 = true;
    // TODO(): compare lsn to link-local, or rem as link-local: use ll instead
  } else
#endif
  {
    c->rem.addr.ip4 = pkt->ip->src;
    c->loc.addr.ip4 = c->mgr->ifp->ip;
  }
  c->rem.port = pkt->tcp->sport;
  c->loc.port = lsn->loc.port;
  if ((l2addr = get_return_l2addr(lsn->mgr->ifp, &c->rem, false, pkt)) ==
      NULL) {
    free(c);      // safety net for lousy networks, not actually needed
    return NULL;  // as path has already been checked at SYN (sending SYN+ACK)
  }
  memcpy(s->mac, l2addr, sizeof(s->mac));
  settmout(c, MIP_TTYPE_KEEPALIVE);
  MG_DEBUG(("%lu accepted %M", c->id, mg_print_ip_port, &c->rem));
  LIST_ADD_HEAD(struct mg_connection, &lsn->mgr->conns, c);
  c->is_accepted = 1;
  c->is_hexdumping = lsn->is_hexdumping;
  c->pfn = lsn->pfn;
  c->pfn_data = lsn->pfn_data;
  c->fn = lsn->fn;
  c->fn_data = lsn->fn_data;
  c->is_tls = lsn->is_tls;
  mg_call(c, MG_EV_OPEN, NULL);
  mg_call(c, MG_EV_ACCEPT, NULL);
  if (!c->is_tls_hs) c->is_tls = 0;  // user did not call mg_tls_init()
  return c;
}

static size_t trim_len(struct mg_connection *c, size_t len) {
  struct mg_tcpip_if *ifp = c->mgr->ifp;
  size_t l2_max_overhead = ifp->framesize - ifp->l2mtu;
  size_t ip_max_h_len = c->rem.is_ip6 ? 40 : 24;  // we don't send options
  size_t tcp_max_h_len = 60 /* RFC-9293 3.7.1; RFC-6691 2 */, udp_h_len = 8;
  size_t max_headers_len =
      ip_max_h_len + (c->is_udp ? udp_h_len : tcp_max_h_len);
  size_t min_mtu = c->rem.is_ip6 ? 1280 /* RFC-8200, IPv6 minimum */
                   : c->is_udp   ? 68   /* RFC-791, IP minimum */
                                 : max_headers_len /* fit full TCP header */;
  // NOTE(): We are effectively reducing transmitted TCP segment length by 20,
  // accounting for possible options; though we currently don't send options
  // except for SYN.

  // If the frame exceeds the available buffer, trim the length.
  if (len + max_headers_len + l2_max_overhead > ifp->tx.len)
    len = ifp->tx.len - max_headers_len - l2_max_overhead;
  // Ensure the MTU isn't lower than the minimum allowed value
  if (ifp->mtu < min_mtu) {
    MG_ERROR(("MTU is lower than minimum, raising to %lu", min_mtu));
    ifp->mtu = (uint16_t) min_mtu;
  }
  // If the total packet size exceeds the MTU, trim the length
  if (len + max_headers_len > ifp->mtu) {
    len = ifp->mtu - max_headers_len;
    if (c->is_udp) MG_ERROR(("UDP datagram exceeds MTU. Truncating it."));
  }

  return len;
}

static bool udp_send(struct mg_connection *c, const void *buf, size_t len) {
  struct mg_tcpip_if *ifp = c->mgr->ifp;
  struct connstate *s = (struct connstate *) (c + 1);
  struct mg_addr ips;
  memset(&ips, 0, sizeof(ips));
#if MG_ENABLE_IPV6
  if (c->loc.is_ip6) {
    ips.addr.ip6[0] = ifp->ip6[0], ips.addr.ip6[1] = ifp->ip6[1],
    ips.is_ip6 = true;
    // TODO(): detect link-local (c->rem) and use it
  } else
#endif
  {
    ips.addr.ip4 = ifp->ip;
  }
  ips.port = c->loc.port;
  return tx_udp(ifp, s->mac, &ips, &c->rem, c->dscp, buf, len);
}

long mg_io_send(struct mg_connection *c, const void *buf, size_t len) {
  struct connstate *s = (struct connstate *) (c + 1);
  len = trim_len(c, len);
  if (c->is_udp) {
    if (!udp_send(c, buf, len)) return MG_IO_WAIT;
  } else {  // TCP, cap to peer's MSS and check window
    struct mg_tcpip_if *ifp = c->mgr->ifp;
    size_t sent;
    uint32_t room = s->maxseq - s->seq;
    if (room == 0) return MG_IO_WAIT;
    if (len > s->dmss) len = s->dmss;  // RFC-6691: reduce if sending opts
    if ((uint32_t) len > room) len = room;
    sent = tx_tcp(ifp, s->mac, &c->loc, &c->rem, c->dscp, TH_PUSH | TH_ACK,
                  mg_htonl(s->seq), mg_htonl(s->ack), buf, len);
    if (sent == 0) {
      return MG_IO_WAIT;
    } else if (sent == (size_t) -1) {
      return MG_IO_ERR;
    } else {
      s->seq += (uint32_t) len;
      if (s->ttype == MIP_TTYPE_ACK) settmout(c, MIP_TTYPE_KEEPALIVE);
    }
  }
  return (long) len;
}

static void handle_tls_recv(struct mg_connection *c) {
  size_t avail = mg_tls_pending(c);  // will change after mg_tls_recv()
  size_t min = avail > MG_MAX_RECV_SIZE ? MG_MAX_RECV_SIZE : avail;
  struct mg_iobuf *io = &c->recv;  // allocated on first avail > 0
  if (io->size - io->len < min && !mg_iobuf_resize(io, io->len + min)) {
    mg_error(c, "oom");
  } else {
    // Decrypt data directly into c->recv. If io->buf = NULL or
    // io->len = io->size (no room), there can be outstanding data that can't be
    // moved (mg_tls_pending() > 0 after mg_tls_recv() returns). So
    // mg_tls_recv() takes care of returning 0 in this case (commented as "MIP")
    long n = mg_tls_recv(c, io->buf != NULL ? &io->buf[io->len] : io->buf,
                         io->size - io->len);
    if (n == MG_IO_ERR) {
      mg_error(c, "TLS recv error");
    } else if (n > 0) {
      // Decrypted successfully - trigger MG_EV_READ
      io->len += (size_t) n;
      mg_call(c, MG_EV_READ, &n);
    }  // else n < 0: outstanding data to be moved to c->recv
  }
}

static void handle_ack(struct connstate *s, uint32_t ackno, uint16_t win) {
  if (ackno < (s->seq - s->win) || ackno > s->seq) return;
  s->maxseq = (uint32_t) (ackno + win);
  s->win = win;
}

static void read_conn(struct mg_connection *c, struct pkt *pkt) {
  struct connstate *s = (struct connstate *) (c + 1);
  struct mg_iobuf *io = c->is_tls ? &c->rtls : &c->recv;
  uint32_t seq = mg_ntohl(pkt->tcp->seq);
  if (pkt->tcp->flags & TH_FIN) {
    uint8_t flags = TH_ACK;
    if (mg_ntohl(pkt->tcp->seq) != s->ack) {
      MG_VERBOSE(("ignoring FIN, %x != %x", mg_ntohl(pkt->tcp->seq), s->ack));
      tx_tcp(c->mgr->ifp, s->mac, &c->loc, &c->rem, c->dscp, TH_ACK,
             mg_htonl(s->seq), mg_htonl(s->ack), "", 0);
      return;
    }
    // If we initiated the closure, we reply with ACK upon receiving FIN
    // If we didn't initiate it, we reply with FIN as part of the normal TCP
    // closure process
    s->ack = (uint32_t) (mg_htonl(pkt->tcp->seq) + pkt->pay.len + 1);
    s->fin_rcvd = true;
    if (c->is_draining && s->ttype == MIP_TTYPE_FIN) {
      if (s->seq == mg_htonl(pkt->tcp->ack)) {  // Simultaneous closure ?
        s->seq++;                               // Yes. Increment our SEQ
      } else {                                  // Otherwise,
        s->seq = mg_htonl(pkt->tcp->ack);       // Set to peer's ACK
      }
      s->twclosure = true;
    } else {
      // Peer closed first: send ACK only, enter CLOSE_WAIT.
      // The connection loop will call init_closure after pending send data
      // is flushed, then send our FIN.
      c->is_draining = 1;
    }
    tx_tcp(c->mgr->ifp, s->mac, &c->loc, &c->rem, c->dscp, flags,
           mg_htonl(s->seq), mg_htonl(s->ack), "", 0);
    if (pkt->pay.len == 0) return;  // if no data, we're done
  } else if (pkt->pay.len <= 1 && mg_ntohl(pkt->tcp->seq) == s->ack - 1) {
    // Keep-Alive (RFC-9293 3.8.4, allow erroneous implementations)
    MG_VERBOSE(("%lu keepalive ACK", c->id));
    tx_tcp(c->mgr->ifp, s->mac, &c->loc, &c->rem, c->dscp, TH_ACK,
           mg_htonl(s->seq), mg_htonl(s->ack), NULL, 0);
    return;                        // no data to process
  } else if (pkt->pay.len == 0) {  // this is an ACK
    if (pkt->tcp->flags & TH_ACK)
      handle_ack(s, mg_ntohl(pkt->tcp->ack), mg_ntohs(pkt->tcp->win));
    if (s->fin_rcvd && s->ttype == MIP_TTYPE_FIN) s->twclosure = true;
    return;  // no data to process
  } else if (seq != s->ack) {
    uint32_t ack = (uint32_t) (mg_htonl(pkt->tcp->seq) + pkt->pay.len);
    if (s->ack == ack) {
      MG_VERBOSE(("ignoring duplicate pkt"));
    } else {
      MG_VERBOSE(("SEQ != ACK: %x %x %x", seq, s->ack, ack));
      tx_tcp(c->mgr->ifp, s->mac, &c->loc, &c->rem, c->dscp, TH_ACK,
             mg_htonl(s->seq), mg_htonl(s->ack), "", 0);
    }
    return;  // drop it
  } else if (io->size - io->len < pkt->pay.len &&
             !mg_iobuf_resize(io, io->len + pkt->pay.len)) {
    mg_error(c, "oom");
    return;  // drop it
  }
  if (pkt->tcp->flags & TH_ACK)
    handle_ack(s, mg_ntohl(pkt->tcp->ack), mg_ntohs(pkt->tcp->win));
  // Copy TCP payload into the IO buffer. If the connection is plain text,
  // we copy to c->recv. If the connection is TLS, this data is encrypted,
  // therefore we copy that encrypted data to the c->rtls iobuffer instead,
  // and then call mg_tls_recv() to decrypt it. NOTE: mg_tls_recv() will
  // call back mg_io_recv() which grabs raw data from c->rtls
  memcpy(&io->buf[io->len], pkt->pay.buf, pkt->pay.len);
  io->len += pkt->pay.len;
  MG_VERBOSE(("%lu SEQ %x -> %x", c->id, mg_htonl(pkt->tcp->seq), s->ack));
  // Advance ACK counter
  s->ack = (uint32_t) (mg_htonl(pkt->tcp->seq) + pkt->pay.len);
  s->unacked += pkt->pay.len;
  // size_t diff = s->acked <= s->ack ? s->ack - s->acked : s->ack;
  if (s->unacked > MG_TCPIP_WIN / 2 && s->acked != s->ack) {
    // Send ACK immediately
    MG_VERBOSE(("%lu imm ACK %lu", c->id, s->acked));
    tx_tcp(c->mgr->ifp, s->mac, &c->loc, &c->rem, c->dscp, TH_ACK,
           mg_htonl(s->seq), mg_htonl(s->ack), NULL, 0);
    s->unacked = 0;
    s->acked = s->ack;
    if (s->ttype != MIP_TTYPE_KEEPALIVE) settmout(c, MIP_TTYPE_KEEPALIVE);
  } else {
    // if not already running, setup a timer to send an ACK later
    if (s->ttype != MIP_TTYPE_ACK) settmout(c, MIP_TTYPE_ACK);
  }
  if (c->is_tls) {
    c->is_tls_hs ? mg_tls_handshake(c) : handle_tls_recv(c);
  } else {
    // Plain text connection, data is already in c->recv, trigger MG_EV_READ
    mg_call(c, MG_EV_READ, &pkt->pay.len);
  }
}

// TCP backlog
struct mg_backlog {
  uint16_t port, mss;  // use port=0 for available entries
  uint8_t age;
};

static int backlog_insert(struct mg_connection *c, uint16_t port,
                          uint16_t mss) {
  struct mg_backlog *p = (struct mg_backlog *) c->data;
  size_t i;
  for (i = 0; i < sizeof(c->data) / sizeof(*p); i++) {
    if (p[i].port != 0) continue;
    p[i].age = 2;  // remove after two calls, average 1.5 call rate
    p[i].port = port, p[i].mss = mss;
    return (int) i;
  }
  return -1;
}

static struct mg_backlog *backlog_retrieve(struct mg_connection *c,
                                           uint16_t key, uint16_t port) {
  struct mg_backlog *p = (struct mg_backlog *) c->data;
  if (key >= sizeof(c->data) / sizeof(*p)) return NULL;
  if (p[key].port != port) return NULL;
  p += key;
  return p;
}

static void backlog_remove(struct mg_connection *c, uint16_t key) {
  struct mg_backlog *p = (struct mg_backlog *) c->data;
  p[key].port = 0;
}

static void backlog_maintain(struct mg_connection *c) {
  struct mg_backlog *p = (struct mg_backlog *) c->data;
  size_t i;  // dec age and remove those where it reaches 0
  for (i = 0; i < sizeof(c->data) / sizeof(*p); i++) {
    if (p[i].port == 0) continue;
    if (p[i].age != 0) --p[i].age;
    if (p[i].age == 0) p[i].port = 0;
  }
}

static void backlog_poll(struct mg_mgr *mgr) {
  struct mg_connection *c = NULL;
  for (c = mgr->conns; c != NULL; c = c->next) {
    if (!c->is_udp && c->is_listening) backlog_maintain(c);
  }
}

// process options (MSS)
static bool handle_opt(struct connstate *s, struct tcp *tcp, bool ip6) {
  uint8_t *opts = (uint8_t *) (tcp + 1);
  int len = 4 * ((int) (tcp->off >> 4) - ((int) sizeof(*tcp) / 4));
  s->dmss = ip6 ? 1220 : 536;  // assume default, RFC-9293 3.7.1
  while (len > 0) {            // RFC-9293 3.1 3.2
    uint8_t kind = opts[0], optlen = 1;
    if (kind != 1) {         // No-Operation
      if (kind == 0) break;  // End of Option List
      if (len < 2 || opts[1] == 0 || opts[1] > len) return false;  // Malformed
      optlen = opts[1];
      if (kind == 2 && optlen == 4)  // set received MSS
        s->dmss = (uint16_t) (((uint16_t) opts[2] << 8) + opts[3]);
    }
    MG_VERBOSE(("kind: %u, optlen: %u, len: %d\n", kind, optlen, len));
    opts += optlen;
    len -= optlen;
  }
  return true;
}

static void rx_tcp(struct mg_tcpip_if *ifp, struct pkt *pkt) {
  struct mg_connection *c = getpeer(ifp->mgr, pkt, false);
  struct connstate *s = c == NULL ? NULL : (struct connstate *) (c + 1);
#if MG_ENABLE_IPV6  // matching of v4/v6 to dest is done by getpeer()
  if (pkt->ip6 != NULL && !tcp6csum_ok(pkt->ip6, pkt->tcp)) return;
#endif
  if (pkt->ip != NULL && !tcpcsum_ok(pkt->ip, pkt->tcp)) return;
  pkt->tcp->flags &= TH_STDFLAGS;  // tolerate creative usage (ECN, ?)
  // Order is VERY important; RFC-9293 3.5.2
  // - check clients (Group 1) and established connections (Group 3)
  if (c != NULL && c->is_connecting && pkt->tcp->flags == (TH_SYN | TH_ACK)) {
    // client got a server connection accept
    if (!handle_opt(s, pkt->tcp, pkt->ip6 != NULL))
      return;  // process options (MSS)
    s->seq = mg_ntohl(pkt->tcp->ack), s->ack = mg_ntohl(pkt->tcp->seq) + 1;
    s->win = mg_ntohs(pkt->tcp->win), s->maxseq = (uint32_t) (s->seq + s->win);
    tx_tcp_ctrlresp(ifp, pkt, TH_ACK, pkt->tcp->ack);
    c->is_connecting = 0;  // Client connected
    settmout(c, MIP_TTYPE_KEEPALIVE);
    mg_call(c, MG_EV_CONNECT, NULL);  // Let user know
    if (c->is_tls_hs) mg_tls_handshake(c);
    if (!c->is_tls_hs) c->is_tls = 0;  // user did not call mg_tls_init()
  } else if (c != NULL && c->is_connecting && pkt->tcp->flags != TH_ACK) {
    mg_error(c, "connection refused");
  } else if (c != NULL && pkt->tcp->flags & TH_RST) {
    uint32_t seqno = mg_ntohl(pkt->tcp->seq);
    if (seqno >= s->ack && seqno < (s->ack + MG_TCPIP_WIN))  // RFC-9293 3.5.3
      mg_error(c, "peer RST");  // RFC-1122 4.2.2.13
  } else if (c != NULL) {
    // process segment
    s->tmiss = 0;                         // Reset missed keep-alive counter
    if (s->ttype == MIP_TTYPE_KEEPALIVE)  // Advance keep-alive timer
      settmout(c,
               MIP_TTYPE_KEEPALIVE);  // unless a former ACK timeout is pending
    read_conn(c, pkt);  // Override timer with ACK timeout if needed
  } else
    // - we don't listen on that port; RFC-9293 3.5.2 Group 1
    // - check listening connections; RFC-9293 3.5.2 Group 2
    if ((c = getpeer(ifp->mgr, pkt, true)) == NULL) {
      // not listening on that port
      if (!(pkt->tcp->flags & TH_RST)) {
        tx_tcp_rst(ifp, pkt, pkt->tcp->flags & TH_ACK);
      }  // else silently discard
    } else if (pkt->tcp->flags == TH_SYN) {
      // listener receives a connection request
      struct connstate cs;  // At this point, s = NULL, there is no connection
      int key;
      uint32_t isn;
      if (pkt->tcp->sport != 0) {
        if (!handle_opt(&cs, pkt->tcp, pkt->ip6 != NULL))
          return;  // process options (MSS)
        key = backlog_insert(c, pkt->tcp->sport,
                             cs.dmss);  // backlog options (MSS)
        if (key < 0) return;  // no room in backlog, discard SYN, client retries
        // Use peer's src port and bl key as ISN, to later identify the
        // handshake
        isn = (mg_htonl(((uint32_t) key << 16) | mg_ntohs(pkt->tcp->sport)));
        if (tx_tcp_ctrlresp(ifp, pkt, TH_SYN | TH_ACK, isn) == 0)
          backlog_remove(c, (uint16_t) key);  // safety net for lousy networks
      }  // what should we do when port=0 ? Linux takes port 0 as any other
         // port
    } else if (pkt->tcp->flags == TH_ACK) {
      // listener receives an ACK
      struct mg_backlog *b = NULL;
      if ((uint16_t) (mg_htonl(pkt->tcp->ack) - 1) ==
          mg_htons(pkt->tcp->sport)) {
        uint16_t key = (uint16_t) ((mg_htonl(pkt->tcp->ack) - 1) >> 16);
        b = backlog_retrieve(c, key, pkt->tcp->sport);
        if (b != NULL) {                // ACK is a response to a SYN+ACK
          accept_conn(c, pkt, b->mss);  // pass options
          backlog_remove(c, key);
        }  // else not an actual match, reset
      }
      if (b == NULL) tx_tcp_rst(ifp, pkt, true);
    } else if (pkt->tcp->flags & TH_RST) {
      // silently discard
    } else if (pkt->tcp->flags & TH_ACK) {  // ACK + something else != RST
      tx_tcp_rst(ifp, pkt, true);
    } else if (pkt->tcp->flags & TH_SYN) {  // SYN + something else != ACK
      tx_tcp_rst(ifp, pkt, false);
    }  // else  silently discard
}

static void rx_ip(struct mg_tcpip_if *ifp, struct pkt *pkt) {
  uint8_t ihl;
  uint16_t frag, len;
  if (pkt->pay.len < sizeof(*pkt->ip)) return;  // Truncated
  if ((pkt->ip->ver >> 4) != 4) return;         // Not IP
  ihl = pkt->ip->ver & 0x0F;
  if (ihl < 5) return;                              // bad IHL
  if (pkt->pay.len < (uint16_t) (ihl * 4)) return;  // Truncated / malformed
  // There can be link padding, take length from IP header
  len = mg_ntohs(pkt->ip->len);  // IP datagram length
  if (len < (uint16_t) (ihl * 4) || len > pkt->pay.len) return;  // malformed
  pkt->pay.len = len;                      // strip padding
  mkpay(pkt, (uint32_t *) pkt->ip + ihl);  // account for opts
  if (!ipcsum_ok(pkt->ip)) return;
  frag = mg_ntohs(pkt->ip->frag);
  if (frag & IP_MORE_FRAGS_MSK || frag & IP_FRAG_OFFSET_MSK) {
    struct mg_connection *c;
    if (pkt->ip->proto == 17) pkt->udp = (struct udp *) (pkt->pay.buf);
    if (pkt->ip->proto == 6) pkt->tcp = (struct tcp *) (pkt->pay.buf);
    c = getpeer(ifp->mgr, pkt, false);
    if (c) mg_error(c, "Received fragmented packet");
  } else if (pkt->ip->proto == 1) {
    pkt->icmp = (struct icmp *) (pkt->pay.buf);
    if (pkt->pay.len < sizeof(*pkt->icmp)) return;
    mkpay(pkt, pkt->icmp + 1);
    rx_icmp(ifp, pkt);
  } else if (pkt->ip->proto == 17) {
    pkt->udp = (struct udp *) (pkt->pay.buf);
    if (pkt->pay.len < sizeof(*pkt->udp)) return;  // truncated
    // Take length from UDP header
    len = mg_ntohs(pkt->udp->len);  // UDP datagram length
    if (len < sizeof(*pkt->udp) || len > pkt->pay.len) return;  // malformed
    pkt->pay.len = len;  // strip excess data
    mkpay(pkt, pkt->udp + 1);
    MG_VERBOSE(("UDP %M:%hu -> %M:%hu len %u", mg_print_ip4, &pkt->ip->src,
                mg_ntohs(pkt->udp->sport), mg_print_ip4, &pkt->ip->dst,
                mg_ntohs(pkt->udp->dport), (int) pkt->pay.len));
    if (ifp->enable_dhcp_client && pkt->udp->dport == mg_htons(68) &&
        len >= offsetof(struct dhcp, options)) {
      pkt->dhcp = (struct dhcp *) (pkt->udp + 1);
      mkpay(pkt, &pkt->dhcp->options);
      rx_dhcp_client(ifp, pkt);
    } else if (ifp->enable_dhcp_server && pkt->udp->dport == mg_htons(67) &&
               len >= offsetof(struct dhcp, options)) {
      pkt->dhcp = (struct dhcp *) (pkt->udp + 1);
      mkpay(pkt, &pkt->dhcp->options);
      rx_dhcp_server(ifp, pkt);
    } else if (!rx_udp(ifp, pkt)) {
      // Should send ICMP Destination Unreachable for unicasts, but keep
      // silent
    }
  } else if (pkt->ip->proto == 6) {
    uint8_t off;
    pkt->tcp = (struct tcp *) (pkt->pay.buf);
    if (pkt->pay.len < sizeof(*pkt->tcp)) return;
    off = pkt->tcp->off >> 4;  // account for opts
    if (pkt->pay.len < (uint16_t) (4 * off)) return;
    mkpay(pkt, (uint32_t *) pkt->tcp + off);
    MG_VERBOSE(("TCP %M:%hu -> %M:%hu len %u", mg_print_ip4, &pkt->ip->src,
                mg_ntohs(pkt->tcp->sport), mg_print_ip4, &pkt->ip->dst,
                mg_ntohs(pkt->tcp->dport), (int) pkt->pay.len));
    rx_tcp(ifp, pkt);
  } else {
    MG_DEBUG(("Unknown IP proto %x", (int) pkt->ip->proto));
    if (mg_log_level >= MG_LL_VERBOSE)
      mg_hexdump(pkt->ip, pkt->pay.len >= 32 ? 32 : pkt->pay.len);
  }
}

#if MG_ENABLE_IPV6
static void rx_ip6(struct mg_tcpip_if *ifp, struct pkt *pkt) {
  uint16_t len = 0, plen;
  uint8_t next, *nhdr;
  bool loop = true;
  if (pkt->pay.len < sizeof(*pkt->ip6)) return;  // Truncated
  if ((pkt->ip6->ver >> 4) != 0x6) return;       // Not IPv6
  plen = mg_ntohs(pkt->ip6->plen);
  if (plen > (pkt->pay.len - sizeof(*pkt->ip6))) return;  // malformed
  next = pkt->ip6->next;
  nhdr = (uint8_t *) (pkt->ip6 + 1);
  while (loop) {
    uint16_t hlen;
    switch (next) {
      case 0:   // Hop-by-Hop 4.3
      case 43:  // Routing 4.4
      case 60:  // Destination Options 4.6
      case 51:  // Authentication RFC-4302
        MG_INFO(("IPv6 extension header %d", (int) next));
        if (((uint32_t) len + 2) > plen) return;  // nhdr[0, 1]; malformed
        next = nhdr[0];
        hlen = (uint16_t) (8 * (nhdr[1] + 1));
        if (((uint32_t) len + hlen) > plen) return;  // malformed
        len += hlen;
        nhdr += hlen;
        break;
      case 44:  // Fragment 4.5
      {
        struct mg_connection *c;
        if (((uint32_t) len + 2) > plen) return;  // nhdr[0, 1]; malformed
        if (nhdr[0] == 17) pkt->udp = (struct udp *) (pkt->pay.buf);
        if (nhdr[0] == 6) pkt->tcp = (struct tcp *) (pkt->pay.buf);
        c = getpeer(ifp->mgr, pkt, false);
        if (c) mg_error(c, "Received fragmented packet");
      }
        return;
      case 59:  // No Next Header 4.7
        return;
      case 50:  // IPsec ESP RFC-4303, unsupported
      default:
        loop = false;
        break;
    }
  }
  // There can be link padding, take payload length from IPv6 header - options
  pkt->pay.buf = (char *) nhdr;
  pkt->pay.len = plen - len;
  if (next == 58) {
    pkt->icmp6 = (struct icmp6 *) (pkt->pay.buf);
    if (pkt->pay.len < sizeof(*pkt->icmp6)) return;
    mkpay(pkt, pkt->icmp6 + 1);
    MG_DEBUG(("ICMPv6 %M -> %M len %u", mg_print_ip6, &pkt->ip6->src,
              mg_print_ip6, &pkt->ip6->dst, (int) pkt->pay.len));
    rx_icmp6(ifp, pkt);
  } else if (next == 17) {
    pkt->udp = (struct udp *) (pkt->pay.buf);
    if (pkt->pay.len < sizeof(*pkt->udp)) return;
    // Take length from UDP header
    len = mg_ntohs(pkt->udp->len);  // UDP datagram length
    if (len < sizeof(*pkt->udp) || len > pkt->pay.len) return;  // malformed
    pkt->pay.len = len;  // strip excess data
    mkpay(pkt, pkt->udp + 1);
    MG_DEBUG(("UDP %M:%hu -> %M:%hu len %u", mg_print_ip6, &pkt->ip6->src,
              mg_ntohs(pkt->udp->sport), mg_print_ip6, &pkt->ip6->dst,
              mg_ntohs(pkt->udp->dport), (int) pkt->pay.len));
    if (ifp->enable_dhcp6_client && pkt->udp->dport == mg_htons(546)) {
      pkt->dhcp6 = (struct dhcp6 *) (pkt->udp + 1);
      mkpay(pkt, pkt->dhcp6 + 1);
      // rx_dhcp6_client(ifp, pkt);
#if 0
    } else if (ifp->enable_dhcp_server && pkt->udp->dport == mg_htons(547)) {
      pkt->dhcp6 = (struct dhcp6 *) (pkt->udp + 1);
      mkpay(pkt, pkt->dhcp6 + 1);
      rx_dhcp6_server(ifp, pkt);
#endif
    } else if (!rx_udp(ifp, pkt)) {
      // Should send ICMPv6 Destination Unreachable for unicasts, keep silent
    }
  } else if (next == 6) {
    uint8_t off;
    pkt->tcp = (struct tcp *) (pkt->pay.buf);
    if (pkt->pay.len < sizeof(*pkt->tcp)) return;
    off = pkt->tcp->off >> 4;  // account for opts
    if (pkt->pay.len < (uint16_t) (4 * off)) return;
    mkpay(pkt, (uint32_t *) pkt->tcp + off);
    MG_DEBUG(("TCP %M:%hu -> %M:%hu len %u", mg_print_ip6, &pkt->ip6->src,
              mg_ntohs(pkt->tcp->sport), mg_print_ip6, &pkt->ip6->dst,
              mg_ntohs(pkt->tcp->dport), (int) pkt->pay.len));
    rx_tcp(ifp, pkt);
  } else {
    MG_DEBUG(("Unknown IPv6 next hdr %x", (int) next));
    if (mg_log_level >= MG_LL_VERBOSE)
      mg_hexdump(pkt->ip6, pkt->pay.len >= 32 ? 32 : pkt->pay.len);
  }
}
#else
#define rx_ip6(x, y)
#endif

static void mg_tcpip_rx(struct mg_tcpip_if *ifp, void *buf, size_t len) {
  struct pkt pkt;
  enum mg_l2proto proto;
  memset(&pkt, 0, sizeof(pkt));
  pkt.raw.buf = (char *) buf;
  pkt.raw.len = len;
  pkt.l2 = (uint8_t *) pkt.raw.buf;
  if (!mg_l2_rx(ifp, &proto, &pkt.pay, &pkt.raw)) return;
  if (ifp->state < MG_TCPIP_STATE_UP) return;  // discard while L2 is not up
  if (proto == MG_TCPIP_L2PROTO_ARP) {
    pkt.arp = (struct arp *) (pkt.pay.buf);
    if (pkt.pay.len < sizeof(*pkt.arp)) return;  // Truncated
    mg_tcpip_call(ifp, MG_TCPIP_EV_ARP, &pkt.raw);
    rx_arp(ifp, &pkt);
  } else if (proto == MG_TCPIP_L2PROTO_IPV6) {
    pkt.ip6 = (struct ip6 *) (pkt.pay.buf);
    rx_ip6(ifp, &pkt);
  } else if (proto == MG_TCPIP_L2PROTO_IPV4) {
    pkt.ip = (struct ip *) (pkt.pay.buf);
    rx_ip(ifp, &pkt);
  }
}

static void mg_ip_poll(struct mg_tcpip_if *ifp, bool s1) {
  if (ifp->state < MG_TCPIP_STATE_UP) return;
  // DHCP RFC-2131 (4.4)
  if (ifp->enable_dhcp_client && s1) {
    if (ifp->state == MG_TCPIP_STATE_UP) {
      tx_dhcp_discover(ifp);  // INIT (4.4.1)
    } else if (ifp->state == MG_TCPIP_STATE_READY &&
               ifp->lease_expire > 0) {  // BOUND / RENEWING / REBINDING
      if (ifp->now >= ifp->lease_expire) {
        ifp->state = MG_TCPIP_STATE_UP, ifp->ip = 0;  // expired, release IP
        onstatechange(ifp);
      } else if (ifp->now + 30UL * 60UL * 1000UL > ifp->lease_expire &&
                 ((ifp->now / 1000) % 60) == 0) {
        // hack: 30 min before deadline, try to rebind (4.3.6) every min
        tx_dhcp_request_re(
            ifp, mg_l2_mapip(ifp->l2type, MG_TCPIP_L2ADDR_BCAST, NULL), ifp->ip,
            0xffffffff);
      }  // TODO(): Handle T1 (RENEWING) and T2 (REBINDING) (4.4.5)
    }
  }
}
static void mg_ip_link(struct mg_tcpip_if *ifp, bool drv_up, bool l2_up) {
  bool cur_drv = (ifp->state != MG_TCPIP_STATE_DOWN);
  bool cur_l2 = (ifp->state >= MG_TCPIP_STATE_UP);
  if (!l2_up && ifp->enable_dhcp_client) ifp->ip = 0;
  if (drv_up != cur_drv || l2_up != cur_l2) {  // link/L2 state has changed
    ifp->state = !drv_up  ? MG_TCPIP_STATE_DOWN
                 : !l2_up ? MG_TCPIP_STATE_LINK_UP
                 : ifp->enable_dhcp_client || ifp->ip == 0 ? MG_TCPIP_STATE_UP
                                                           : MG_TCPIP_STATE_IP;
    onstatechange(ifp);
  } else if (!ifp->enable_dhcp_client && ifp->state == MG_TCPIP_STATE_UP &&
             ifp->ip) {
    ifp->state = MG_TCPIP_STATE_IP;  // ifp->fn has set an IP
    onstatechange(ifp);
  }
}

#if MG_ENABLE_IPV6
static void mg_ip6_poll(struct mg_tcpip_if *ifp, bool s1) {
  if (ifp->state6 < MG_TCPIP_STATE_UP) return;
  if (ifp->enable_slaac && s1 && ifp->state6 == MG_TCPIP_STATE_UP)
    tx_ndp_rs(ifp);
}
static void mg_ip6_link(struct mg_tcpip_if *ifp, bool drv_up, bool l2_up) {
  bool cur_drv = (ifp->state6 != MG_TCPIP_STATE_DOWN);
  bool cur_l2 = (ifp->state6 >= MG_TCPIP_STATE_UP);
  const uint8_t px[8] = {0xfe, 0x80, 0, 0, 0, 0, 0, 0};  // RFC-4291 2.5.6
  if (drv_up != cur_drv || l2_up != cur_l2) {  // link/L2 state has changed
    if (ifp->l2type == MG_TCPIP_L2_ETH && l2_up && ifp->ip6ll[0] == 0 &&
        ifp->ip6ll[1] == 0) {  // gen ll address
      mg_l2_genip6(ifp->l2type, ifp->ip6ll, 64, ifp->mac);
      memcpy(ifp->ip6ll, px, 8);  // RFC-2464 5
    }  // just got our link local address if we didn't have one.
    // If static configuration is used, global addresses,
    // prefix length, and gw are already filled at this point.
    if (ifp->ip6[0] == 0 && ifp->ip6[1] == 0) ifp->enable_slaac = true;
    if (!l2_up && ifp->enable_slaac) ifp->ip6[0] = ifp->ip6[1] = 0;
    ifp->state6 = !drv_up  ? MG_TCPIP_STATE_DOWN
                  : !l2_up ? MG_TCPIP_STATE_LINK_UP
                  : ifp->ip6ll[0] == 0 || ifp->enable_slaac || ifp->ip6[0] == 0
                      ? MG_TCPIP_STATE_UP
                      : MG_TCPIP_STATE_IP;
    onstate6change(ifp);
  } else if (!ifp->enable_slaac && ifp->state6 == MG_TCPIP_STATE_UP &&
             ifp->ip6ll[0] != 0 && ifp->ip6[0] != 0) {
    ifp->state6 = MG_TCPIP_STATE_IP;  // ifp->fn has set an IP
    onstate6change(ifp);
  }
  if ((ifp->l2type == MG_TCPIP_L2_PPP || ifp->l2type == MG_TCPIP_L2_PPPoE) &&
      ifp->state6 == MG_TCPIP_STATE_UP && ifp->ip6ll[0] == 0 &&
      ifp->ip6ll[1] != 0) {     // IPV6CP has got an IFCID, gen ll address
    memcpy(ifp->ip6ll, px, 8);  // RFC-5072 5
    onstate6change(ifp);
  }  // just got our link local address if we didn't have one.
}
#else
#define mg_ip6_poll(x, y)
#define mg_ip6_link(x, y, z)
#endif

static void mg_tcpip_poll(struct mg_tcpip_if *ifp, uint64_t now) {
  struct mg_connection *c;
  bool expired_1000ms = mg_timer_expired(&ifp->timer_1000ms, 1000, now);
  ifp->now = now;

  if (expired_1000ms) {
#if MG_ENABLE_TCPIP_PRINT_DEBUG_STATS
    const char *names[] = {"down", "lnk up", "lnk rdy", "req", "ip", "ready"};
    size_t max = sizeof(names) / sizeof(char *);
    unsigned int state = ifp->state >= max ? max - 1 : ifp->state;
    MG_INFO(("Status: %s, IP: %M, rx:%u, tx:%u, dr:%u, er:%u", names[state],
             mg_print_ip4, &ifp->ip, ifp->nrecv, ifp->nsent, ifp->ndrop,
             ifp->nerr));
#if MG_ENABLE_IPV6
    state = ifp->state6 >= max ? max - 1 : ifp->state6;
    if (state > MG_TCPIP_STATE_UP)
      MG_INFO(("Status: %s, IPv6: %M", names[state], mg_print_ip6, &ifp->ip6));
#endif
#endif
    backlog_poll(ifp->mgr);
  }
  // Handle gw ARP request timeout, order is important
  if (expired_1000ms && ifp->state == MG_TCPIP_STATE_IP) {
    ifp->state = MG_TCPIP_STATE_READY;  // keep best-effort MAC or poison mark
    onstatechange(ifp);
  }
  if (expired_1000ms && ifp->state == MG_TCPIP_STATE_READY && !ifp->gw_ready &&
      ifp->gw != 0)
    mg_tcpip_arp_request(ifp, ifp->gw, NULL);  // retry GW ARP request
#if MG_ENABLE_IPV6
  // Handle gw NS/NA req/resp timeout, order is important
  if (expired_1000ms && ifp->state6 == MG_TCPIP_STATE_IP) {
    ifp->state6 = MG_TCPIP_STATE_READY;  // keep best-effort MAC or poison mark
    onstate6change(ifp);
  }
  if (expired_1000ms && ifp->state == MG_TCPIP_STATE_READY && !ifp->gw6_ready &&
      (ifp->gw6[0] != 0 || ifp->gw6[1] != 0))
    tx_ndp_ns(ifp, ifp->gw6, NULL);  // retry GW hwaddr resolution
#endif

  {  // poll driver and let L2 do its work, if any
    // Handle physical interface up/down status
    bool drv_up, l2_up;
    drv_up = ifp->driver->poll ? ifp->driver->poll(ifp, expired_1000ms) : true;
    l2_up = mg_l2_poll(ifp, expired_1000ms);  // Handle L2 up/down link status;
    if (expired_1000ms) {                     // ifp->state rules over state6
      mg_ip_link(ifp, drv_up, l2_up);         // Handle IPv4
      mg_ip6_link(ifp, drv_up, l2_up);        // Handle IPv6
      if (ifp->state < MG_TCPIP_STATE_UP) MG_ERROR(("Network is down"));
      mg_tcpip_call(ifp, MG_TCPIP_EV_TIMER_1S, NULL);
    }
  }

  mg_ip_poll(ifp, expired_1000ms);   // Handle IPv4
  mg_ip6_poll(ifp, expired_1000ms);  // Handle IPv6

  // Read data from the network (mg_tcpip_rx() will discard for us)
  if (ifp->driver->rx != NULL) {  // Simple polling driver, returns one frame
    size_t len =
        ifp->driver->rx(ifp->recv_queue.buf, ifp->recv_queue.size, ifp);
    if (len > 0) {
      ifp->nrecv++;
      mg_tcpip_rx(ifp, ifp->recv_queue.buf, len);
    }
  } else {  // Complex poll / Interrupt-based driver. Queues recvd frames
    char *buf;
    size_t len, cnt = 7;  // Max 7 packets to fetch
    while (cnt-- > 0 && (len = mg_queue_next(&ifp->recv_queue, &buf)) > 0) {
      mg_tcpip_rx(ifp, buf, len);
      mg_queue_del(&ifp->recv_queue, len);
    }
  }
  if (ifp->state < MG_TCPIP_STATE_UP) return;  // need to let L2 do its job

  // Process timeouts
  for (c = ifp->mgr->conns; c != NULL; c = c->next) {
    struct connstate *s = (struct connstate *) (c + 1);
    if ((c->is_udp && !c->is_arplooking) || c->is_listening || c->is_resolving)
      continue;
    if (ifp->now > s->timer) {
      if (s->ttype == MIP_TTYPE_ARP) {
        mg_error(c, "ARP timeout");
      } else if (c->is_udp) {
        continue;
      } else if (s->ttype == MIP_TTYPE_ACK && s->acked != s->ack) {
        MG_VERBOSE(("%lu ack %x %x", c->id, s->seq, s->ack));
        tx_tcp(ifp, s->mac, &c->loc, &c->rem, c->dscp, TH_ACK, mg_htonl(s->seq),
               mg_htonl(s->ack), NULL, 0);
        s->acked = s->ack;
      } else if (s->ttype == MIP_TTYPE_SYN) {
        mg_error(c, "Connection timeout");
      } else if (s->ttype == MIP_TTYPE_FIN) {
        c->is_closing = 1;
        continue;
      } else {
        if (s->tmiss++ > 2) {
          mg_error(c, "keepalive");
        } else {
          MG_VERBOSE(("%lu keepalive", c->id));
          tx_tcp(ifp, s->mac, &c->loc, &c->rem, c->dscp, TH_ACK,
                 mg_htonl(s->seq - 1), mg_htonl(s->ack), NULL, 0);
        }
      }

      settmout(c, MIP_TTYPE_KEEPALIVE);
    }
  }
}

// This function executes in interrupt context, thus it should copy data
// somewhere fast. Note that newlib's malloc is not thread safe, thus use
// our lock-free queue with preallocated buffer to copy data and return asap
void mg_tcpip_qwrite(void *buf, size_t len, struct mg_tcpip_if *ifp) {
  char *p;
  if (mg_queue_book(&ifp->recv_queue, &p, len) >= len) {
    memcpy(p, buf, len);
    mg_queue_add(&ifp->recv_queue, len);
    ifp->nrecv++;
  } else {
    ifp->ndrop++;
  }
}

void mg_tcpip_init(struct mg_mgr *mgr, struct mg_tcpip_if *ifp) {
  // If L2 address is not set, make a random one; fill MTU
  mg_l2_init(ifp);
  ifp->mtu = ifp->l2mtu;

  if (ifp->dhcp_name[0] == '\0')  // If DHCP name is not set, use "mip"
    memcpy(ifp->dhcp_name, "mip", 4);
  ifp->dhcp_name[sizeof(ifp->dhcp_name) - 1] = '\0';  // Just in case

  if (ifp->driver->init && !ifp->driver->init(ifp)) {
    MG_ERROR(("driver init failed"));
  } else {
    ifp->tx.buf = (char *) mg_calloc(1, ifp->framesize),
    ifp->tx.len = ifp->framesize;
    if (ifp->recv_queue.size == 0)
      ifp->recv_queue.size = ifp->driver->rx ? ifp->framesize : 8192;
    ifp->recv_queue.buf = (char *) mg_calloc(1, ifp->recv_queue.size);
    ifp->timer_1000ms = mg_millis();
    mgr->ifp = ifp;
    ifp->mgr = mgr;
    mgr->extraconnsize = sizeof(struct connstate);
    if (ifp->ip == 0 && ifp->l2type != MG_TCPIP_L2_PPP &&
        ifp->l2type != MG_TCPIP_L2_PPPoE)
      ifp->enable_dhcp_client = true;
    mg_random(&ifp->eport, sizeof(ifp->eport));  // Random from 0 to 65535
    ifp->eport |= MG_EPHEMERAL_PORT_BASE;        // Random from
                                           // MG_EPHEMERAL_PORT_BASE to 65535
    if (ifp->tx.buf == NULL || ifp->recv_queue.buf == NULL) MG_ERROR(("OOM"));
  }
}

void mg_tcpip_free(struct mg_tcpip_if *ifp) {
  mg_free(ifp->recv_queue.buf);
  mg_free(ifp->tx.buf);
  mg_free(ifp->dns4_url);
}

static void send_syn(struct mg_connection *c) {
  struct connstate *s = (struct connstate *) (c + 1);
  uint32_t isn = mg_htonl((uint32_t) mg_ntohs(c->loc.port));
  tx_tcp(c->mgr->ifp, s->mac, &c->loc, &c->rem, c->dscp, TH_SYN, isn, 0, NULL,
         0);
}

static void l2addr_resolved(struct mg_connection *c) {
  if (c->is_udp) {
    c->is_connecting = 0;
    mg_call(c, MG_EV_CONNECT, NULL);
  } else {
    send_syn(c);
    settmout(c, MIP_TTYPE_SYN);
  }
}

void mg_connect_resolved(struct mg_connection *c) {
  struct mg_tcpip_if *ifp = c->mgr->ifp;
  uint8_t *l2addr;
  c->is_resolving = 0;
  if (ifp->eport < MG_EPHEMERAL_PORT_BASE) ifp->eport = MG_EPHEMERAL_PORT_BASE;
  c->loc.port = mg_htons(ifp->eport++);
#if MG_ENABLE_IPV6
  if (c->rem.is_ip6) {
    if (c->rem.addr.ip6[0] == ifp->ip6ll[0]) {  // same local LAN, use ll
      c->loc.addr.ip6[0] = ifp->ip6ll[0], c->loc.addr.ip6[1] = ifp->ip6ll[1];
    } else {  // use global address
      c->loc.addr.ip6[0] = ifp->ip6[0], c->loc.addr.ip6[1] = ifp->ip6[1];
    }
    c->loc.is_ip6 = true;
  } else
#endif
  {
    c->loc.addr.ip4 = ifp->ip;
  }
  MG_DEBUG(("%lu %M -> %M", c->id, mg_print_ip_port, &c->loc, mg_print_ip_port,
            &c->rem));
  mg_call(c, MG_EV_RESOLVE, NULL);
  c->is_connecting = 1;
  if (c->is_udp && (l2addr = tcpip_mapip(ifp, &c->rem)) != NULL) {
    struct connstate *s = (struct connstate *) (c + 1);
    memcpy(s->mac, l2addr, sizeof(s->mac));
    l2addr_resolved(c);  // broadcast or multicast
#if MG_ENABLE_IPV6
  } else if (c->rem.is_ip6) {
    if (match_prefix((uint8_t *) c->rem.addr.ip6, ifp->prefix,
                     ifp->prefix_len)                       // same global LAN
        || (c->rem.addr.ip6[0] == ifp->ip6ll[0]             // same local LAN
            && !MG_IP6MATCH(c->rem.addr.ip6, ifp->gw6))) {  // and not gw
      // If we're in the same LAN, fire a Neighbor Solicitation
      MG_DEBUG(("%lu NS lookup...", c->id));
      tx_ndp_ns(ifp, c->rem.addr.ip6, NULL);  // RFC-4861 4.3, requesting
      settmout(c, MIP_TTYPE_ARP);
      c->is_arplooking = 1;
    } else if (ifp->gw6_ready) {
      struct connstate *s = (struct connstate *) (c + 1);
      memcpy(s->mac, ifp->gw6mac, sizeof(s->mac));
      l2addr_resolved(c);
    } else {
      MG_ERROR(("No IPv6 gateway, can't connect"));
    }
#endif
  } else {
    uint32_t rem_ip = c->rem.addr.ip4;
    if (ifp->ip && ((rem_ip & ifp->mask) == (ifp->ip & ifp->mask)) &&
        rem_ip != ifp->gw) {  // skip if gw (onstatechange -> ARP)
      // If we're in the same LAN, fire an ARP lookup.
      MG_DEBUG(("%lu ARP lookup...", c->id));
      mg_tcpip_arp_request(ifp, rem_ip, NULL);
      settmout(c, MIP_TTYPE_ARP);
      c->is_arplooking = 1;
    } else if (ifp->gw_ready) {
      struct connstate *s = (struct connstate *) (c + 1);
      memcpy(s->mac, ifp->gwmac, sizeof(s->mac));
      l2addr_resolved(c);
    } else {
      MG_ERROR(("No gateway, can't connect"));
    }
  }
}

bool mg_open_listener(struct mg_connection *c, const char *url) {
  c->loc.port = mg_htons(mg_url_port(url));
  if (!mg_aton(mg_url_host(url), &c->loc)) {
    MG_ERROR(("invalid listening URL: %s", url));
    return false;
  }
  return true;
}

static void write_conn(struct mg_connection *c) {
  long len = c->is_tls ? mg_tls_send(c, c->send.buf, c->send.len)
                       : mg_io_send(c, c->send.buf, c->send.len);
  // TODO(): mg_tls_send() may return 0 forever on steady OOM
  if (len == MG_IO_ERR) {
    mg_error(c, "tx err");
  } else if (len > 0) {
    mg_iobuf_del(&c->send, 0, (size_t) len);
    mg_call(c, MG_EV_WRITE, &len);
  }
}

static void init_closure(struct mg_connection *c) {
  struct connstate *s = (struct connstate *) (c + 1);
  if (c->is_udp == false && c->is_listening == false &&
      c->is_connecting == false) {  // For TCP conns,
    tx_tcp(c->mgr->ifp, s->mac, &c->loc, &c->rem, c->dscp, TH_FIN | TH_ACK,
           mg_htonl(s->seq), mg_htonl(s->ack), NULL, 0);
    settmout(c, MIP_TTYPE_FIN);
  }
}

static void close_conn(struct mg_connection *c) {
  struct connstate *s = (struct connstate *) (c + 1);
  mg_close_conn(c);
  (void) s;
}

static bool can_write(struct mg_connection *c) {
  return c->is_connecting == 0 && c->is_resolving == 0 && c->send.len > 0 &&
         c->is_tls_hs == 0 && c->is_arplooking == 0;
}

void mg_mgr_poll(struct mg_mgr *mgr, int ms) {
  struct mg_connection *c, *tmp;
  uint64_t now = mg_millis();
  mg_timer_poll(&mgr->timers, now);
  mg_ota_poll(mgr);
  if (mgr->ifp == NULL || mgr->ifp->driver == NULL) return;
  mg_tcpip_poll(mgr->ifp, now);
  for (c = mgr->conns; c != NULL; c = tmp) {
    struct connstate *s = (struct connstate *) (c + 1);
    bool is_tls = c->is_tls && !c->is_resolving && !c->is_arplooking &&
                  !c->is_listening && !c->is_connecting;
    tmp = c->next;
    mg_call(c, MG_EV_POLL, &now);
    MG_VERBOSE(("%lu .. %c%c%c%c%c %lu %lu", c->id, c->is_tls ? 'T' : 't',
                c->is_connecting ? 'C' : 'c', c->is_tls_hs ? 'H' : 'h',
                c->is_resolving ? 'R' : 'r', c->is_closing ? 'C' : 'c',
                mg_tls_pending(c), c->rtls.len));
    // order is important, TLS conn close with > 1 record in buffer (below)
    if (is_tls && (c->rtls.len > 0 || mg_tls_pending(c) > 0))
      c->is_tls_hs ? mg_tls_handshake(c) : handle_tls_recv(c);
    if (can_write(c)) write_conn(c);
    if (is_tls && c->send.len == 0) mg_tls_flush(c);
    if (c->is_draining && c->send.len == 0 && s->ttype != MIP_TTYPE_FIN)
      init_closure(c);
    // For non-TLS, close immediately upon completing the 3-way closure
    // For TLS, handle any pending data (above) until MIP_TTYPE_FIN expires
    if (s->twclosure &&
        (!c->is_tls || (c->rtls.len == 0 && mg_tls_pending(c) == 0)))
      c->is_closing = 1;
    if (c->is_closing) close_conn(c);
  }
  (void) ms;
}

bool mg_send(struct mg_connection *c, const void *buf, size_t len) {
  struct mg_tcpip_if *ifp = c->mgr->ifp;
  bool res = false;
  if (!c->loc.is_ip6 && (ifp->ip == 0 || ifp->state != MG_TCPIP_STATE_READY)) {
    mg_error(c, "net down");
#if MG_ENABLE_IPV6
  } else if (c->loc.is_ip6 && ifp->state6 != MG_TCPIP_STATE_READY) {
    mg_error(c, "net down");
#endif
  } else if (c->is_udp && (c->is_arplooking || c->is_resolving)) {
    // Fail to send, no target MAC or IP
    MG_VERBOSE(("still resolving..."));
  } else if (c->is_udp) {
    len = trim_len(c, len);  // Trimming length if necessary
    res = udp_send(c, buf, len);
  } else {
    res = len == 0 || mg_iobuf_add(&c->send, c->send.len, buf, len) > 0;
    // returning 0 means an OOM condition (iobuf couldn't resize), yet this is
    // so far recoverable, let the caller decide
  }
  return res;
}

uint8_t mcast_addr[6] = {0x01, 0x00, 0x5e, 0x00, 0x00, 0xfb};
void mg_multicast_add(struct mg_connection *c, char *ip) {
  (void) ip;  // ip4/6_mcastmac(mcast_mac, &ip); ipv6 param
  // TODO(): actual IP -> MAC; check database, update
  c->mgr->ifp->update_mac_hash_table = true;  // mark dirty
}

bool mg_dnsc_init(struct mg_mgr *mgr, struct mg_dns *dnsc);

static void setdns4(struct mg_tcpip_if *ifp, uint32_t *ip) {
  struct mg_dns *dnsc;
  mg_free(ifp->dns4_url);
  ifp->dns4_url = mg_mprintf("udp://%M:53", mg_print_ip4, ip);
  dnsc = &ifp->mgr->dns4;
  dnsc->url = (const char *) ifp->dns4_url;
  MG_DEBUG(("Set DNS URL to %s", dnsc->url));
  if (ifp->mgr->use_dns6) return;
  if (dnsc->c != NULL) mg_close_conn(dnsc->c);
  if (!mg_dnsc_init(ifp->mgr, dnsc))  // create DNS connection
    MG_ERROR(("DNS connection creation failed"));
}

void mg_tcpip_mapip(struct mg_connection *c, struct mg_addr *ip) {
  struct connstate *s = (struct connstate *) (c + 1);
  uint8_t *l2addr = tcpip_mapip(c->mgr->ifp, ip);
  if (l2addr == NULL) return;
  memcpy(s->mac, l2addr, sizeof(s->mac));
}

#endif  // MG_ENABLE_TCPIP
