Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ESP32S2: implement API for Promiscuous mode #4914

Closed
matthewjerome opened this issue Jun 24, 2021 · 18 comments · Fixed by #5537
Closed

ESP32S2: implement API for Promiscuous mode #4914

matthewjerome opened this issue Jun 24, 2021 · 18 comments · Fixed by #5537
Labels
enhancement espressif applies to multiple Espressif chips network
Milestone

Comments

@matthewjerome
Copy link

Has anyone taken a look at implementing promiscuous mode with Circuit Python + ESP32S2 ? I have tried a few approaches and no success.

Here is the sample code that I have been working on - after building using the make BOARD=unexpectedmaker_feathers2 command, the uf2 file is generated but freezes the chip when the esp_wifi_set_promiscuous(true) line is enabled.

Is there some other options that need to be set before setting esp_wifi_set_promiscuous ?

Similar to the AP Mode recently implemented : #4246
Example in Micropython + ESP8266 : https://github.com/mzakharo/micropython-promiscuous-esp8266
Example Reference ESP-IDF Implementation : https://github.com/adafruit/esp-idf/tree/circuitpython/examples/wifi/simple_sniffer

Target Python Implementation :

# callback from underlying esp_wifi_set_promiscuous_rx_cb()
def monitor(mac_in):
   print("MONITOR CALLBACK")
   print(mac_in)

def setupSensor():
   try:
       # put ESP32 in Promiscuous mode - channel, callback function - is this possible?
       wifi.radio.start_prom(2,monitor)
   except:
       print("ERROR")

ports/esp32s2/common-hal/wifi/Radio.h

typedef struct {
...
   bool prom_mode;
...
} wifi_radio_obj_t;

shared-bindings/wifi/Radio.h

…
extern void common_hal_wifi_radio_start_prom(wifi_radio_obj_t *self, uint8_t channel, mp_obj_t callback);
extern void common_hal_wifi_radio_stop_prom(wifi_radio_obj_t *self);
…

shared-bindings/wifi/Radio.c

//|     def start_prom(self,
//|                  channel: Optional[int] = 1
//|         """Starts an Promiscuous Mode with a callback
//|
//|


STATIC mp_obj_t wifi_radio_start_prom(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args)
{
   enum { ARG_channel, ARG_callback };
   static const mp_arg_t allowed_args[] = {
       { MP_QSTR_channel, MP_ARG_REQUIRED | MP_ARG_INT, {.u_int = 1} },
       { MP_QSTR_callback, MP_ARG_REQUIRED | MP_ARG_OBJ }
   };

   wifi_radio_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]);
   mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
   mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);

   common_hal_wifi_radio_start_prom(self, args[ARG_channel].u_int, args[ARG_callback].u_obj);
   return mp_const_none;
}

STATIC MP_DEFINE_CONST_FUN_OBJ_KW(wifi_radio_start_prom_obj, 1, wifi_radio_start_prom);

//|     def stop_prom(self) -> None:
//|         """Stops the Promiscuous Mode."""
//|         ...
//|
STATIC mp_obj_t wifi_radio_stop_prom(mp_obj_t self) {
   common_hal_wifi_radio_stop_prom(self);
   return mp_const_none;
}
MP_DEFINE_CONST_FUN_OBJ_1(wifi_radio_stop_prom_obj, wifi_radio_stop_prom);

ports/esp32s2/common-hal/wifi/Radio.c

static void set_mode_prom(wifi_radio_obj_t *self, bool state) {
   self->prom_mode = state;
}

void common_hal_wifi_radio_start_prom(wifi_radio_obj_t *self, uint8_t channel, mp_obj_t callback) {
nvs_flash_init();
esp_netif_init();
 ESP_ERROR_CHECK(esp_event_loop_init(NULL, NULL));
 wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
 ESP_ERROR_CHECK(esp_wifi_init(&cfg));
 ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_NULL)); // <-- No STA, no softAP.
 ESP_ERROR_CHECK(esp_wifi_start());
 wifi_promiscuous_filter_t flt;
 flt.filter_mask = WIFI_PROMIS_FILTER_MASK_DATA;
 ESP_ERROR_CHECK(esp_wifi_set_promiscuous_filter(&flt));
 ESP_ERROR_CHECK(esp_wifi_set_promiscuous_rx_cb(callback));
 ESP_ERROR_CHECK(esp_wifi_set_promiscuous(true));
 set_mode_prom(self, true);
}

