I dived into the open source lwip library in ESP8266 Arduino Core, and figured out how to observe raw Ethernet packets.
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: