"Wireshark" on ESP8266: PacketDump

I dived into the open source lwip library in ESP8266 Arduino Core, and figured out how to observe raw Ethernet packets.

PacketDump.ino demo

lwip's struct netif

lwip declares a network interface structure in lwip/netif.h:

struct netif {
  struct netif *next;

  ip_addr_t ip_addr;
  ip_addr_t netmask;
  ip_addr_t gw;

  netif_input_fn input;
  netif_output_fn output;
  netif_linkoutput_fn linkoutput;

  u16_t mtu;
  u8_t hwaddr_len;
  u8_t hwaddr[NETIF_MAX_HWADDR_LEN];
  u8_t flags;
  char name[2];
  u8_t num;

  // (some fields are omitted)
};

There are three function pointers:

  • The physical layer driver invokes input function when an Ethernet frame arrives.
  • The IP layer invokes output function when an IP packet is ready to be sent. It is usually hooked to an ARP implementation to perform ARP queries.
  • The linkoutput function calls into the physical layer driver to transmit an Ethernet frame.

The relevant function signatures are also declared in lwip/netif.h:

typedef err_t (*netif_input_fn)(struct pbuf *p, struct netif *inp);
typedef err_t (*netif_output_fn)(struct netif *netif, struct pbuf *p, ip_addr_t *ipaddr);
typedef err_t (*netif_linkoutput_fn)(struct netif *netif, struct pbuf *p);

We can get a hold of netif instances through the struct netif* netif_list singly linked list.

PacketDump on ESP8266 Arduino

These findings allowed me to create a Wireshark-like packet dumper on the ESP8266.

PacketDump.ino

PacketDump.ino initializes the packet dumper. The code first enables WiFi, then replaces netif->input and netif->output functions with two custom functions, myInput and myLinkOutput. myInput function uses PacketParser class to parse and print the incoming packet, and then invokes lwip stack through the original function pointer. Similarly, myLinkOutput function parses the outgoing packet, and then invokes physical layer driver through the original function pointer.

PacketParser pp(Serial);

netif_input_fn oldInput = nullptr;
netif_linkoutput_fn oldLinkOutput = nullptr;

void
processPacket(char dir, const pbuf* p)
{
  Serial.print(dir);
  Serial.print(' ');
  pp.processEthernet(p, 0);
  Serial.println();
}

err_t
myInput(pbuf* p, netif* inp)
{
  processPacket('>', p);
  return (*oldInput)(p, inp);
}

err_t
myLinkOutput(netif* netif, pbuf* p)
{
  processPacket('<', p);
  return (*oldLinkOutput)(netif, p);
}

void
replaceNetifFns(netif* netif)
{
  Serial.printf_P(PSTR("Replacing callbacks on netif %c%c%d\n"), netif->name[0], netif->name[1], netif->num);
  oldInput = netif->input;
  netif->input = myInput;
  oldLinkOutput = netif->linkoutput;
  netif->linkoutput = myLinkOutput;
}

void
setup()
{
  Serial.begin(115200);
  Serial.println();

  WiFi.persistent(false);
  // wifi_promiscuous_enable(1);
  WiFi.mode(WIFI_STA);

  assert(netif_list != nullptr); // should have one interface
  assert(netif_list->next == nullptr); // should have only one interface
  replaceNetifFns(netif_list);

  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
  }
  delay(1000);
  Serial.print(F("WiFi connected IP="));
  Serial.println(WiFi.localIP());
}

This initialization procedure only allows the ESP8266 to receive multicast packets and unicast packets addressed toward itself. Even if I attempted to enable promiscuous mode, it cannot receive unicast packets addressed to other hosts, because the WiFi access point would not deliver them.

PacketParser class

The PacketParser class contains logic to dissect Ethernet, ARP, IPv4, TCP, and UDP packets. It makes use of packet header definitions in lwip's header files, although it is possible to provide my own header definitions in order to understand more protocols.

Code Download

View PacketDump code on GitHub Gist

Download PacketDump code as ZIP

To compile the code, make sure to select "lwIP Variant: v1.4 Prebuilt" in Arduno IDE Tools menu:

lwIP Variant setting