void common_hal_wifi_radio_stop_prom(wifi_radio_obj_t *self) {
   set_mode_prom(self, false);
}
@dhalbert dhalbert added this to the Long term milestone Jun 24, 2021
@dhalbert dhalbert added the espressif applies to multiple Espressif chips label Jun 24, 2021
@anecdata
Copy link
Member

Any clues from the debug serial?

@tannewt
Copy link
Member

tannewt commented Jun 24, 2021

For the API, I'd suggest looking at how we do start_scanning() in BLE. We don't use callbacks due to their out-of-order nature. Instead, we return an iterator that yields objects.

@anecdata
Copy link
Member

Just a note that promiscuous mode can be run concurrently with STA, AP, or APSTA modes, but Espressif recommends not using promiscuous mode along with heavy non-promiscuous traffic.
https://docs.espressif.com/projects/esp-idf/en/v4.2/esp32s2/api-guides/wifi.html#wi-fi-sniffer-mode

@matthewjerome
Copy link
Author

Hi everyone - thank you for the quick response and insight.

In the past week I have tried various different approaches with changing the order of initializing the ESP32 module etc but no luck. One thing that I did find is that setting WIFI_MODE_NULL causes the response of esp_wifi_get_mode() to return 0 - meaning an initialization error - this may be one reason it wasn't working since the expected result is 1 or ESP_OK. Based on the example in examples/wifi/iperf/main/cmd_wifi.c, the WIFI_MODE_NULL is used but this may not be an available option based on this issue - espressif/esp-idf#1951.

Moving on from here, after trying to start in WIFI_MODE_AP and WIFI_MODE_STA the response from esp_wifi_get_mode() returns 1 but the chip still hangs after enabling the line esp_wifi_set_promiscuous(true).

From many dozen attempts at changing order of commands and other values, the most recent attempt of the circuitpython build is based on the below code and seems like the correct approach but still no luck.

Including or excluding the esp_wifi_set_promiscuous_filter(&flt) and esp_wifi_set_promiscuous_rx_cb(callback) does not seem to make a difference to the promiscuous mode enable line nor does the order of when they are called.

@anecdata - you mentioned the output from the debug serial - could you elaborate on this? right now the only output that I am using is from minicom -D /dev/cu.usbmodemC7FD1A3025A11 -b 11520 and only outputs print() statements from python and not the direct C-code output from the esp32. do you have some suggestions for getting a lower level debug output from the esp32 outside of python directly from the chip or in C?

esp_err_t event_handler(void *ctx, system_event_t *event)
{
    return ESP_OK;
}

void common_hal_wifi_radio_start_prom(wifi_radio_obj_t *self, uint8_t channel, mp_obj_t callback) {
  esp_wifi_stop() ;
  nvs_flash_init();
  esp_netif_init();
  ESP_ERROR_CHECK( esp_event_loop_init(event_handler, NULL) );
  wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
  ESP_ERROR_CHECK(esp_wifi_init(&cfg));
  ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM) );
  ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
  ESP_ERROR_CHECK(esp_wifi_start());
  ESP_ERROR_CHECK(esp_wifi_set_promiscuous(true));
  set_mode_prom(self, true);
}

@anecdata
Copy link
Member

anecdata commented Jul 6, 2021

make BOARD=... DEBUG=1 will enable debug messages to the debug console on pins 43 & 44.

@anecdata
Copy link
Member

anecdata commented Jul 6, 2021

There may be an issue having some of these lines in the start_prom function:

  nvs_flash_init();
  esp_netif_init();
  ESP_ERROR_CHECK( esp_event_loop_init(event_handler, NULL) );
  wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
  ESP_ERROR_CHECK(esp_wifi_init(&cfg));
  ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM) );

Most of that is already done at system start up time, I'm not sure what happens if they are executed again: https://github.com/adafruit/circuitpython/blob/main/ports/esp32s2/common-hal/wifi/__init__.c

@matthewjerome
Copy link
Author

matthewjerome commented Jul 6, 2021

tried removing these lines and few variations (STA / AP modes and including/removing start/stop wifi) but no luck - still freezing on the line esp_wifi_set_promiscuous(true);. in terms of "debug console on pins 43 & 44", could you elaborate on this? is this something to enable a log or a output other than the REPL ? thank you for your help!

attempt 1 :

void common_hal_wifi_radio_start_prom(wifi_radio_obj_t *self, uint8_t channel, mp_obj_t callback) {
	esp_wifi_stop() ;
	ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
	ESP_ERROR_CHECK(esp_wifi_start());
	esp_wifi_set_promiscuous(true);
	set_mode_prom(self, true);
}

attempt 2 :

void common_hal_wifi_radio_start_prom(wifi_radio_obj_t *self, uint8_t channel, mp_obj_t callback) {
	esp_wifi_stop() ;
	ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
	ESP_ERROR_CHECK(esp_wifi_start());
	esp_wifi_set_promiscuous(true);
	set_mode_prom(self, true);
}

attempt 3 :

void common_hal_wifi_radio_start_prom(wifi_radio_obj_t *self, uint8_t channel, mp_obj_t callback) {
	ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
	esp_wifi_set_promiscuous(true);
	set_mode_prom(self, true);
}

attempt 4 :

void common_hal_wifi_radio_start_prom(wifi_radio_obj_t *self, uint8_t channel, mp_obj_t callback) {
	ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
	esp_wifi_set_promiscuous(true);
	set_mode_prom(self, true);
}

@anecdata
Copy link
Member

anecdata commented Jul 6, 2021

The RX and TX pins on the FeatherS2 can be connected to another USB port on your computer via a USB breakout or similar, and output can be monitored with your serial program. This requires rebuilding circuitpython with the DEBUG=1 flag as above. There are some messages that are output by the underlying Espressif core code, some have been added by CircuitPython contributors in common-hal, and you can add your own to aid in debugging. For example, see messages starting with ESP_LOG in: https://github.com/adafruit/circuitpython/blob/main/ports/esp32s2/common-hal/wifi/__init__.c.

@anecdata
Copy link
Member

anecdata commented Sep 19, 2021

I think it's worth discussing some design goals. Monitor will potentially be subjected to a firehose of wifi frames, even with filtering by channel, type (mgmt, data, etc), subtype (Beacon, Probe Request), etc. Limitations of RAM and processing throughput will both probably come into play.

Espressif's esp-idf example allows filtering by channel and type, and either saving to SD or I think somehow viewing over JTAG, but I think this may not be a common use case for CircuitPython users, who may want to see what's happening now and process it lightly. Just to get my feet wet, I modified the Espressif example to simply print frames to serial with headers decoded. There are other inexpensive tools (like Raspberry Pi) that can do monitor mode and use standard Linux tools. I have a Zero W implementation using tcpdump with custom Python post-processing, and a forked & modified Arduino/ESP-IDF hybrid sniffer implementation that offloads filtered frames via serial to custom CircuitPython post-processing. I think an ESP32-Sx implementation has the potential to simplify and improve on those.

In any case, there are a variety of ways the firehose could be throttled down, and we should think about what's the best CircuitPython operation and API. How many frames to return, whether they should be the most recent or a more random sampling, whether duplicates should be weeded out in the core code (e.g., Beacon frames at a rate of 10/sec per AP per SSID is typical), whether we simply drop new incoming frames when some buffer is full or drop the older frames in favor of the newer frames, etc. One problem with a fixed size buffer that drops frames from one end or the other is that it will not necessarily be representative of the traffic over a longer period than the timeframe of the buffer; in other words at the CircuitPython level the results will be chunks taken from the stream rather than some smoother sampling. We could attempt to enforce enough filters to keep the data volume manageable, but that's very difficult to predict across wifi environments.

Any thoughts, or specific use cases?

@tannewt
Copy link
Member

tannewt commented Sep 20, 2021

I'd suggest modeling the API after the BLE scanning API. It has similar issues of getting a lot of inputs and needing to filter early.

@matthewjerome
Copy link
Author

Providing a means to generate a simple list of "device X was present for Y seconds" is probably enough to get started. By passing in filters of channel, mgmt packet type and optionally (?) a minimum signal strength or minimum packet count found before storing is enough. This would further minimize the amount of data passed back to the CircuitPython level can be minimized.

