Install with Docker - oshri-almog/esp32c5-kismet-wifi-interface GitHub Wiki

This page runs Kismet with the ESP32-C5 source from the project's Docker image, instead of building Kismet by hand. It is for Linux and Raspberry Pi users with boards plugged in, for Windows and macOS users with Docker Desktop, and for anyone who wants to feed boards on one machine to a Kismet server on another. Every variable, path, tag and build option is listed in Docker Reference.

Pick your setup

You have You run Section
Boards plugged into a Linux PC or a Raspberry Pi, and want Kismet on the same machine the kismet service Kismet with boards on Linux or a Raspberry Pi
No boards yet the demo service, with a fake board Try It Without Hardware
Windows or macOS with Docker Desktop, boards on the same computer Kismet in Docker Desktop, fed by the Python remote helper Docker Desktop on Windows and macOS
Boards on this machine, Kismet on another the helper service Boards here, Kismet elsewhere: the helper service

If you are still deciding between Docker and a native build, Choosing a Setup compares them. How It Works explains the pieces.

What has been tested

  • The project's CI (GitHub Actions), amd64 and arm64: the image built from the current files, and the smoke test passed on both architectures, 22 of 22 checks: the fake board in all three radios, and the helper role feeding a second container with a login whose password holds &, a space and %41. No real boards are involved.
  • Raspberry Pi 4 (8 GB, Debian 13 trixie, arm64), 2026-10-02, the current image: sudo docker compose --profile demo build kismet demo built both images in about 78.5 minutes. Then, with Compose and four boards on a powered hub:
    • the kismet service found the four boards by itself, made their /dev/serial/by-id links, and all four captured Wi-Fi, with no capability added;
    • with KISMET_SOURCES naming the boards by their /dev/serial/by-id links, two on Wi-Fi, one on 802.15.4 and one on BLE, all four sources ran; the Wi-Fi and BLE sources received packets, the 802.15.4 one none during the test;
    • while the container held the boards, opening one from the host failed with Device or resource busy, and the container's sources kept running. The Python remote helper's --list on the host still listed those boards;
    • the helper service fed two boards, one on Wi-Fi and one on BLE, to a Kismet outside Docker on the same Pi, and both remote sources ran with packets: with a login, with a password holding &, a space and %41, with an API key, and with a user name holding :. A user name with : together with an & was refused at start with exit status 2;
    • in both services, every kismet_cap_esp32c5 ran with no capabilities;
    • the demo service left the Pi's boards alone and ran only its fake board;
    • docker run without the device rules left the boards out and logged which rule was missing.
  • Windows 11, Docker Desktop 4.92 (Docker Engine 29.8, amd64): the image build, the smoke test, the Compose demo, and Kismet in a container fed by the Python remote helper from a real board on a COM port. These runs used an image built from an earlier version of the Docker files, and an earlier Python remote helper.
  • Windows 11, Docker Desktop 4.92.0 (Docker Engine 29.8.0, amd64), 2026-10-02, the published v0.1.0 images: docker pull of latest and demo without a login; the Compose demo, which pulled its image instead of building it, and the demo with docker run, in all three radios; the kismet service with Compose, and its update with docker compose pull, then docker compose up -d; the docker run form of step 2 of Kismet in Docker Desktop, boards through the Python remote helper; and an API key made with curl from Git Bash and with curl.exe from Windows PowerShell 5.1. No boards were involved.
  • Not tested yet: the published images on a Raspberry Pi (their arm64 image was only read from the registry, not pulled with Docker) and with real boards, boards passed into Docker Desktop through usbipd, macOS, Fedora, Arch, rootless Docker and Podman, and Kismet's web UI in a browser (only its REST API was checked). On the Pi: the login reset, a kismet_site.conf of your own, and a restart after a reboot.

Before you start

  • Flashed boards. Each board needs this project's firmware (Flashing the Firmware), and connects by its native USB port (USB ID 303a:1001). The image holds no firmware and no flashing tools, so flash before you start the container: from the host, or with the web flasher in Chrome or Edge on any desktop computer.

  • A powered USB hub for more than one or two boards. An unpowered hub browns out under several boards, and the failures look like firmware bugs. See Hardware.

  • A 64-bit system on a Raspberry Pi. The images are built for linux/amd64 and linux/arm64 only, so a Pi 4 or 5 needs a 64-bit OS. This prints arm64 on a suitable Pi:

    dpkg --print-architecture
  • Internet access, to pull the image or to build it.

Install Docker

Debian and Raspberry Pi

These are the Docker packages installed on the test Pi (Debian 13), plus git, which step 1 uses to get the project files and which a minimal install may not have:

sudo apt-get update
sudo apt-get install -y docker.io docker-buildx docker-compose git

That gives Docker 26.1.5 with BuildKit (docker-buildx 0.13.1) and Docker Compose 2.26.1. Debian's docker-compose package is Compose version 2, which adds the docker compose command used on this page; docker.io on its own does not include it. Every docker compose command run on the test Pi ran with these packages.

