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.
| 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.
-
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
helperrole 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 demobuilt both images in about 78.5 minutes. Then, with Compose and four boards on a powered hub:- the
kismetservice found the four boards by itself, made their/dev/serial/by-idlinks, and all four captured Wi-Fi, with no capability added; - with
KISMET_SOURCESnaming the boards by their/dev/serial/by-idlinks, 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--liston the host still listed those boards; - the
helperservice 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_esp32c5ran with no capabilities; - the
demoservice left the Pi's boards alone and ran only its fake board; -
docker runwithout the device rules left the boards out and logged which rule was missing.
- the
- 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.0images:docker pulloflatestanddemowithout a login; the Compose demo, which pulled its image instead of building it, and the demo withdocker run, in all three radios; thekismetservice with Compose, and its update withdocker compose pull, thendocker compose up -d; thedocker runform of step 2 of Kismet in Docker Desktop, boards through the Python remote helper; and an API key made withcurlfrom Git Bash and withcurl.exefrom 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.confof your own, and a restart after a reboot.
-
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/amd64andlinux/arm64only, so a Pi 4 or 5 needs a 64-bit OS. This printsarm64on a suitable Pi:dpkg --print-architecture
-
Internet access, to pull the image or to build it.
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 gitThat 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.
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.
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.
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 inNote:
sudodoes not pass your shell's environment variables on to Docker. Put Compose settings in the.envfile described below, not inKISMET_PORT=... sudo docker compose ....
This runs the kismet service: Kismet in a container, with one source per board plugged into this machine.
git clone https://github.com/oshri-almog/esp32c5-kismet-wifi-interface
cd esp32c5-kismet-wifi-interfaceCompose reads compose.yaml and .env from this directory, so run every docker compose command below from here.
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.
Create a file named .env next to compose.yaml. Replace the password with your own:
KISMET_USER=admin
KISMET_PASSWORD=choose-a-long-passwordSet 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 .envThe project's .gitignore lists .env, so git leaves it out of your commits.
sudo docker compose up -dThe 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 kismetWith 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.
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 runWhy 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 -dThe 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.
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.
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:latestThe 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.
The container does not get the host's /dev. Two things give it the boards and nothing else:
-
Device rules.
compose.yamlgives thekismetandhelperservicesdevice_cgroup_rulesthat allow two classes of device: major 166 (ttyACM, the boards' native USB) and major 188 (ttyUSB). Thedocker runequivalent is--device-cgroup-rule 'c 166:* rmw' --device-cgroup-rule 'c 188:* rmw'. -
Nodes made by the container. A container sees the host's
/sys. The entrypoint reads thettyACMandttyUSBdevices there and makes their/devnodes itself, then checks again every second: a board plugged in, rebooted or back under anotherttyACMnumber 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-idlinks 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-kismetwithout Compose). -
Boards named in
KISMET_SOURCEScan 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'sttyACMnumber 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 inKISMET_SOURCESso 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
-cin the container's command: auto-discovery would add it a second time. UseKISMET_SOURCESinstead. -
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 withDevice or resource busywhile the container's sources kept running. A program run withsudo, such assudo esptool, is let in all the same, and would disturb the capture.--liston 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 runsudo docker compose stop kismet(orsudo docker stop esp32c5-kismet) before you flash a board, open it in a serial monitor, or give it to thehelperservice 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
dialoutgroup does not matter here.
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.
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 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.
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-interfaceRun every docker compose and docker build command below from this directory, and create .env here.
The demo image runs Kismet with a fake board, and works on Docker Desktop:
docker compose --profile demo up demoThen 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 kismetOr 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.
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.
-
Create
.envnext tocompose.yamlwith your login (change the password):KISMET_USER=admin KISMET_PASSWORD=choose-a-long-password
-
Start Kismet:
docker compose up -dThe 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=0in.envskips 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 afterdocker 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:2501only.To use an image you built yourself, build it from the project directory and write
esp32c5-kismetin place ofghcr.io/oshri-almog/esp32c5-kismet:latestin the command above:docker build -f docker/Dockerfile -t esp32c5-kismet .
This container needs no device rules, since it opens no board itself.
-
Create an API key with the
datasourcerole, 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.cmdIt prints the key, 32 hex characters.
duration0 means it never expires. The key is kept in thekismet-homevolume, so it survives restarts and updates. Kismet's web UI can also create keys, under Settings → API Keys.Note: In Windows PowerShell 5.1,
curlis another command (Invoke-WebRequest), and PowerShell strips the double quotes inside the JSON beforecurl.exesees them. Use Git Bash or the web UI for this step, orcurl.exewith 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.
-
On Windows, install the Python remote helper as Install on Windows describes, and list the boards:
python -m esp32c5_kismet.remote --list
-
Start the helper with the key and a board. Change
COM14to 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,localhostresolves to the IPv6 address::1first, and in the tests against Kismet in WSL2 each attempt on::1cost about 2 s; Docker Desktop was not measured separately. The current Python remote helper tries127.0.0.1first when you writelocalhost, 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
-
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.
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.
-
Port clash. A Kismet in WSL and one in Docker Desktop both want
localhost:2501. Give the container another host port withKISMET_PORT=2502in.env(or-p 127.0.0.1:2502:2501ondocker 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 adockercommand reaches the container as aC:/...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.
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
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.
-
Make port 2501 of the server reachable from this machine. The
kismetservice publishes it on all interfaces. -
Create an API key on the server with the
datasourcerole. 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.cmdIt prints the key, 32 hex characters.
-
On the machine with the boards, get the project files (step 1 above) and create
.envwith 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 inESP32C5_MODE, as in thekismetservice. -
Start the helper. Name the service:
--profile helper upon its own would also start thekismetservice, and the two would compete for the same boards.sudo docker compose --profile helper up -d helper
Without Compose, give the same settings with
-einstead 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 helperTo run an image you built yourself (Building the image yourself), write
esp32c5-kismetin place ofghcr.io/oshri-almog/esp32c5-kismet:latest. The wordhelperafter the image name picks the container's role. -
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 logsshows 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_contextline 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 addsINFO: 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_esp32c5tries 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 loggedFATAL: Datasource could not connect websocketand 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=0turns that off). With no board and noKISMET_SOURCES, the container exits withhelper: no ESP32-C5 board found and KISMET_SOURCES is empty, and Docker restarts it (restart: unless-stopped) until a board is there. A source inKISMET_SOURCESthat names no port (a bareesp32c5) stops before it connects when there is no board or more than one, withCould not probe local source prior to connecting to the remote hostand 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 logsError connecting new remote source <name> (<uuid>) - cannot open /dev/ttyACM9: No such file or directoryand 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(orsudo docker restart esp32c5-helper). A board named inKISMET_SOURCESis 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_USERandKISMET_PASSWORDalso work. The helper sends them in anAuthorizationheader, so any character goes,&, spaces and%41included (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 andhelper: 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_esp32c5in its environment (KISMET_CAP_APIKEY, orKISMET_CAP_USERandKISMET_CAP_PASSWORD), not on its command line. Withdocker run, the key is on thedocker runcommand 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
kismetservice;compose.yamlsets 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_SOURCESand runsudo 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 dropchannel_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(orsudo 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 reasonwebsocket 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.
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.
sudo docker compose build
sudo docker compose up -dThe 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 demoOn 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 helpersudo 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.
| 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.
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.
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.logThe 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.
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 | amd64 | arm64 (the Pi's build) |
|---|---|---|
Kismet image (:latest) |
177 MB | 140 MB |
Demo image (:demo) |
227 MB | 177 MB |
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 -dOn 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 -dOn 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 helpergit pull # with an image you build
sudo docker compose --profile helper build helper
sudo docker compose --profile helper up -d helperWithout 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-kismetFor 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.
| 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 pruneordocker system prune --volumes. They delete every unused unnamed volume on the machine, not only this project's (with-a,docker volume prunedeletes unused named volumes too, such askismet-data). Remove this project's containers and volumes by name, as in the table.
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.
- Docker Reference: every variable, path, port, tag and build option.
- Try It Without Hardware: the demo in detail.
- Remote Capture: helpers on other machines, API keys and transports.
- Kismet Configuration: changing Kismet's settings, including in the container.
- Install on Raspberry Pi and Building Kismet with ESP32-C5 Support: Kismet without Docker.