Example : [A:72,B:12,C:99,D:2 ... ]

What do you think?

@anecdata
Copy link
Member

anecdata commented Sep 21, 2021

Makes sense to me. I'm still trying to get my arms around the idf example, and how to translate that to common-hal. Seems to me that:

  • sniffer start sets up a work queue, creates the sniffer task, sets up the sniffer callback for incoming frames, sets up the filters (frame type and channel in this case), and enables monitor mode
  • the sniffer callback receives one frame at a time and adds it to the work queue, if enough memory is available (I don't know what happens if the callback can't keep up - frame type is the only filter that seems to affect frames going to the callback or not, other filtering has to be calculated in sniffer task)
  • the sniffer task consumes frames from the work queue, and does TBD to them
  • sniffer stop shuts it all down

This is different and separate from the existing station and AP operation where there is a single event handler to catch asynchronous wifi events. I think we need the callback and the continuous (while started) sniffer task, but they don't get exposed to the API, just like the wifi event handler isn't exposed to the API.

Probably initially we should start with a simple set of filters similar to what you suggest. I think longer-term we might want: channel hopping to get frames from a list of (not-necessaily-contiguous) channels, time interval as an option in addition to frame count, and maybe a way to just return frame headers instead of whole frame bodies. I don't know how much call there would be to handle non-mgmt frames or specific SSIDs or specific SA/DA/BSSID MACs. There's also the issue of whether to try to scrub out duplicates.

I would like to pop the frame we capture up a level to also capture especially the RSSI and the channel (in case of channel hopping): https://github.com/espressif/esp-idf/blob/e493a4c30e1b2f7c54a376327388b9e12262cbbf/components/esp_wifi/include/esp_wifi_types.h#L378 Or they could be passed in like the pcap parameters are passed in along with the packet buffer in the idf example. Maybe that's the better approach to reduce the size of each buffer returned.

Another consideration: memory available to esp-idf is not large. The way I understand it, devices that have extra PSRAM have that RAM devoted to CircuitPython code use (heap). So the number of frames we can accumulate may be very limited. WiFi devices come and go all the time, and traffic is bursty. It will be interesting to see how much practical control we're able to achieve over the smoothness (ability to continuously pass a even sampling up to CircuitPython) or chunkiness (e.g., we got got a burst of 10 frames from the 100 or 1000 or 10_000 that hit the antenna over some interval that includes the overhead of passing the frames up to user code and doing something with them).

The API, as tannewt suggests, would be pretty simple: start_scan with the various filtering parameters, stop_scan, and an iterable of sniffed frames.

@matthewjerome Did you get the debug console working, and are you working toward a PR? I'm far from expert at CircuitPython core development, but I'm happy to help in any way I can.

@anecdata
Copy link
Member

anecdata commented Sep 23, 2021

One more trade-off to consider. Processing is faster in C than Python, which argues for doing more packet parsing in C and returning more fields as opposed to a raw packet. But that reduces the duty cycle of packet capture.

On the other hand, doing more packet parsing in Python will make the capture more "chunky" in that we'd be grabbing some packets quickly, then processing that chunk more slowly while we miss capturing more packets.

I don't know, maybe it's a parameter and the code can do either. I don't have an intuition for the performance trade-off, or code size if that's an issue. I think it's safe to say we don't want to fully parse packets. The variations are complex, would require a lot of code, and a lot of the information is pretty esoteric and would probably have little value to most users.

In between, there are degrees of parsing we could think about. Parsing the frame header is pretty simple, it's all fixed fields... some bits, some bytes, some multi-byte sequences. The frame body is a little more involved. Management type frames have different numbers of fixed-length fields (from 0 to 12 bytes total) by subtype, followed by the variable-length Information Elements, each of which has a code, a length, and a body. So to parse the frame body, wherever it's done, the code has to step through the IEs one by one to see what's there and extract anything interesting.

Regardless of where on the spectrum of core <--> user the parsing is done, there is at least one particularly interesting variable element that's probably worth extracting in the core: SSID (when present), and it shouldn't be too demanding as it's usually the first IE.

@anecdata
Copy link
Member

anecdata commented Sep 23, 2021