Docker's own packages, from Docker's instructions for Debian (https://docs.docker.com/engine/install/debian/), include Compose too, but those instructions start by removing Debian's Docker packages, and that route has not been tried in this project. Without Compose at all, use plain docker commands: Without Compose gives them for the kismet service, with a table of the other commands, and step 4 of the helper service for the helper. Docker Reference has the docker run form of all three services.

Other Linux distributions

Install Docker Engine and the Compose plugin as Docker describes for your distribution: https://docs.docker.com/engine/install/. Fedora and Arch have not been tried with this project.

Windows and macOS

Install Docker Desktop: https://docs.docker.com/desktop/. It includes Compose. Docker Desktop 4.92 (Docker Engine 29.8) on Windows 11 was tested; macOS was not. Read Docker Desktop on Windows and macOS first: containers there cannot see USB boards.

sudo or the docker group

On Debian, Docker's socket (/var/run/docker.sock) belongs to root and the docker group. A user in neither runs every Docker command with sudo, and this page writes the Linux commands that way.

The alternative is to add yourself to the docker group. Membership is equivalent to root: any member can start a container that mounts the host's whole file system. The test Pi keeps using sudo for that reason. If you accept the trade-off:

sudo usermod -aG docker "$USER"    # then log out and back in

Note: sudo does not pass your shell's environment variables on to Docker. Put Compose settings in the .env file described below, not in KISMET_PORT=... sudo docker compose ....

Kismet with boards on Linux or a Raspberry Pi

This runs the kismet service: Kismet in a container, with one source per board plugged into this machine.

1. Get the project files

git clone https://github.com/oshri-almog/esp32c5-kismet-wifi-interface
cd esp32c5-kismet-wifi-interface

Compose reads compose.yaml and .env from this directory, so run every docker compose command below from here.

2. Check that the host sees the boards

ls -l /dev/serial/by-id/

Each board shows up as a link named after its MAC, such as usb-Espressif_USB_JTAG_serial_debug_unit_F0:F5:BD:01:02:03-if00 -> ../../ttyACM0. The name stays with the board; the ttyACM number can change from one plug-in to the next. Every ESP32 on its native USB port gets a name of this shape, so other Espressif boards appear here too.

If nothing is listed, the board is not on the USB bus: check the cable and the hub, and see Troubleshooting.

3. Choose the login and the sources

Create a file named .env next to compose.yaml. Replace the password with your own:

KISMET_USER=admin
KISMET_PASSWORD=choose-a-long-password

Set both or neither. Without them the container makes up a login and prints it once (step 5).

By default every board found becomes a Wi-Fi source. To put one board on each radio, list the sources yourself. Change the ttyACM numbers to the ones step 2 showed:

KISMET_SOURCES="esp32c5-ttyACM0:mode=wifi esp32c5-ttyACM1:mode=zigbee esp32c5-ttyACM2:mode=btle"

Or put every board found on one radio with ESP32C5_MODE=zigbee (or btle).

A source can also name a board by its /dev/serial/by-id link, which stays right when the ttyACM numbers change. The container makes the same links as the host:

KISMET_SOURCES="esp32c5:device=/dev/serial/by-id/usb-Espressif_USB_JTAG_serial_debug_unit_F0:F5:BD:01:02:03-if00,mode=zigbee"

Separate definitions with spaces; a definition itself cannot contain a space. The full syntax is in Source Definitions. The file holds a password, so keep it private:

chmod 600 .env

The project's .gitignore lists .env, so git leaves it out of your commits.

4. Start Kismet

sudo docker compose up -d

The first time, Compose pulls ghcr.io/oshri-almog/esp32c5-kismet:latest, published for amd64 and arm64; Docker picks the one that matches the machine. No login is needed. The download is about 48 MB, and the image takes 177 MB on disk on amd64. Compose pulls only when no image of that name is on the machine: an image you built under that name (Building the image yourself) is used as it is, until sudo docker compose pull replaces it with the published one. If the pull fails, Compose builds the image locally instead, which takes about 18.5 minutes on a fast PC and about 80 minutes on a Raspberry Pi 4.

On Docker Desktop (amd64), Compose pulled the published demo image this way on its first start, and docker compose pull pulled latest. On the Pi, docker compose has not yet been run with the published image.

Follow the start-up in the container's log:

sudo docker compose logs -f kismet

With two boards found you should see lines like these, then Kismet's own log. Compose puts kismet-1 | in front of every line of the service's log; docker logs shows the same lines without it. Ctrl+C stops following the log; Kismet keeps running.

kismet-1  | [esp32c5-kismet] source: esp32c5-ttyACM0:mode=wifi
kismet-1  | [esp32c5-kismet] source: esp32c5-ttyACM1:mode=wifi
kismet-1  | [esp32c5-kismet] Kismet starts with 2 source(s)

Once a board is streaming, Kismet logs a line such as INFO: esp32c5-ttyACM0 capturing (wifi).

If no board is found at start, the container waits up to 30 s for one (waiting up to 30 s for an ESP32-C5 board). When it finds the boards by itself (no KISMET_SOURCES), whether they were there at start or turned up during the wait, it then looks again every 2 s until two looks in a row find the same boards, so that the boards of a hub that come up one after another are all taken. That ends 2 to 4 s after the last board appears, and never more than 10 s after the first board was found, or 40 s from the start of a wait (ESP32C5_WAIT plus 10 s). If none turns up, Kismet starts without sources and the log says:

[esp32c5-kismet] no ESP32-C5 board found. Plug one in and restart the container, or add sources from
[esp32c5-kismet] the web UI (Data Sources); boards plugged in now appear in the container on their own.

5. Find the login

If you set KISMET_USER and KISMET_PASSWORD, that is the login. Otherwise the container made one up and printed it:

sudo docker compose logs kismet | grep "web login"

The line looks like this, with your own password and Compose's kismet-1 | in front:

kismet-1  | [esp32c5-kismet] no web login was set, so Kismet's is now: user admin, password 5c1e0b7a92d4f3e8a6b1c0d2

Without Compose, use sudo docker logs esp32c5-kismet 2>&1 | grep "web login"; that line has no prefix.

Only the container that made the login printed it. After the container has been recreated (by an update, for example), read the login from the file Kismet keeps it in:

sudo docker compose exec kismet cat /root/.kismet/kismet_httpd.conf    # with Compose
sudo docker exec esp32c5-kismet cat /root/.kismet/kismet_httpd.conf    # with docker run

Why the container always sets a login: Kismet without one serves a page that lets the first visitor choose the admin login, and with port 2501 published on the network that is whoever gets there first. The container picks the login in this order:

Situation Login used
KISMET_USER and KISMET_PASSWORD both set Those two, written at every start. They replace a login kept from before, including one changed in the web UI.
Neither set, a login kept in the /root/.kismet volume The kept one, unchanged.
Neither set, nothing kept User admin and a random password of 24 hex characters, printed once.

If only one of the two variables is set, the log says KISMET_USER and KISMET_PASSWORD go together; ignoring the one that is set at every start, whichever login is then used.

To change the login later, set both variables in .env and run sudo docker compose up -d again, which recreates the container with the new values.

To start over with a new random login, take KISMET_USER and KISMET_PASSWORD out of .env, then remove the container, delete the kismet-home volume and start again:

sudo docker compose down
sudo docker volume rm esp32c5-kismet_kismet-home
sudo docker compose up -d

The new login is printed as above. This keeps the logs, which are in esp32c5-kismet_kismet-data, but deletes the API keys, so remote helpers need a new key. down comes first because Docker does not delete a volume that a container still uses, even a stopped one. Do not use docker compose down -v for this: it deletes the logs too.

With docker run, the volume is called kismet-home: run sudo docker rm -f esp32c5-kismet, then sudo docker volume rm kismet-home, then the docker run command again.

6. Open Kismet

In a browser, open http://192.168.1.50:2501, with your machine's address in place of 192.168.1.50 (or http://localhost:2501 on the machine itself), and log in.

Under Data Sources, each board should be listed and running. On the Pi (native build), the first packets arrived 1.7 to 2.7 s after Kismet started in the first field test (2.2 to 3.7 s in later runs). A board last used on another radio reboots into the requested one first, which took about 1.5 s. Continue with Guide: First Capture.

A source in error shows its reason there, with one exception: when the helper gives up on a board after 15 s without a capture, Kismet shows only IPC connection closed. The helper's reason is then in the container's log (sudo docker compose logs kismet), as a line such as ERROR: esp32c5-ttyACM0: no capture from the board on /dev/ttyACM0 for 15 seconds; is it flashed with the esp32c5 sniffer firmware, and is nothing else holding the port?. A board that fails this way right after it changed radio may have hung in the switch, which only a reset or a replug cures: see A board stops answering after a radio switch.

Note: Only capture on networks and devices you own or are authorised to test.

Without Compose

The same service with docker run. Change the password, and add -e KISMET_SOURCES="..." to choose the sources:

sudo docker run -d --name esp32c5-kismet --restart unless-stopped --init \
    --device-cgroup-rule 'c 166:* rmw' --device-cgroup-rule 'c 188:* rmw' \
    -p 2501:2501 \
    -v kismet-data:/data -v kismet-home:/root/.kismet \
    -e KISMET_USER=admin -e KISMET_PASSWORD=choose-a-long-password \
    ghcr.io/oshri-almog/esp32c5-kismet:latest

The two --device-cgroup-rule options are what compose.yaml sets for the service; How the boards reach the container explains them. No capability has to be added (no --cap-add): the capture helper drops every capability it has and needs none.

To run an image you built yourself (Building the image yourself), write esp32c5-kismet in place of ghcr.io/oshri-almog/esp32c5-kismet:latest.

The container's messages go to its standard error, so include it when you search the log:

sudo docker logs esp32c5-kismet 2>&1 | grep "web login"

The other Compose commands on this page become these, for the container named esp32c5-kismet:

With Compose With docker run
sudo docker compose logs -f kismet sudo docker logs -f esp32c5-kismet
sudo docker compose exec kismet ls -l /data sudo docker exec esp32c5-kismet ls -l /data
sudo docker compose cp kismet:/data/Kismet-20260928-11-13-48-1.kismet . sudo docker cp esp32c5-kismet:/data/Kismet-20260928-11-13-48-1.kismet .
sudo docker compose restart kismet sudo docker restart esp32c5-kismet
sudo docker compose stop kismet sudo docker stop esp32c5-kismet
sudo docker compose up -d after changing .env sudo docker rm -f esp32c5-kismet, then docker run again with the new -e values

To pass options to Kismet, put kismet and then the options after the image name, for example ... ghcr.io/oshri-almog/esp32c5-kismet:latest kismet --no-logging. The first word after the image picks the container's role, so the options alone would be run as a command.

How the boards reach the container

The container does not get the host's /dev. Two things give it the boards and nothing else:

  1. Device rules. compose.yaml gives the kismet and helper services device_cgroup_rules that allow two classes of device: major 166 (ttyACM, the boards' native USB) and major 188 (ttyUSB). The docker run equivalent is --device-cgroup-rule 'c 166:* rmw' --device-cgroup-rule 'c 188:* rmw'.
  2. Nodes made by the container. A container sees the host's /sys. The entrypoint reads the ttyACM and ttyUSB devices there and makes their /dev nodes itself, then checks again every second: a board plugged in, rebooted or back under another ttyACM number gets its node within about a second, and the node of a board that has gone is removed. It also makes the /dev/serial/by-id links the host's udev would make.

A board counts as found when its USB ID is 303a:1001. Its port is not opened to check, because opening the port moves DTR and RTS, which are the board's reset lines. At start the entrypoint also tries once, per device class, whether the device rules let it open such a device, on a spare device number, never on a board. A board whose class is not allowed is reported and left out:

[esp32c5-kismet] found ttyACM0 in sysfs but the container may not use it: allow it with
[esp32c5-kismet] compose.yaml's device_cgroup_rules, or docker run --device-cgroup-rule 'c 166:* rmw'

Why not share the host's /dev. A /dev:/dev bind would hand the container every device node on the host: its terminals, /dev/shm and its disks. An earlier version of compose.yaml did that; the device rules replaced it for this reason.

Why not --device /dev/ttyACM0. A fixed device entry is one node. A board that is plugged in again, or re-enumerates, can come back as another ttyACM number, and the container would lose it. Nodes given with --device are still used if you prefer them.

What this means in practice:

  • Boards plugged in after start get their node, but they are not added as sources, because the source list is worked out once at start. Add them from Kismet's Data Sources panel, or restart the container with sudo docker compose restart kismet (sudo docker restart esp32c5-kismet without Compose).
  • Boards named in KISMET_SOURCES can come later. A board you name there that is not plugged in at start stays in Kismet's Data Sources list with the reason, and Kismet retries it every 5 s, so it is picked up within about 5 s of being plugged in. You can list all the boards of a hub up front this way. Name them by their by-id links, since a board's ttyACM number is only known once it is plugged in.
  • Other Espressif boards (ESP32-C3, C6, H2, S3, P4) share the USB ID 303a:1001. With any of them plugged in, list your sources in KISMET_SOURCES so that only the sniffer boards are used.
  • One board, one source. A board captures with one radio at a time. Do not also give it to Kismet with -c in the container's command: auto-discovery would add it a second time. Use KISMET_SOURCES instead.
  • Stop the container before you use its boards elsewhere. While the container captures from a board, the helpers keep other programs out of its port, on the host and in other containers too: a second capture is refused with ... is already in use by another capture ..., and esptool cannot open the port. On the Pi, an open from the host failed with Device or resource busy while the container's sources kept running. A program run with sudo, such as sudo esptool, is let in all the same, and would disturb the capture. --list on the host cannot see the container's lock and still lists such a board (the Python remote helper's did on the Pi), so a board in that list may be in use by the container. So run sudo docker compose stop kismet (or sudo docker stop esp32c5-kismet) before you flash a board, open it in a serial monitor, or give it to the helper service or a native Kismet.
  • Flashing a board the container used for 802.15.4. A board flashed while it is in 802.15.4 mode can come back deaf on Wi-Fi, a known firmware issue: its Wi-Fi source runs but gets no packets, with no error. Run it as a Wi-Fi source once before you flash it. If it is deaf already, run it as a BLE source and then as a Wi-Fi source again; Flashing the Firmware has the details.
  • Only these boards. Kismet's own capture helpers for other hardware, such as Wi-Fi cards, are switched off in the image: they crash without the NET_ADMIN capability, which the container does not have. Docker Reference lists them.
  • Permissions. Everything in the container runs as root, so the host's dialout group does not matter here.

Settings

Set these in .env next to compose.yaml, or with -e on docker run. The full list is in Docker Reference.

Variable Default Meaning
KISMET_SOURCES empty Source definitions separated by spaces. Empty: every board found, each on the radio in ESP32C5_MODE.
ESP32C5_MODE wifi wifi, zigbee or btle: the radio for boards found by the container. Not used when KISMET_SOURCES is set.
ESP32C5_WAIT 30 Seconds to wait at start for a first board, when none is there. Boards the container finds by itself are then looked at again every 2 s until the list stops changing, for at most 10 s more (ESP32C5_WAIT + 10 s in all after a wait). 0 turns off both.
KISMET_USER, KISMET_PASSWORD empty The web login (step 5).
KISMET_PORT 2501 Compose only: the host port for Kismet's web UI. Change it when another Kismet already uses 2501.

For example, ESP32C5_WAIT=0 in .env, or -e ESP32C5_WAIT=0 on docker run, starts Kismet at once when no board is plugged in.

Where your data is

The image keeps two directories as volumes, and Compose gives them names so that they survive updates:

Volume (Compose name) In the container Holds
esp32c5-kismet_kismet-data /data Kismet's logs: one .kismet database per run, such as Kismet-20260928-11-13-48-1.kismet (time in UTC).
esp32c5-kismet_kismet-home /root/.kismet The web login (kismet_httpd.conf), the API keys and sessions (session.db), and Kismet's other per-user state.

To list the logs, and copy one out to the current directory (change the file name to one from the list):

sudo docker compose exec kismet ls -l /data
sudo docker compose cp kismet:/data/Kismet-20260928-11-13-48-1.kismet .

With docker run, the same with the container's name:

sudo docker exec esp32c5-kismet ls -l /data
sudo docker cp esp32c5-kismet:/data/Kismet-20260928-11-13-48-1.kismet .

The image also holds Kismet's log tools, such as kismetdb_to_pcap; Guide: Exporting to Wireshark shows how to turn a log into a pcapng file. Kismet writes only its own .kismet database by default; Kismet Configuration shows how to add pcapng logs.

docker run without -v gives each container two unnamed volumes, which docker rm -v removes with the container.

Docker Desktop on Windows and macOS

Docker Desktop runs containers in a Linux virtual machine that cannot see the computer's USB serial devices. On Windows the boards stay on their COM ports; Docker Desktop on macOS presumably has the same limit, but it was not tested. That leaves three ways to use it, after you get the project files.

Get the project files

Compose reads compose.yaml from the project, and docker build, if you build the image yourself, needs the project's files as well. In PowerShell, with Git for Windows installed (on macOS, in Terminal):

git clone https://github.com/oshri-almog/esp32c5-kismet-wifi-interface
cd esp32c5-kismet-wifi-interface

Run every docker compose and docker build command below from this directory, and create .env here.

The demo

The demo image runs Kismet with a fake board, and works on Docker Desktop:

docker compose --profile demo up demo

Then open http://localhost:2501 and log in as demo / demo. If your .env sets KISMET_USER and KISMET_PASSWORD (as the next section and step 3 on Linux do), the demo uses that login instead. Try It Without Hardware has the details.

The demo publishes the same host port as the kismet service, 2501 (on this computer's loopback only), so it cannot start while the kismet service is running. Stop the kismet service first, and start it again when you are done with the demo:

docker compose stop kismet
docker compose --profile demo up demo    # Ctrl+C ends the demo
docker compose start kismet

Or give the demo another port with KISMET_PORT in .env (for example KISMET_PORT=2502, then open http://localhost:2502). That moves the kismet service's port as well, the next time its container is created.

Kismet in Docker Desktop, boards through the Python remote helper

This is the tested way to use real boards with Docker on Windows: Kismet runs in Docker Desktop, and the Python remote helper runs on Windows, opens the boards on their COM ports and sends them to Kismet over remote capture.

  1. Create .env next to compose.yaml with your login (change the password):

    KISMET_USER=admin
    KISMET_PASSWORD=choose-a-long-password
  2. Start Kismet:

    docker compose up -d

    The container finds no board, waits 30 s, then starts Kismet without sources and logs the no-board message. That is expected here: the sources arrive from the remote helper. ESP32C5_WAIT=0 in .env skips the wait. The first time, Compose pulls the image, about 48 MB. On the test PC, with the image already pulled, Kismet answered about 32 s after docker compose up -d, after the 30 s wait, with no sources.

    Or, without Compose, publishing Kismet on this computer only and skipping the wait:

    docker run -d --name esp32c5-kismet -p 127.0.0.1:2501:2501 -e KISMET_USER=admin -e KISMET_PASSWORD=choose-a-long-password -e ESP32C5_WAIT=0 -v kismet-data:/data -v kismet-home:/root/.kismet ghcr.io/oshri-almog/esp32c5-kismet:latest

    On the test PC, with the published image already pulled, this command ran as written (with another password): Kismet answered 1.4 s later, logged the no-board message, and listened on 127.0.0.1:2501 only.

    To use an image you built yourself, build it from the project directory and write esp32c5-kismet in place of ghcr.io/oshri-almog/esp32c5-kismet:latest in the command above:

    docker build -f docker/Dockerfile -t esp32c5-kismet .

    This container needs no device rules, since it opens no board itself.

  3. Create an API key with the datasource role, which lets the helper feed sources and nothing else. From Git Bash (tested), with your login:

    curl -u admin:choose-a-long-password --data-urlencode 'json={"name": "windows-helper", "role": "datasource", "duration": 0}' http://127.0.0.1:2501/auth/apikey/generate.cmd

    It prints the key, 32 hex characters. duration 0 means it never expires. The key is kept in the kismet-home volume, so it survives restarts and updates. Kismet's web UI can also create keys, under Settings → API Keys.

    Note: In Windows PowerShell 5.1, curl is another command (Invoke-WebRequest), and PowerShell strips the double quotes inside the JSON before curl.exe sees them. Use Git Bash or the web UI for this step, or curl.exe with a backslash before each inner quote, as on Kismet Configuration:

    curl.exe -u admin:choose-a-long-password --data-urlencode 'json={\"name\": \"windows-helper\", \"role\": \"datasource\", \"duration\": 0}' http://127.0.0.1:2501/auth/apikey/generate.cmd

    In Windows PowerShell 5.1 this printed a key against the published image in Docker Desktop, as the Git Bash form did.

  4. On Windows, install the Python remote helper as Install on Windows describes, and list the boards:

    python -m esp32c5_kismet.remote --list
  5. Start the helper with the key and a board. Change COM14 to a port from the list and the key to yours:

    python -m esp32c5_kismet.remote --connect 127.0.0.1:2501 --apikey 3F9A6C1E07B24D58A1C9E2F4608B7D35 --source esp32c5-COM14:mode=wifi

    The examples use 127.0.0.1. On Windows, localhost resolves to the IPv6 address ::1 first, and in the tests against Kismet in WSL2 each attempt on ::1 cost about 2 s; Docker Desktop was not measured separately. The current Python remote helper tries 127.0.0.1 first when you write localhost, so either works with it while Kismet is up.

    The key can also come from the environment instead of the command line, where it shows in the process list. In PowerShell:

    $env:KISMET_CAP_APIKEY = "3F9A6C1E07B24D58A1C9E2F4608B7D35"
    python -m esp32c5_kismet.remote --connect 127.0.0.1:2501 --source esp32c5-COM14:mode=wifi
  6. In Kismet's web UI, Data Sources now lists the board as a remote source. Stop the helper with Ctrl+C.

What the test showed, with an earlier image and helper: a board on COM32 feeding Kismet in Docker Desktop with a login, hopping all 42 Wi-Fi channels at 5 per second, gave 6249 packets in about 2.5 minutes with no error packets, and 128 Wi-Fi devices, 11 of them on 5 GHz. Run on with an API key for about 90 s more, the source reached 15,032 packets and 179 devices. After docker restart, the helper reconnected by itself in about 7 s and the API key was still valid. Remote Capture covers the helper's options, and Guide: Windows Boards to a Pi does the same with the Kismet server on a Pi.

Boards passed into Docker Desktop through usbipd (untested)

On Windows, usbipd-win (https://github.com/dorssel/usbipd-win) can attach a USB device to WSL 2. Docker Desktop's engine runs in the same WSL 2 virtual machine, so an attached board should be visible to the kismet service as on Linux. Nobody has tried this with these boards. Attaching a board to WSL takes it away from Windows, so the Python remote helper cannot use it at the same time. Install on WSL2 describes attaching boards to WSL.

Windows notes

  • Port clash. A Kismet in WSL and one in Docker Desktop both want localhost:2501. Give the container another host port with KISMET_PORT=2502 in .env (or -p 127.0.0.1:2502:2501 on docker run), and connect helpers to that port.

  • Git Bash rewrites container paths. Git Bash turns arguments that look like POSIX paths into Windows paths, so /root/.kismet/... in a docker command reaches the container as a C:/... path. Run such commands in PowerShell, or prefix them in Git Bash:

    MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*' docker compose exec kismet cat /root/.kismet/kismet_httpd.conf
  • macOS. The Python remote helper has not been run on macOS; see Install on macOS and BSD.

Boards here, Kismet elsewhere: the helper service

The helper service runs no Kismet. It sends the boards plugged into this machine to a Kismet server on another machine over remote capture, one kismet_cap_esp32c5 (the C helper) per source.

flowchart LR
  subgraph here["This machine: boards and the helper service"]
    b0["board on ttyACM0"] --> h["helper container<br/>one kismet_cap_esp32c5 per source"]
    b1["board on ttyACM1"] --> h
  end
  subgraph there["Kismet server, 192.168.1.50"]
    k["Kismet<br/>web port 2501"]
  end
  h -- "websocket on port 2501<br/>API key, datasource role" --> k
Loading

The Kismet server must know the esp32c5 source type: this project's image (the kismet service on another machine), or a Kismet built with Building Kismet with ESP32-C5 Support. A stock Kismet cannot take these sources.

  1. Make port 2501 of the server reachable from this machine. The kismet service publishes it on all interfaces.

  2. Create an API key on the server with the datasource role. Change the address and the login to the server's:

    curl -u admin:choose-a-long-password --data-urlencode 'json={"name": "pi-helper", "role": "datasource", "duration": 0}' http://192.168.1.50:2501/auth/apikey/generate.cmd

    It prints the key, 32 hex characters.

  3. On the machine with the boards, get the project files (step 1 above) and create .env with the server's address, the key and the sources. Change all three to yours:

    KISMET_SERVER=192.168.1.50:2501
    KISMET_APIKEY=3F9A6C1E07B24D58A1C9E2F4608B7D35
    KISMET_SOURCES="esp32c5-ttyACM0:mode=wifi esp32c5-ttyACM1:mode=btle"

    Without KISMET_SOURCES, every board found is sent in ESP32C5_MODE, as in the kismet service.

  4. Start the helper. Name the service: --profile helper up on its own would also start the kismet service, and the two would compete for the same boards.

    sudo docker compose --profile helper up -d helper

    Without Compose, give the same settings with -e instead of .env. Change the address, the key and the sources to yours:

    sudo docker run -d --name esp32c5-helper --restart unless-stopped --init \
        --device-cgroup-rule 'c 166:* rmw' --device-cgroup-rule 'c 188:* rmw' \
        -e KISMET_SERVER=192.168.1.50:2501 \
        -e KISMET_APIKEY=3F9A6C1E07B24D58A1C9E2F4608B7D35 \
        -e KISMET_SOURCES="esp32c5-ttyACM0:mode=wifi esp32c5-ttyACM1:mode=btle" \
        ghcr.io/oshri-almog/esp32c5-kismet:latest helper

    To run an image you built yourself (Building the image yourself), write esp32c5-kismet in place of ghcr.io/oshri-almog/esp32c5-kismet:latest. The word helper after the image name picks the container's role.

  5. Check it. The log shows one line per source each time its helper starts:

    sudo docker compose --profile helper logs -f helper    # with Compose
    sudo docker logs -f esp32c5-helper                     # with docker run

    With Compose, each line starts with helper-1 |; docker logs shows it without:

    helper-1  | [esp32c5-kismet] helper: esp32c5-ttyACM0:mode=wifi -> 192.168.1.50:2501
    helper-1  | [esp32c5-kismet] helper: esp32c5-ttyACM1:mode=btle -> 192.168.1.50:2501
    helper-1  | [2026/10/02 08:44:13:0054] W: lws_create_context: unreasonable ulimit -n workaround
    helper-1  | [2026/10/02 08:44:13:0054] W: lws_create_context: unreasonable ulimit -n workaround
    helper-1  | INFO: 192.168.1.50:2501 starting capture...
    helper-1  | INFO: 192.168.1.50:2501 starting capture...
    

    The W: lws_create_context line comes once from each helper as it starts. It is harmless: the libwebsockets library finds Docker's limit on open files unreasonably high, and works around it. Once Kismet has opened a source, its helper adds INFO: 192.168.1.50:2501 starting capture.... On the server, Data Sources lists the boards as remote sources. On the Pi, two boards fed this way ran with packets. Outside Docker on the Pi, an earlier version of the C helper had its first packet about 1.2 s after it started, and packets then came every second. The current helper has done as well (first packet in under 1 s) only against the fake board in the end-to-end test, not yet with real boards.

How the helper service behaves:

  • Each source retries by itself. When its board has been away for 15 s, or the server cannot be reached, kismet_cap_esp32c5 tries again 5 s later without exiting (INFO: Sleeping 5 seconds before attempting to reconnect to remote server). The container starts it again 5 s after it exits, which happens only after a command-line error or a kill. Kismet never re-opens a remote source on its own; it waits for the helper to come back and recognises the source by its UUID. A helper started before its server logged FATAL: Datasource could not connect websocket and retried until the server came up.
  • No waiting for a first board. Boards it finds by itself are checked again every 2 s until the list stops changing, at most 10 s, since boards on a hub come up one after another (ESP32C5_WAIT=0 turns that off). With no board and no KISMET_SOURCES, the container exits with helper: no ESP32-C5 board found and KISMET_SOURCES is empty, and Docker restarts it (restart: unless-stopped) until a board is there. A source in KISMET_SOURCES that names no port (a bare esp32c5) stops before it connects when there is no board or more than one, with Could not probe local source prior to connecting to the remote host and the reason, and its helper tries again 5 s later, until the board is there. One that names a port (esp32c5-ttyACM0, device=, a by-id link) connects anyway; the open fails, Kismet logs Error connecting new remote source <name> (<uuid>) - cannot open /dev/ttyACM9: No such file or directory and does not list the source, and the helper tries again every 5 s until the board is there.
  • Boards plugged in later are not added; run sudo docker compose --profile helper restart helper (or sudo docker restart esp32c5-helper). A board named in KISMET_SOURCES is the exception: as the previous point says, its helper tries again every 5 s until the board is there.
  • A login instead of a key. KISMET_USER and KISMET_PASSWORD also work. The helper sends them in an Authorization header, so any character goes, &, spaces and %41 included (the smoke test's password holds all three). The one login that cannot work is a user name with : in it together with an & anywhere in the user name or password: the header ends a user name at its first :, so such a login goes in the remote capture URL instead, and Kismet decodes the URL's query before it splits it at every &. The container refuses that login and exits with status 2 and helper: a KISMET_USER with ':' in it cannot log in over remote capture when KISMET_USER or KISMET_PASSWORD holds '&'; use KISMET_APIKEY. A user name with : and no & anywhere works. An API key has no such limit, and can only feed sources.
  • The credentials stay out of the process list. The container hands them to kismet_cap_esp32c5 in its environment (KISMET_CAP_APIKEY, or KISMET_CAP_USER and KISMET_CAP_PASSWORD), not on its command line. With docker run, the key is on the docker run command line itself, so it is in your shell's history; Compose reads it from .env.
  • It uses the C helper. The Python remote helper is not in the image. On Windows, use the Python remote helper directly, as in Docker Desktop on Windows and macOS.
  • The device rules are needed as for the kismet service; compose.yaml sets them. No capability has to be added.
  • Closing a source in Kismet lasts only until its helper connects again, about 5 s later. To stop one source for good, take it out of KISMET_SOURCES and run sudo docker compose --profile helper up -d helper, which recreates the container.
  • Removed options wait for a Kismet restart. Kismet knows a remote source by its UUID, and one that connects again keeps any option its new definition leaves out, for as long as that Kismet runs. After you change a definition in KISMET_SOURCES, for example to drop channel_hop=false, restart the Kismet server as well, or write the new value out (channel_hop=true).
  • Stopping. sudo docker compose --profile helper stop helper (or sudo docker stop esp32c5-helper) ends every helper; the container exited in about 1 s in the test. Kismet then shows the sources in error with the reason websocket connection closed, which is expected.

Remote Capture explains remote capture in general, including the legacy TCP port 3501, which the image keeps on the container's loopback, out of reach.

Building the image yourself

Build the image yourself to try another Kismet commit, or to test changes to the helper or the image. Otherwise there is no need: Compose and docker run pull the published image.

With Compose

sudo docker compose build
sudo docker compose up -d

The first command builds the kismet service's image (and the helper's, which is the same image) and tags it ghcr.io/oshri-almog/esp32c5-kismet:latest on this machine, where it stands in for the published image of that name until a sudo docker compose pull replaces it. sudo docker compose up -d --build does both in one step. The demo image builds with sudo docker compose --profile demo build demo. To build both images at once, tagged ghcr.io/oshri-almog/esp32c5-kismet:latest and :demo:

sudo docker compose --profile demo build kismet demo

On a machine that runs only the helper service, build and start that service by name instead, so that the kismet service is not started as well:

sudo docker compose --profile helper up -d --build helper

With docker build

sudo docker build -f docker/Dockerfile -t esp32c5-kismet .
sudo docker build -f docker/Dockerfile --target demo -t esp32c5-kismet:demo .

The first makes the Kismet image (target kismet, the default); the second makes the demo image with the fake board. These local names are the ones to use with docker run. Compose looks for ghcr.io/oshri-almog/esp32c5-kismet:latest instead; to let it use the same build, add that name as a second tag, as in A long build over SSH.

How long it takes

Machine Time
Windows 11 PC, Docker Desktop 4.92 (Docker Engine 29.8), whose VM had 20 CPUs and 16 GB as docker info reported 18.5 minutes for a first build of the demo image, of which configuring, compiling and installing Kismet took 17.5 minutes at 4 compilers. Rebuilds from the cache took 2 to 39 s.
Raspberry Pi 4, 8 GB, Debian 13 (arm64) About 80 minutes for a first build of both targets with docker build (Docker 26.1.5), almost all of it compiling Kismet at 4 compilers. A later build of both with docker compose --profile demo build kismet demo, which took the Debian packages and the Kismet clone from the cache but compiled Kismet again, took 78.5 minutes, of which configuring and compiling Kismet took about 76.5 minutes. For comparison, compiling Kismet natively on the same Pi took about 78 minutes.
Raspberry Pi with 2 GB Longer: it builds with one compiler. Not measured, and it may not finish without swap (see Parallel compilers and memory).

Almost all of the time is Kismet itself, which the build compiles from source because no Kismet release includes this source yet. The build compiles Kismet with a stand-in for the helper first, so that a change to the helper's source rebuilds only the helper on top of the cached Kismet layer: about a minute on the Pi. A change to kismet/add-to-kismet.sh, kismet/datasource_esp32c5.h or kismet/capture_esp32c5/Makefile.in, or to a build argument, compiles Kismet again. Docker Reference lists what each change rebuilds.

The build needs internet access: Debian's package servers, GitHub (to clone Kismet) and Docker Hub (for the Debian base image and the Dockerfile syntax image). Disk space for the build was not measured.

Parallel compilers and memory

The build counts on about 1.5 GB of memory per compiler for Kismet's C++. By default it runs as many compilers as the free memory allows at that rate, but no more than the number of cores and no more than 4, and at least 1: an 8 GB Pi 4 gets 4, a 2 GB Pi gets 1. The build prints its choice as building with -j<N>.

Some files need more than that. On the 8 GB Pi, one Kismet file (phy_80211.cc) needed about 2.4 GB for its own compiler, more than a 2 GB Pi has in all. A 2 GB Pi therefore probably needs swap even with one compiler; without it, the build can run out of memory on that file, possibly long after it started. This is untested.

To set the number of compilers yourself:

sudo docker build -f docker/Dockerfile --build-arg JOBS=2 -t esp32c5-kismet .

A different JOBS value from the last build misses Docker's cache and compiles Kismet again (Docker's build cache works this way; not tried with this Dockerfile), so keep one value, or leave it unset.

A long build over SSH

On a Pi, a build over an SSH session stops if the session drops. The test Pi ran its 80-minute build as a transient systemd unit instead, which keeps going after you log out. Change the path to where you cloned the project:

sudo systemd-run --unit=esp32c5-docker-build --working-directory="$HOME/esp32c5-kismet-wifi-interface" sh -c "exec > $HOME/docker-build.log 2>&1; docker build -f docker/Dockerfile -t esp32c5-kismet -t ghcr.io/oshri-almog/esp32c5-kismet:latest ."
tail -f ~/docker-build.log

The build gets two names, so that both ways of running it use the result: docker run with esp32c5-kismet, and docker compose up -d, which finds ghcr.io/oshri-almog/esp32c5-kismet:latest on the machine and then neither pulls nor builds. On Docker Desktop (amd64), with that name already on the machine, a plain docker compose up -d printed no pull and no build and started the container in about 1 s. With only the first name, Compose would not find your build: it would pull the published image and run that instead. A later sudo docker compose pull also replaces your build under the ghcr.io name with the published image.

Another Kismet commit

The image builds Kismet at commit cfe427074, the one the source was developed against. To try another, set KISMET_REF. This example builds Kismet's newest code, on its master branch; put a commit hash or a tag in place of master to build that one:

sudo docker build -f docker/Dockerfile --build-arg KISMET_REF=master -t esp32c5-kismet .

A later build with the same KISMET_REF takes Kismet from Docker's cache, even after master has moved on; add --no-cache to fetch it again.

kismet/add-to-kismet.sh stops the build with anchor not found, Kismet has changed: ... if that commit has moved the lines it edits.

Image size

Image amd64 arm64 (the Pi's build)
Kismet image (:latest) 177 MB 140 MB
Demo image (:demo) 227 MB 177 MB

Updating

Your login, API keys and logs are in the volumes, so an update keeps them.

With a published image:

sudo docker compose pull
sudo docker compose up -d

On Docker Desktop (amd64), with v0.1.0 already pulled, docker compose pull printed Image ghcr.io/oshri-almog/esp32c5-kismet:latest Pulled, and docker compose up -d left the running container as it was, since the image had not changed. Compose recreates the container when the image has changed; with only one version published, an update that brings a new image has not been tried yet.

With an image you build:

git pull
sudo docker compose build
sudo docker compose up -d

On a machine that runs only the helper service, name the service in each command. The plain up -d above would start the kismet service there as well, and the two would compete for the same boards (--profile helper in these commands does no harm, but it is the service's name that keeps the kismet service out):

sudo docker compose --profile helper pull helper    # with a published image
sudo docker compose --profile helper up -d helper
git pull                                            # with an image you build
sudo docker compose --profile helper build helper
sudo docker compose --profile helper up -d helper

Without Compose: pull or build the new image, remove the old container, and run the docker run command again. docker rm keeps the named volumes kismet-data and kismet-home:

sudo docker pull ghcr.io/oshri-almog/esp32c5-kismet:latest
sudo docker rm -f esp32c5-kismet

For the helper, remove esp32c5-helper instead (sudo docker rm -f -v esp32c5-helper) and run its docker run command again.

Remote helpers reconnect by themselves when Kismet comes back: the Python remote helper took about 7 s after a docker restart, and the C helper retries every 5 s. The boards' firmware is updated separately; Guide: Updating covers both.

Stopping and removing

Command Effect
sudo docker compose stop Stops the containers; sudo docker compose start starts them again.
sudo docker compose down Removes the containers and the network; the volumes stay.
sudo docker compose down -v Also deletes the volumes: the login, the API keys and all logs.
sudo docker compose --profile helper stop helper Stops the helper service; sudo docker compose --profile helper start helper starts it again.
sudo docker compose --profile helper down Removes the helper service's container as well (and the kismet service's, if it runs here); the volumes stay.
sudo docker rm -f esp32c5-kismet Removes a docker run container; its named volumes stay.
sudo docker volume rm kismet-data kismet-home Deletes the docker run volumes.
sudo docker rm -f -v esp32c5-helper Removes a docker run helper container and its two unnamed volumes.

What matters for the helper service is its name. Compose acts on a service named on the command line whatever its profile, so --profile helper beside the name does no harm but is not needed. A command without a service name acts only on services without a profile and those whose profile is given: sudo docker compose down leaves the helper's container in place, and sudo docker compose --profile helper down removes it together with the kismet service's. For the same reason, sudo docker compose --profile helper up -d with no service name starts the kismet service as well (a bare --profile helper pull fetches the kismet service's image too, which is harmless: it is the same image). See Docker's page on profiles.

The kismet and helper services have restart: unless-stopped, so they start again when Docker starts, for example after a reboot, unless you stopped them. Guide: Running as a Service covers starting Kismet at boot.

Warning: Do not clean up with docker volume prune or docker system prune --volumes. They delete every unused unnamed volume on the machine, not only this project's (with -a, docker volume prune deletes unused named volumes too, such as kismet-data). Remove this project's containers and volumes by name, as in the table.

Log lines you can ignore

These come from Kismet itself, or from the libwebsockets library, not from this project:

Line Why
ERROR: Tried to re-register duplicate alert FLIPPERZERO Kismet at this commit logs it at every start. Harmless; do not treat "ERROR" lines alone as a failure.
ALERT: ROOTUSER Kismet is running as root Kismet runs as root inside the container.
INFO: (HTTPD) Could not read session data file, skipping loading saved sessions. A new, empty kismet-home volume.
Launching remote capture server on 127.0.0.1 3501 Kismet's legacy TCP remote capture port, kept on the container's loopback, where nothing outside can reach it.
A log path with /data// Kismet's own way of joining the log directory and file name.
ALERT: LOGDISABLED ... Demo only: the demo runs Kismet with --no-logging.
W: lws_create_context: unreasonable ulimit -n workaround, after a time stamp helper service, once per helper as it starts: libwebsockets finds the container's limit on open files unreasonably high, and works around it. Harmless.

For anything else, see Troubleshooting.

See also

⚠️ **GitHub.com Fallback** ⚠️