Still playing around with heavily-modified example code that does management frames only, single-channel or channel-hopping, parses frame headers, and does a coarse parse and dump of the fixed and variable components of the frame body. The test environment has a relatively limited set of devices in range. Let it run a few hours last night, and do get interspersed errors from the callback:

  • Not enough memory for promiscuous packet (esp-idf heap stats when this occurs is typically something like: total=166500 free=4596 largest=64)

  • sniffer work queue full (CONFIG_SNIFFER_WORK_QUEUE_LEN=128)

So I think it's safe to say that filters should be set relatively tightly to stay within good operating conditions. It seems the errors are caught without incident, and packet capture continues, but some packets are missed.

@anecdata
Copy link
Member

anecdata commented Sep 25, 2021

WiFi and BLE scanning both seek to discover a relatively fixed list of devices in range, and those devices are explicitly sending frequent beacons and advertisements in order to be discovered within a fixed length of time. Advertisements from a connected BLE device are conceptually infinite, but I believe the typical use case makes occasional scanning for new data a practical approach.

WiFi monitor mode, on the other hand, is conceptually an infinite process with no clear end point. Occasional scanning may miss relatively ephemeral but useful events. I wonder if an explicit queue mechanism would be more appropriate, something more akin to keypad.EventQueue. An explicit queue with sufficiently tight filtering criteria could allow a more continuous flow, or at least a more random sampling of the continuous flow.

The API could get even more interesting on processors with more memory (e.g., esp32-s3), faster processors, or on something like a Raspberry Pi Zero W with CircuitPython.

@skickar
Copy link

skickar commented Sep 28, 2021

I'm featuring CircuitPython on the esp32s2 on the Hak5 YouTube channel and was pleased to see this is being worked on. Just wanted to mention I'd be happy to feature this on Hak5 with a shout-out and $100 gift card to whoever manages to pull it off, thank you all for your work!

@anecdata
Copy link
Member

anecdata commented Sep 28, 2021

There's a very rough and rudimentary POC here, basically the Espressif sniffer example (minus the SD Card, JTAG, and GUI), shoehorned into CP and tweaked to parse the more interesting RX_CTRL header and frame header fields and split up the frame body into its fixed and variable (Information Element) components, just enough to extract an SSID if available. Management frame type only at the moment, any subtype. Channel changes after each frame. CircuitPython code just starts and stops monitor mode, no parameters are currently sent, and no data is currently returned:

while True:
    print("enable monitor...")
    wifi.radio.set_monitor_enabled(True)
    time.sleep(5)
    wifi.radio.set_monitor_enabled(False)
    print("monitor disabled.")
    time.sleep(55)

CP console:

enable monitor...
monitor disabled.
enable monitor...
...

debug console:

...
I (503458) wifi:ic_disable_sniffer
I (503458) monitor: stop WiFi promiscuous ok
I (558558) wifi:ic_enable_sniffer
I (558558) monitor: start WiFi promiscuous ok
...
LEN=200, CH=03, RSSI=-48, FC=80, V=0, SUBT=80, FLAGS=00, DUR=00000, A1=ff:ff:ff:ff:ff:ff, A2={redacted}, A3={redacted}, Frag=005, SeqN=00241,
Beacon
Fixed: {hex data redacted}
Variable:  {hex data redacted}
ID=000 LEN=005 SSID={redacted}
...

Build with DEBUG=1 and the lightly-formatted frames will dump to the debug console. Unless you're in a Faraday cage, there will be A LOT of frames.

There are a ton of things left to do to improve code, add parameters and return data, and conform to the CircuitPython idioms. Significant open questions remain on the API (see above). If someone else wants to run with this or start from scratch, that's totally fine.

@anecdata
Copy link
Member

anecdata commented Oct 4, 2021

There's a new branch https://github.com/anecdata/circuitpython/tree/monitor_min with a rough, simple received = wifi.radio.monitor_recv_into(buf, MAXBUF) API. It needs fleshing out and cleaning up.

There's a thread now on the Adafruit Discord, in the #circuitpython-dev channel to discuss some design and implementation issues.

@microdev1 microdev1 linked a pull request Nov 2, 2021 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement espressif applies to multiple Espressif chips network
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants