# Jocasta > A network inventory that keys each device by its hardware address. Jocasta is a single-binary network inventory with a web UI, a JSON API and an MCP server. Configuration is a YAML file or JOCASTA_ environment variables; the configuration reference lists every setting. # Get started # Jocasta Jocasta keeps an inventory of the devices on your network. It logs every change. It is one binary, with a web UI, a JSON API and an MCP server. > [!WARNING] > > **Pre-1.0** > > A release can change the config format, the database schema or the API with no upgrade path. Pin a version. Before you move to a new version, read the [upgrade notes](). To read the docs for your release, select it in the version menu at the top. ## How it works 1. Jocasta sweeps the networks you list, every 5 minutes. 2. It records each device under its hardware address, because an IP address is only a lease. 3. It logs what changed. To see other VLANs, add your router. To see traffic, add flow exports. To see lookups, add your DNS resolver. ## Start here - **[Your first inventory]()** Run Jocasta, sweep your network, name a device, and get a message when it goes quiet. 10 minutes. - **[How Jocasta works]()** Devices, scans, sources and presence. - **[Plugins]()** Routers, flow exports, DNS resolvers. - **[Notifications]()** ntfy, signed webhooks, chat services. - **[MCP server]() and [JSON API]()** For agents and scripts. - **[Configuration reference]()** Every setting, its type and default. ## Get help [GitHub issues](), or [Fix common problems](). For language models: [`llms.txt`]() lists every page as Markdown, and [`llms-full.txt`]() holds them all in one file. # Your first inventory In this tutorial, you run Jocasta in a container and sweep your network. Then you name a device and get a message on your phone when it goes quiet. It takes about 10 minutes. You need a Linux machine with Docker on the network. You also need the network's prefix. To find it, run `ip -4 addr`. For example, `inet 192.0.2.20/24` means `192.0.2.0/24`. ## 1\. Write the config Create `jocasta.yaml` in an empty directory: ``` networks: - "192.0.2.0/24" server: auth: cookie_secure: false # sign in over plain HTTP ``` ## 2\. Start Jocasta ``` docker run -d --name jocasta --network host \ -v jocasta-data:/data \ -v ./jocasta.yaml:/data/jocasta.yaml:ro \ ghcr.io/pushkar-anand/jocasta:latest ``` With host networking, the sweep can read hardware addresses. ## 3\. Create the admin account Open `http://:8080`. Enter a username and a password of 8 or more characters, and select **Create admin account**. Until this account exists, anyone who reaches the page can create it. So do this step immediately. ## 4\. Look at the first sweep The first sweep runs when Jocasta starts. Open **Devices**. Each row is one device, with its addresses, vendor and name. A row with no vendor is usually a phone or a laptop. These devices make up their hardware address. A row with no name is a device that did not answer a name lookup. ## 5\. Name a device 1. Select a device you recognise. 2. Select **Edit details**. 3. Set **Label** to `Office printer` and **Group** to `Office`. 4. Select **Save**. The device list shows the new label. ## 6\. Watch it On the device's page, select **Watch**. A watched device goes quiet 15 minutes after the last scan that saw it. To test this, turn the device off for 20 minutes. Then open **Events**. ## 7\. Get a message on your phone 1. Install [ntfy]() on your phone and subscribe to a topic. Topics are public. So use a name that nobody can guess, such as `jocasta-k3v9q2`. 2. Add it to `jocasta.yaml`: ``` notify: phone: ntfy: url: "https://ntfy.sh/jocasta-k3v9q2" ``` 3. Restart Jocasta: `docker restart jocasta`. 4. Open the account menu, top right, and select **Notifications**. 5. Under `phone`, tick **Watched device went quiet** and **Watched device came back**. 6. Select **Save**. 7. Select **Send test**. Your phone gets the message "Jocasta test". ## Next - [How Jocasta works]() - [Connect your router]() for devices on other VLANs - [Serve over HTTPS]() # How Jocasta works ## Devices Jocasta identifies a device by its hardware (MAC) address. An IP address is only a lease. So your label and notes stay with the device when its IP address changes. A sweep reads hardware addresses only on its own segment. Jocasta knows a device on another segment by its IP address until a router reports its hardware address. The device page shows which: **by hardware address** or **by IP address**. If a record for that hardware address already exists, Jocasta merges the two and keeps your labels. Phones, tablets and laptops make up a new hardware address for each Wi-Fi network. Such an address names no vendor, and each new address is a new device. Jocasta deletes a device 90 days after a scan last saw it, unless you gave it a label, group, type or notes, or ignored or watched it. If a device stops answering on an address, Jocasta removes that address after the device answers on a different address in the same network for 6 hours. ## Networks A network is an IP prefix, such as `192.0.2.0/24`. Jocasta sweeps the prefixes under `networks`. A [router plugin]() adds the segments that the router serves, with their names and VLAN tags. ## Scans A scan is one recorded reading: - **Sweep.** An ICMP echo to every address in `networks`, then a name lookup for each address that answers. Every 5 minutes. - **Port scan.** A TCP connection to a list of ports on every known address. Off until you set `scan.ports.enabled: true`, then every 6 hours. Jocasta also scans a new device when a sweep or a router read finds it. - **Plugin read.** A router's ARP and DHCP tables, read with each sweep. A DNS resolver's query log, read every minute. The Scans page lists them. To start a scan yourself, use [`jocasta scan`](), [`jocasta ports`]() or [`jocasta plugin run`](). ## Sources Each scan records its facts under a source. The sweep's source is `sweep:`. A plugin's source is its instance name, such as `gateway`. The device page lists the name that each source gives the device. If two sources give different names, the source with the higher standing wins. For example, a static DHCP lease wins over a name that the device announces. See [How names are chosen](). ## Presence A device is seen recently if a scan saw it in the last 15 minutes. If not, it is quiet. Both states come from past scans. A page that you open does not start a scan. A watched device logs an event when it goes quiet and when it comes back. Other devices do not, because phones and laptops would fill the log. See [Get told when a device goes quiet](). ## The change log The Events page lists every change: new devices, addresses added and removed, new names, ports opened and closed, type changes, and your own edits. [Notifications]() send these events to other services. ## What you own The label, group, type, notes, ignored flag and watched flag are yours. Scans never change them. See [Label, group and ignore devices](). If you do not give a device a type, Jocasta guesses one. See [How a device gets its type](). ## Retention | Record | Kept for | Setting | | --- | --- | --- | | Change log and scans | 90 days | `retention.history` | | Hourly traffic totals | 30 days | `retention.traffic` | | Hourly DNS lookup counts | 30 days | `retention.dns` | | Devices you never touched, after last seen | 90 days | `retention.devices` | `0s` keeps a record forever. ## Limits - An open port is a port that accepted a TCP connection. Jocasta guesses the service name from the port number. - If a device has no recorded ports, port scanning can be off. - Traffic shows only what passed the router while flow export was on. - Jocasta names an internet address after the network that announces it. For a cloud address, that is the provider. # Tour the web UI This page shows each page of the web UI. The pages read the same data as the JSON API. The Overview, Map and Topology pages refresh themselves. The screenshots below use synthetic data: addresses from RFC 5737, hardware addresses from RFC 7042, and invented names. The internet peers are well-known public services, so Jocasta can find their organisations. ## Overview Counts for each network, a presence summary, the last sweep, and recent changes. ## Device list Filter by group, network or presence. Search by name, address or vendor. The list hides ignored devices, unless you ask for them. ## Device page Everything that Jocasta knows about one device: - Identity: what Jocasta found the device to be. It also shows whether Jocasta knows the device by its hardware address or only by its IP address. - What you call it: label, group, type and notes. No scan or plugin changes these. - Connected: the path from the internet to the device, through each router, switch and access point. Each step shows the port or Wi-Fi network, the band and the VLAN. If the device connects directly to a port that Jocasta reads, the page shows the link rate. For a Wi-Fi client, it shows the rate in each direction and the signal. If a wired link runs slower than both ends can, the page tells you. Show on topology opens the Topology page on the device. - Addresses: every address that the device had, and the network of each address. - Ports: the TCP ports that the device listens on, and the ports that it closed since. This section shows only after a port scan reached the device. See [CLI](). - Sources: the name that each source, such as the sweep or the router, gives the device at each address. It also shows when Jocasta last heard from each source. - History: the entries for the device in the change log. ### Talks to If traffic collection is set up, this section shows who the device exchanged data with. You can show the last 24 hours, 7 days or 30 days. See [NetFlow](). The top of the section shows the data received and sent. It also shows the number of peers on your network and of organisations on the internet. Below that, there is one tab for each network segment and one for the internet. Each tab has a count. The busiest tab opens first. On the tab of a segment, each peer is one row, for all the services on which the device reached it. If the peer is a device, the row links to its page. Addresses that no device has collapse into one row for each subnet. The Internet tab groups addresses by the organisation that announces them. Use the search box and the filters to show one service only, or to search by name, address or organisation. Each row shows who opened its connections. "→ 12" means that the device opened 12. "← 3" means that the peer opened 3. A row can show both. The direction filter shows one side only. Something on the internet can try to reach the device without exchanging data, such as a knock on a forwarded port. In that case, a note shows how many addresses tried, from how many organisations, and on which ports. Jocasta counts tries on the outside address of the router under the router. Jocasta learns that address from the flows themselves. The device can start connections that carry no data, such as a refused port, a port that nobody answered, or a ping. Jocasta counts these at the top and does not show them in the tabs. To add them, tick "Show tries with no data". Each try then goes in the row of its peer, with the number that got an answer and the ports tried. If the tries add up to probing of the network, a notice at the top tells you. The Broadcasts tab lists the packets that the device sent to a whole subnet or multicast group. Most of these are discovery protocols, such as mDNS, SSDP, DHCP or a sync tool that looks for its peers. Jocasta guesses the names from the port and group. ## Traffic page If traffic collection is set up, this page shows a summary of the whole network. You can show the last 24 hours, 7 days or 30 days. These are its cards: - **Probing your network**: devices that tried 20 or more addresses within an hour without exchanging data. Also devices that tried 20 or more ports on one address in this way. A device that you run scans from also shows here. - **Reached from the internet**: each device and service that something on the internet connected to. This card leaves out a service that uses the same port at both ends, such as a VPN server on IKE. Its clients look like replies. - **Probed from the internet**: connections from the internet that carried no data, for each device and for the outside address of the router. - **Busiest devices** and **Top internet destinations**. Each organisation opens to show the devices behind it. - **New this week**: organisations that a device first exchanged data with in the last 7 days. There is a tab for each network segment. A tab shows every card for the devices of that segment only, with its totals at the top. You can also show one group only. The outside address of the router can itself be private. This happens when the carrier-grade NAT of the ISP, or another router, is in front of it. If so, the page tells you. Then nothing on the internet can connect in over IPv4, so the two internet cards stay empty. ## Map If traffic collection is set up, the Map shows the traffic of the last hour as a tree. It redraws every minute, because Jocasta records new traffic every minute. - The router is in the middle, with a hub for each network and one for the internet. - Devices and organisations spread out around their hub. The Map shows the 60 busiest devices and the 12 busiest organisations. - A branch is thicker when more traffic passed along it. It moves if something under it was active in the last 15 minutes. - A faint line joins each device to what it exchanged traffic with: an organisation, or a device on another network. Traffic between two devices on the same network never passes the router, so the Map does not show it. - The legend in the top right gives the colour of each network, with its device count. Point at a network to show its devices. Click a device or organisation to highlight its lines. The Map then names the services on each line ("https", "mqtts", "port 123 UDP") and dims everything else. Click the device again to open its page. To go back, click the empty map or press Escape. To zoom, scroll or pinch. To pan, drag. To zoom in on a spot, double-click it. The search box dims everything that does not match. Enter zooms to the first match. The Map keeps the view, the search and the place of each device when it redraws. ### World The World tab shows the same internet traffic of the last hour on a map of the world. - A country is darker when more traffic went to addresses registered there. A marker pulses on the countries that were active in the last 15 minutes. - A line goes from the country of your network to each of the others. It is thicker when it carried more traffic, and it moves while it is active. - Click a country to see the devices and organisations behind its traffic. The search finds countries by name. The country of your network comes from the outside address of the router. If that address is private, it comes from `location.country`. See [NetFlow](). A country is where an address is registered. For a big provider, this is often its home country. The server itself can be in a different country. ## Topology The Topology page shows the network from the internet down. It redraws every minute. See [Show what is plugged in where](). - The internet is at the top, with the router below it. Each switch and access point hangs from the port that it is connected to. A thick line is a trunk. A thin line carries one VLAN. A dashed line is Wi-Fi. - Under each box, the page groups its wired devices by VLAN, with the port of each device on the right. It groups its Wi-Fi devices by network, with the band of each device. - A solid box is a device that Jocasta reads. A dashed box is a device that only announced itself to a device that Jocasta reads. Jocasta infers a dotted box when several devices share one port behind it. If one of these devices is a hypervisor, the box is that device. The others are its virtual machines. To mark a host that Jocasta did not recognise, set its type to Hypervisor on its page. If most devices in a dotted box have the hardware address of a virtual machine, the box has the label Hypervisor. - A quiet device stays where Jocasta last saw it, dimmed. Some devices are seen recently, but no router, switch or access point saw them on a port. The page lists them under the tree. - The legend in the top right gives the colour of each VLAN, with its device count. Point at a VLAN to show its devices. Click a device to highlight the path from the internet down to it. Click it again to open its page. Search, zoom and pan work as on the Map. ## Network page One segment, its VLAN tag and name, and the devices on it. ## Events The full change log. ## Light theme A toggle in the header changes the theme. Each browser keeps its own choice. # Install For a guided first run, see [Your first inventory](). ## Container image Images for amd64 and arm64 are on the GitHub Container Registry. Each image has the tag `latest` and a release tag, such as `:v0.11.1`. The image is distroless and does not run as root. It listens on `0.0.0.0:8080`, keeps its database in `/data`, and reads `/data/jocasta.yaml`. ``` docker run -d --name jocasta --network host \ -v jocasta-data:/data \ -v ./jocasta.yaml:/data/jocasta.yaml:ro \ ghcr.io/pushkar-anand/jocasta:latest ``` ``` services: jocasta: image: ghcr.io/pushkar-anand/jocasta:latest network_mode: host restart: unless-stopped volumes: - jocasta-data:/data - ./jocasta.yaml:/data/jocasta.yaml:ro volumes: jocasta-data: ``` Use host networking. On a bridge network (`-p 8080:8080`), the sweep finds hosts but not their hardware addresses. Also, mDNS, DNS-SD and SSDP names do not arrive. A [router plugin]() can supply the hardware addresses. Neither mode needs extra capabilities. ## Prebuilt binaries Archives for Linux, macOS and Windows, on amd64 and arm64, are on the [releases page](). A `checksums.txt` file is with them. 1. Extract `jocasta` from the archive for your system. 2. Put a `jocasta.yaml` next to it. 3. From that directory, run `jocasta serve`. It does not need root. It listens on `localhost:8080`. To reach it from other machines, set `server.host: 0.0.0.0`. ## From source You need Go 1.27 or newer. ``` git clone https://github.com/pushkar-anand/jocasta.git cd jocasta make build # -> bin/jocasta make docker # -> jocasta:latest ``` ## First run On the first visit, Jocasta asks you to create the admin account. Until this account exists, anyone who reaches the page can create it. So create it immediately. To add more users, see [Manage users and sign-in](). To sign in over plain HTTP, set `server.auth.cookie_secure: false`. See [Serve over HTTPS](). Jocasta runs database migrations when it starts. Before you upgrade, see [Back up and upgrade](). # Configure You must set only `networks`, the prefixes to sweep. All other settings have a default. [`jocasta.example.yaml`]() lists every setting, with its default and a line about what it does. The [configuration reference]() gives the type, allowed values and environment variable of each setting. Jocasta reads settings in this order. Each one overrides the one before it: 1. built-in defaults 2. a YAML file, `jocasta.yaml` in the working directory, or the path given to `--config` 3. environment variables prefixed `JOCASTA_` In an environment variable, a double underscore separates levels. A single underscore stays part of the key: ``` JOCASTA_SCAN__DEVICES__INTERVAL=10m # scan.devices.interval JOCASTA_PLUGINS__ROUTEROS__GATEWAY__PASSWORD=change-me ``` Jocasta shows times in the time zone of the server. In a container, this time zone is UTC. To see local times, set your time zone. This setting changes only how Jocasta shows times. It always stores them in UTC. ``` location: timezone: "Australia/Sydney" # an IANA zone name ``` Jocasta deletes a device when no scan has seen it for 90 days. It does not delete a device that you labelled, grouped, typed, noted, ignored or watched. Each time a phone changes its privacy address, it leaves one such device. To change the window, set `retention.devices`. To keep every device, set it to `0`: ``` retention: devices: "2160h" ``` Do not commit a `jocasta.yaml` that holds real addresses or credentials to a repository. ## What to turn on next - **Port scanning**: to probe every known address on your networks for open TCP ports at a set interval, set `scan.ports.enabled: true`. Jocasta also probes each address on the TCP ports on which its device advertises a service. For a single scan, see [CLI](). - **Routers**: devices on every VLAN, and what is connected where. See [Plugins](). - **Traffic**: who each device talks to, from the flow exports of your router. It adds a Traffic page and a live Map. See [NetFlow]() and [Tour the web UI](). - **DNS lookups**: what each device looks up, from the query log of your resolver. See [Record what devices look up](). - **Notifications**: new devices, opened ports and other changes. Jocasta sends them to ntfy, a signed webhook, or any service that accepts an HTTP request. See [Notifications](). - **MCP server**: AI agents can query the inventory. See [MCP server](). - **JSON API**: under `/api`, for scripts and dashboards. See [JSON API](). # Devices # Label, group and ignore devices These fields are yours. Scans never change them. They stay with the device when its address or name changes. To edit them, you need an Admin or Editor account. - **Label**: the name that Jocasta shows everywhere. Up to 200 characters. - **Group**: puts devices together, such as `Office`. You can filter the device list by group. Up to 100 characters. - **Type**: sets the icon. **Auto** keeps Jocasta's guess. See [How a device gets its type](). - **Notes**: free text. Up to 2,000 characters. - **Ignored**: hides the device from lists and counts. - **Watched**: logs an event when the device goes quiet or comes back. See [Get told when a device goes quiet](). ## Edit a device 1. In **Devices**, select the device. 2. Select **Edit details**. 3. Change the fields. 4. Select **Save**. If you leave a field empty, Jocasta clears it. The change log records each edit. To change only the label and group, select the pencil at the end of the row in the device list. ## Ignore a device Ignore a device that you know and never want to see, such as a neighbour's printer: 1. In **Edit details**, tick **Ignore this device**. 2. Select **Save**. The sweep still records the device. The port scan skips it, and notifications leave it out. To see it again, select **Show ignored** in the device list filters. ## From a script or an agent Use `PATCH /api/devices/{id}` in the [JSON API](), or the `update_device_curation` tool in the [MCP server](). Both need a `read_write` token. The MCP prompt `triage_devices` suggests a label, group and type for each device that has none. # How names are chosen More than one source can name a device. Jocasta shows the name from the source with the highest rank in this list. If you give a device a label, Jocasta shows the label instead of all of these names. See [Label, group and ignore devices](). 1. Reverse DNS. Jocasta looks it up for each address that a sweep finds. 2. A static DHCP lease on your router. 3. mDNS. Jocasta asks each address that the sweep finds and that has no reverse DNS name. 4. A dynamic DHCP lease on your router. 5. NetBIOS. Jocasta asks each address that the sweep finds and that has no name from reverse DNS or mDNS. 6. DNS-SD. If an address still has no name, Jocasta uses the name that it gives the services it advertises. 7. SSDP. If an address still has no name, Jocasta uses the label in its UPnP description. If two names of the same rank are different, the device keeps its current name while a source still gives that name. If no source gives it, the device takes the name that Jocasta heard last. A device can have several addresses, such as a router with an address on each VLAN. Each address can give a name. The device keeps one of these names. It does not change to the name of the address that Jocasta read last. The device page lists the name that each source gives for each address. Jocasta does not use the reverse DNS names that systemd-resolved makes up, such as `_gateway`. When a sweep has a name for a device, a lookup with a lower rank does not replace it. The device keeps that name until a lookup with the same or a higher rank gives a different name. ## mDNS mDNS names phones, TVs, printers and computers that reverse DNS does not know, such as `living-room-tv.local`. Jocasta sends one query to each such device, on UDP port 5353. This query does not need host networking or multicast. As the mDNS standard asks, most devices answer only a query from their own segment. So devices on other segments rarely get an mDNS name. To turn mDNS off, set `scan.devices.resolve_mdns: false`. It does not depend on `scan.devices.resolve_names`. If reverse DNS is off, no device has a reverse DNS name. Then Jocasta asks every device that answers the sweep over mDNS. ## NetBIOS NetBIOS names Windows computers and devices that run Samba, such as a NAS. A NetBIOS name, such as `DESKTOP-4F2K`, has at most 15 characters. Jocasta sends one query, on UDP port 137, to each device that still has no name. Windows answers only when file and printer sharing is on. By default, it answers only a query from its own segment. To turn NetBIOS off, set `scan.devices.resolve_netbios: false`. ## DNS-SD DNS-SD names TVs, speakers, printers and other devices that advertise their services over mDNS but do not answer an mDNS query for their own name. After each sweep, Jocasta asks the multicast group `224.0.0.251:5353` which service types are on the segment. Then it asks which devices offer each type. A Google Cast device gets the name that its owner gave it in the Home app, such as `Living Room TV`. Any other device gets the name that most of its services share. Jocasta does not use a name that is only a serial number or another ID. Jocasta binds UDP port 5353 to hear the answers. It shares this port with any mDNS responder on the same machine, such as Avahi. Like SSDP, DNS-SD reaches only Jocasta's own segment, and only with host networking. To turn it off, set `scan.devices.resolve_dnssd: false`. Jocasta also records the services that each device advertises, with or without a name. The Ports table on the device page lists the services that the latest sweep heard. The port scan probes each advertised TCP port, so the table shows whether the port answers. The scan also probes every port that it recorded as open. So when a service moves to a new port, its old port closes. A port that the scan probes only because a device advertised it opens and closes without an event. This is because a phone advertises on a new random port each time. The classifier reads every service that a sweep heard within `retention.history`. So if one sweep misses an answer, the device's type does not change. For example, a print service marks a printer, and the Android TV remote service marks a TV. If no sweep heard a service within `retention.history`, Jocasta deletes it. ## SSDP SSDP names TVs, speakers, media players, printers and routers that no other source names. The name is the label that the device shows its owner, such as `Living Room TV`. If a sweep leaves a device without a name, Jocasta sends one SSDP search to the multicast group `239.255.255.250:1900`. A device that answers points to its UPnP description. For each device that still has no name, Jocasta gets that description and takes the `friendlyName` from it. It gets the description only from the address that answered, over plain HTTP. It reads at most 64 KB and waits at most 2 seconds. The search reaches only Jocasta's own segment, and only with host networking, because a bridge network does not pass multicast. To turn SSDP off, set `scan.devices.resolve_ssdp: false`. # How a device gets its type After each scan, Jocasta guesses the type of each device from these facts: - the vendor of its hardware address - its name - its open TCP ports - the DNS-SD services it advertises, and the model of a Google Cast device - whether it made up its hardware address - the name of its network, such as `IoT` - whether it has the `.1` address of its subnet ## Rules Each rule names a type and the facts it needs. For example, vendor Brother means a printer, and port 8123 means a Home Assistant hub. Of the rules that match, the rule with the most facts wins. If two rules have the same number of facts, the rule listed first wins. The device page shows the guess as **auto**, with a confidence: - **High**: the rule needs two or more facts, or two rules agree. - **Low**: the guess comes from one weak fact, such as the network's name, and nothing else. - **Medium**: all other cases. If a later scan changes the guess, the change log records **Type changed**. ## Set the type yourself 1. Open the device and select **Edit details**. 2. Select a type. 3. Select **Save**. Scans never change a type that you set. The device page still shows the guess. To use the guess again, select **Auto**. ## Ask why The `explain_device` tool of the [MCP server]() returns the facts, the rule that won, and every other rule that matched. ## Types Router, switch, access point, firewall, server, NAS, hypervisor, desktop, laptop, phone, tablet, printer, camera, TV, media player, speaker, voice assistant, game console, smart-home hub, smart-home device, wearable, VoIP phone. # Plugins # Plugins Plugins read what a sweep cannot reach: - A router gives the devices on every segment, and what is connected where. - A flow export gives who talks to whom. - A DNS resolver gives what devices look up. | Plugin | Devices and networks | Topology | Traffic | DNS lookups | | --- | --- | --- | --- | --- | | [MikroTik RouterOS]() | Yes | Yes | Through [NetFlow]() | | | [OpenWrt]() | Yes | Yes | Through [NetFlow]() | | | [NetFlow]() | | | Yes | | | [Technitium]() | | | | Yes | | [Pi-hole]() | | | | Yes | | [AdGuard Home]() | | | | Yes | ## Configure a plugin Each plugin is a block under `plugins`. The first key is the plugin kind. The second key is an instance name that you choose: ``` plugins: routeros: gateway: # instance name, shown as the source enabled: true host: "192.0.2.1" # ... ``` The instance name is the [source]() of the facts that the plugin records. Routers and DNS resolvers share one set of names. For example, a `routeros` instance and a `technitium` instance cannot both be `gateway`. A `netflow` listener can have the same name as its router. Put passwords in the environment, such as `JOCASTA_PLUGINS__ROUTEROS__GATEWAY__PASSWORD`. See [Configure](). The [configuration reference]() lists every setting. A source with `ssl: false` sends its password as plain text. Jocasta shows a warning when it starts. ## Check a plugin ``` jocasta plugin run gateway ``` This command reads the source once and prints what the source reports. It changes nothing. See [CLI](). ## Topology The Topology page comes from a MikroTik or OpenWrt router. To see the port of each device, also read the switches and access points. See [Show what is plugged in where](). ## DNS resolvers Technitium, Pi-hole and AdGuard Home give the lookups of each device. See [Record what devices look up](). # MikroTik RouterOS Jocasta reads RouterOS over its REST API: ``` plugins: routeros: gateway: # instance name, shown as the source enabled: true host: "192.0.2.1" user: "jocasta" # a read-only user is enough password: "change-me" # or JOCASTA_PLUGINS__ROUTEROS__GATEWAY__PASSWORD ssl: true insecure: true # RouterOS serves a self-signed cert unless you import one ``` Before you start the server, test the source with `jocasta plugin run gateway`. See [CLI](). ## Topology Each RouterOS table adds this to the [Topology page](): - **Bridge host table**: the port behind which each hardware address is. If the bridge has VLAN filtering on, it also gives the VLAN. - **Neighbour discovery** (MNDP or LLDP): the name of the switch or access point on each port. By default, RouterOS runs it on the `LAN` interface list. See `/ip neighbor discovery-settings`. - **Wi-Fi registration table**: which devices are Wi-Fi clients, with their network, band, signal and rate in each direction. It comes from the wifi package or the wireless package. A router that manages access points with CAPsMAN also lists their clients. - **Ethernet monitor**: the link rate of each Ethernet and SFP port, and the fastest rate that both ends offer. To read the ports of a MikroTik switch or access point, add it as another instance with `topology_only: true`. See [Show what is plugged in where](). ## Export traffic flows To record who devices talk to, send Traffic Flow to the Jocasta host. Then set up the [NetFlow listener](). In this example, the Jocasta host is `192.0.2.10`: ``` /ip traffic-flow set enabled=yes interfaces=all /ip traffic-flow target add dst-address=192.0.2.10 port=2055 version=ipfix ``` ## All settings The [configuration reference]() lists every `plugins.routeros` setting, with its default and allowed values. # OpenWrt Jocasta reads OpenWrt over ubus, at `http:///ubus`. ubus is the JSON-RPC endpoint that LuCI uses. Every router with LuCI serves it, so you do not need to install anything. Jocasta reads the neighbour table, the DHCP leases, the static leases, and the interfaces with their VLANs. For the Topology page, it also reads the ports, the bridge table and the Wi-Fi clients. It changes nothing on the router. Give Jocasta its own login, which can only read. On the router: 1. Copy [`openwrt/jocasta.json`]() to `/usr/share/rpcd/acl.d/jocasta.json`. This file allows the reads that Jocasta makes and nothing else. The groups of LuCI do not allow reads of the bridge table. So without this file, root also cannot read it. 2. Hash a password for the login: ``` uhttpd -m 'change-me' ``` 1. Add the login to `/etc/config/rpcd`, with the hash from the last step: ``` config login option username 'jocasta' option password '$1$...' list read 'jocasta' ``` 1. Reload rpcd: ``` /etc/init.d/rpcd reload ``` Then add the router to Jocasta: ``` plugins: openwrt: gateway: # instance name, shown as the source enabled: true host: "192.0.2.1" user: "jocasta" password: "change-me" # or JOCASTA_PLUGINS__OPENWRT__GATEWAY__PASSWORD ssl: false # true once luci-ssl is installed insecure: false # true for uhttpd's self-signed cert ``` Test the source with `jocasta plugin run gateway`. If rpcd refuses the login, or the ACL file is missing, Jocasta reports rejected credentials. A name that you set under Network → DHCP and DNS → Static Leases ranks above the name that a device asks for. A static lease on RouterOS works the same way. Jocasta names a network after its OpenWrt interface, such as `lan` or `iot`. An interface on a VLAN device has the VLAN tag. ## Topology Each OpenWrt table adds this to the [Topology page](): - **Bridge table of the kernel**: the port behind which each hardware address is. Most current routers have a DSA switch. On a DSA switch, each switch port is a separate device, so the table names the port. An older router has a swconfig switch. On a swconfig switch, all switch ports are one device, so the table names only that device. rpcd reads at most 256 entries of the table. If a bridge has more, Jocasta shows the first 256 and logs a warning. - **bridge-vlan sections** of the network configuration: the VLANs on each port. - **Station list of iwinfo**: which devices are Wi-Fi clients, with their network, band, signal and rate in each direction. - **Device list of LuCI**: the link rate of each port. By default, OpenWrt runs no neighbour discovery. So an OpenWrt source names no neighbours. But if Jocasta also reads an OpenWrt access point, the router finds the access point among the addresses on its ports. To read the ports of an OpenWrt access point, add it as another instance with `topology_only: true`. See [Show what is plugged in where](). ## Export traffic flows To record who devices talk to, install softflowd and send its exports to the Jocasta host. Then set up the [NetFlow listener](). In this example, the Jocasta host is `192.0.2.10`: ``` opkg update && opkg install softflowd # OpenWrt 24.10 and older apk update && apk add softflowd # OpenWrt 25.12 and newer uci set softflowd.@softflowd[0].enabled='1' uci set softflowd.@softflowd[0].host_port='192.0.2.10:2055' uci set softflowd.@softflowd[0].export_version='9' uci set softflowd.@softflowd[0].track_ipv6='1' uci set softflowd.@softflowd[0].timeout='maxlife=300' uci delete softflowd.@softflowd[0].sampling_rate uci commit softflowd /etc/init.d/softflowd enable /etc/init.d/softflowd start ``` Each line changes a default. With the defaults, Jocasta does not see some of the traffic: - `export_version` 9 carries IPv6. The default, 5, cannot. IPFIX (`10`) also works. - `track_ipv6` records IPv6 conversations, not only IPv4. - `maxlife=300` reports a long download every five minutes. Without it, softflowd reports a conversation only when it ends. So a stream that runs all evening shows in the hour when it stopped. - When you delete `sampling_rate`, softflowd counts every packet. The default, `100`, counts one packet in 100. Jocasta scales sampled counts back up. But it can miss a conversation of only a few packets. softflowd watches `br-lan`, the default LAN bridge. To record another segment, such as a VLAN on `br-lan.20`, add a softflowd section for its interface. Give that section its own `pid_file` and `control_socket`. In `exporters`, list the LAN address of the router, because the exports come from that address. ## All settings The [configuration reference]() lists every `plugins.openwrt` setting, with its default and allowed values. # Show what is plugged in where The Topology page shows the network from the internet down. It shows the router, switches, access points, and each device on its port or Wi-Fi network. The data comes from the bridge and Wi-Fi tables of each [MikroTik]() and [OpenWrt]() router that Jocasta reads. If a switch or access point announces itself to the router, the page shows it with the devices behind it. To see the switch port of each device, also read the switch, with `topology_only: true`: ``` plugins: routeros: gateway: enabled: true host: "192.0.2.1" # ... switch_core: enabled: true host: "192.0.2.2" user: "jocasta" password: "change-me" topology_only: true # ports only; the router lists the devices ``` A `topology_only` source finds no devices. So its management address cannot change the name of a network. Jocasta reads every RouterOS and OpenWrt source at each `scan.devices.interval`. `jocasta plugin run switch_core` prints the ports of the switch, the addresses on each port, and the neighbours of the switch. For what the tables of each router add, see [RouterOS]() and [OpenWrt](). For how to read the page, see [Tour the web UI](). # NetFlow Jocasta can record which devices talk to each other, and to where on the internet. It reads the flow records that your router exports (NetFlow v5, v9 or IPFIX). It keeps hourly totals for each device for `retention.traffic`, which is 30 days by default. It does not store single connections. ``` plugins: netflow: gateway: enabled: true listen: ":2055" exporters: - "192.0.2.1" # the router's address; anything else is dropped ``` You must set `exporters`. Jocasta reads only datagrams from the listed addresses. A device on the same network can forge such an address. So bind `listen` to an address that only the router reaches. Or, in your firewall, allow the port only from the router. With host networking, no more setup is necessary. On a bridge network, publish the port as UDP: `-p 2055:2055/udp`. ## Set up the router Send the flow export of the router to the Jocasta host and port: - [MikroTik RouterOS](): Traffic Flow, as IPFIX. - [OpenWrt](): softflowd, as NetFlow v9. Export from one router only. If two routers see the same conversation, both report it, and Jocasta counts it twice. Jocasta shows an internet address by the organisation that announces it. It puts the address on the map by the country where it is registered. For this, it uses [IP to ASN data]() and [IP to Country data]() by [DB-IP](), licensed under [CC BY 4.0](). The world map draws a line from the country of the network to each country that it talked to. Jocasta finds the country of the network from the outside address of the router. This address is private if the ISP puts the router behind carrier-grade NAT. In that case, set the country yourself, by its two-letter code. Jocasta then uses this country, whatever the outside address is: ``` location: country: "AU" ``` Jocasta uses only the country, to start the lines from its middle. It does not ask for or store a more exact location. ## All settings The [configuration reference]() lists every `plugins.netflow` setting, with its default and allowed values. # Record what devices look up Jocasta reads the query log of [Technitium](), [Pi-hole]() v6 or [AdGuard Home](). For each device, it keeps hourly counts of each name that the device looked up, blocked or not. If traffic collection is on, the traffic of a device names each address after the names that the device looked up. | | Technitium | Pi-hole | AdGuard Home | | --- | --- | --- | --- | | Answer addresses, to name traffic | Yes | No | Yes | | Read-only login | Yes | No | No | Jocasta reads each resolver every minute, up to 20,000 lookups each time. It keeps the counts for `retention.dns`, which is 30 days by default. ## Point devices at the resolver Jocasta counts a lookup against the address that asked. If the router forwards the lookups of its clients, Jocasta counts them all against the router. So in the DHCP settings of the router, give out the address of the resolver as the DNS server. ## Check it ``` jocasta plugin run resolver ``` This command prints the lookups of the last hour, for each device and name. # Technitium Jocasta reads the query log of a [Technitium DNS Server]() over its HTTP API. [Record what devices look up]() tells what Jocasta records. It also tells how to make each device ask the server directly. ``` plugins: technitium: resolver: enabled: true host: "192.0.2.53" port: 53443 ssl: true insecure: true # for the server's self-signed certificate token: "change-me" ``` To prepare the server: 1. In Apps, App Store, install Query Logs (Sqlite). Technitium keeps a query log only through an app like this one. 2. In Administration, Users, add a user for Jocasta. 3. In Administration, Permissions, give that user View on the Logs section and nothing else. 4. Sign in as that user. In the user menu, select Create API Token. 5. Put the token in `token`, or in `JOCASTA_PLUGINS__TECHNITIUM__RESOLVER__TOKEN`. If more than one app logs queries, set `log_app` to the app to read. Use the name that the Apps page shows. In a cluster, each node logs the lookups that it answered to its own disk. Add one entry for each node, at the node's own address. Do not use a virtual address that the nodes share, such as a VRRP address. It reaches only one node at a time, so Jocasta does not read the lookups of the other node. The primary node copies the token and the log app to every node. ## All settings The [configuration reference]() lists every `plugins.technitium` setting, with its default and allowed values. # Pi-hole Jocasta reads the query log of a [Pi-hole]() v6 over its API. [Record what devices look up]() tells what Jocasta records. It also tells how to make each device ask Pi-hole directly. Pi-hole does not log answer addresses, so its lookups do not name traffic. ``` plugins: pihole: resolver: enabled: true host: "192.0.2.53" port: 443 ssl: true insecure: true # for Pi-hole's self-signed certificate password: "change-me" ``` To prepare the server: 1. In Pi-hole's web interface, create an app password. 2. Put it in `password`, or in `JOCASTA_PLUGINS__PIHOLE__RESOLVER__PASSWORD`. Pi-hole has no read-only login, so the app password can also change settings. A new app password signs out every session. This includes the session of Jocasta. Jocasta signs in again on its next read. If the Pi-hole has no password, leave `password` empty. Keep the privacy level of Pi-hole at the level that shows everything. A level that hides domains or clients also hides them from Jocasta. Each read takes at most 20,000 lookups. If more lookups collect while Jocasta is stopped, it keeps the newest. It logs that it skipped the rest. ## All settings The [configuration reference]() lists every `plugins.pihole` setting, with its default and allowed values. # AdGuard Home Jocasta reads the query log of an [AdGuard Home]() over its API. [Record what devices look up]() tells what Jocasta records. It also tells how to make each device ask AdGuard Home directly. ``` plugins: adguard: resolver: enabled: true host: "192.0.2.53" port: 443 ssl: true user: "jocasta" password: "change-me" ``` Put the web interface login in `user` and `password`. You can also put the password in `JOCASTA_PLUGINS__ADGUARD__RESOLVER__PASSWORD`. AdGuard Home has no read-only users, so the login can also change settings. If the server has no login, leave `user` empty. In the settings of AdGuard Home, keep the query log on. Keep client address anonymisation off. An anonymised address names no device, so Jocasta counts its lookups against no device. Each read takes at most 20,000 lookups. If more lookups collect while Jocasta is stopped, it keeps the newest. It logs that it skipped the rest. ## All settings The [configuration reference]() lists every `plugins.adguard` setting, with its default and allowed values. # Notifications # Notifications Jocasta can send the changes from each scan to these destinations: - an [ntfy]() topic - your own URL, as a signed JSON POST - any service that accepts an HTTP request, such as Discord, Slack or Telegram. See [Send to another service](). - Under `notify` in the config file, add each destination by name. Give each destination one service block: ``` notify: phone: ntfy: url: "https://ntfy.example.com/jocasta" # the topic's address, as ntfy shows it token: "" # or JOCASTA_NOTIFY__PHONE__NTFY__TOKEN automation: webhook: url: "https://hooks.example.com/jocasta" secret: "change-me" # required; signs each request ``` A destination is on, unless it has `enabled: false`. 2. Restart Jocasta, so that it reads the change. 3. Open the account menu at the top right, and select **Notifications**. 4. For each destination, tick the kinds of change to send to it. 5. Select **Save**. 6. Optional: select **Send test**. The destination gets a message with the title "Jocasta test". Jocasta sends one message for each scan that changes something, such as "2 new devices on 192.0.2.0/24". In the first scan of a network, every device is new. So Jocasta sends only a count for that scan ("Found 42 devices"). Jocasta does not send changes to devices that you ignore, or your own edits. If Jocasta cannot deliver a message, it logs the failure and shows it on the Notifications page. It does not send the message again. The [configuration reference]() lists every `notify` setting, with its default and allowed values. # Get told when a device goes quiet Watch the devices that you want to hear about when they go away, such as a server, a NAS or a camera. A watched device goes quiet when no scan has seen it for longer than the online window. By default, the online window is 15 minutes. The device comes back when a scan sees it again. Phones and laptops go quiet and come back all day. So a device that you do not watch never logs these events. 1. On the device's page, select **Watch**. 2. Open the account menu at the top right, and select **Notifications**. 3. For a destination, tick **Watched device went quiet** and **Watched device came back**. 4. Select **Save**. A message has a title such as "1 device went quiet on 192.0.2.0/24". It has a line such as "NAS went quiet after 18 minutes without an answer". The change log records both kinds of event, even when no destination gets them. To list the watched devices that are quiet now, filter the device list to **Watched only** and **Quiet**. Only a scan that could see a device can mark it quiet. That scan is a sweep of a network where the device has an address, or a read of a router that lists the device. If no scan covers the network of a device, the device stays as it was last seen. If a router read fails before it ends, it marks no device quiet. To change how long a device can stay unseen, set the online window. The online window also sets which devices the device list counts as seen recently: ``` inventory: online_window: "30m" ``` # Check a webhook's signature A webhook request is a POST. Its JSON body has these fields: - `title` and `message`: the text of the notification. - `scan_id`: the scan the changes came from. It can be missing. - `events`: the changes. Each change has a `kind`. It can also have `device_id`, `device` and `change`. The request has two headers: - `X-Jocasta-Signature-256`: `sha256=`, then the hex HMAC-SHA256 of the raw body. The key is the `secret` of the destination. - `X-Jocasta-Delivery`: a random ID for this request. Log it. If you already handled a delivery with this ID, discard the request. To check a request, compute the HMAC over the body exactly as you got it. Do this before you parse the body. Then compare the result with the header in constant time. In Go: ``` mac := hmac.New(sha256.New, []byte(secret)) mac.Write(body) want := "sha256=" + hex.EncodeToString(mac.Sum(nil)) ok := hmac.Equal([]byte(want), []byte(r.Header.Get("X-Jocasta-Signature-256"))) ``` Reject the request when `ok` is false. # Send to another service The `http` service posts each message with a body that you write as a template. So it can reach any service that accepts an HTTP request. Find the recipe for your service below, and copy it under `notify`. The body is a [Go template](). The template gets these values and functions: - `.Title` and `.Body`: the title of the message, such as "2 new devices on 192.0.2.0/24", and its text, with one change on each line. - `.ScanID`: the scan that the changes came from. It is 0 for a test. - `.Events`: each change, with `.Kind`, `.DeviceID`, `.Device` and `.Change`. - `json`: writes a value as JSON, with quotes and escapes. Use it for every value in a JSON body, as `{{ json .Title }}`. Device names can contain quotes, and the body contains line breaks. - `truncate`: cuts a string to a number of characters and adds `…` at the end, as `{{ .Body | truncate 2000 | json }}`. A message lists up to 20 changes. Long device names can make it longer than the limit of a service. The service then returns an error. Jocasta sends the body as `application/json`, unless `headers` sets a different `Content-Type`. When Jocasta starts, it renders the template with two sample messages. One sample has a change. The other is the test message, which has no change. If the template does not parse, or its output is not valid JSON, Jocasta reports the problem and does not start. A device gives its own name. So its name can contain text that a chat service reads as a mention, such as `@everyone`, or as formatting. The Slack, Telegram, Gotify, Pushover and Apprise recipes send plain text. Discord has no plain-text mode. So its recipe turns mentions off, but a device name can still show with formatting. Discord, with the webhook URL from the Integrations settings of the channel: ``` discord: http: url: "https://discord.com/api/webhooks//" body: '{"content": {{ printf "%s\n%s" .Title .Body | truncate 2000 | json }}, "allowed_mentions": {"parse": []}}' ``` Slack, with an incoming webhook URL. The message goes in a `plain_text` block, which Slack shows as it is. The recipe also sends the title as `text`, which Slack shows in its notifications: ``` slack: http: url: "https://hooks.slack.com/services/" body: '{"text": {{ json .Title }}, "blocks": [{"type": "section", "text": {"type": "plain_text", "text": {{ printf "%s\n%s" .Title .Body | truncate 3000 | json }}}}]}' ``` Telegram, with the token of a bot and the chat to post in: ``` telegram: http: url: "https://api.telegram.org/bot/sendMessage" body: '{"chat_id": "", "text": {{ printf "%s\n%s" .Title .Body | truncate 4096 | json }}}' ``` [Gotify](), with the token of an application: ``` gotify: http: url: "https://gotify.example.com/message" headers: X-Gotify-Key: "" body: '{"title": {{ json .Title }}, "message": {{ json .Body }}, "priority": 5}' ``` [Pushover](), with the token of an application and your user key: ``` pushover: http: url: "https://api.pushover.net/1/messages.json" body: '{"token": "", "user": "", "title": {{ json .Title }}, "message": {{ .Body | truncate 1024 | json }}}' ``` For email, SMS, Microsoft Teams, Signal and [many more](), run an [Apprise API]() server next to Jocasta. In Apprise, save the Apprise URLs of the services under a key. Then post to that key: ``` apprise: http: url: "http://apprise.example.com:8000/notify/jocasta" body: '{"title": {{ json .Title }}, "body": {{ json .Body }}}' ``` Home Assistant, n8n and Node-RED accept any JSON, so they use the `webhook` service. For Home Assistant, add an automation with a webhook trigger. Then set the `url` of a webhook destination to the webhook of that automation. Home Assistant does not check the signature, but you must still set a secret. The automation reads the message as `trigger.json.title` and `trigger.json.message`. It reads each change under `trigger.json.events`. ``` home_assistant: webhook: url: "http://homeassistant.local:8123/api/webhook/" secret: "change-me" ``` # Integrations # MCP server Jocasta can serve the inventory to AI agents, such as Claude Code, over the [Model Context Protocol](). By default, the MCP server is off. An agent connected this way can answer questions such as: - What joined the network this week? - Which devices have SSH open? - What is 192.0.2.47, and when was it first seen? - Which devices talked to a new organisation today? With a `read_write` token, the agent can also label and group devices. It reads what past scans recorded. It never starts a scan. ## Turn it on 1. Set `server.mcp.enabled: true` in the config file, or `JOCASTA_SERVER__MCP__ENABLED=true`. While the MCP server is off, `/mcp` answers 404. 2. Create an API token. See [API tokens](). A `read` token gets only the tools that read the inventory. A `read_write` token also gets the tools that change it. The endpoint is `/mcp`. It uses the Streamable HTTP transport. Send the token as `Authorization: Bearer `. When a client connects, it finds the available tools and [prompts](<#prompts>). The devices themselves report their hostnames and vendor names. So any device on your network can choose what an agent reads in these fields. Give an agent a `read` token, unless it must change something. ## Connect a client The examples use `https://jocasta.example.test/mcp`. Replace it with the address of your server. **Claude Code** ``` claude mcp add --transport http jocasta https://jocasta.example.test/mcp \ --header "Authorization: Bearer " ``` **Codex** Codex reads the token from an environment variable. So the token is not in its config file: ``` export JOCASTA_TOKEN= codex mcp add jocasta --url https://jocasta.example.test/mcp \ --bearer-token-env-var JOCASTA_TOKEN ``` **Hermes Agent** 1. Put `JOCASTA_TOKEN=` in `~/.hermes/.env`. 2. Add the server to `~/.hermes/config.yaml`: ``` mcp_servers: jocasta: url: "https://jocasta.example.test/mcp" headers: Authorization: "Bearer ${JOCASTA_TOKEN}" ``` 3. In a running session, run `/reload-mcp`. **Other clients** Any client works if it supports Streamable HTTP and lets you set a request header. Some clients can only start a local server over stdio. For these clients, a bridge such as [`mcp-remote`]() can forward to the endpoint. ## Tools | Tool | What it answers | | --- | --- | | `list_devices` | Which devices match a search, group, network, type, presence (seen recently or quiet) or whether you watch them. | | `get_device` | Everything about one device: addresses, ports, advertised services, sources, your labels, and where it is plugged in and how fast. | | `explain_device` | Why a device has the class it has: the facts the classifier used, the rule that decided and every other rule that matched. | | `list_events` | What changed, across the network or for one device. | | `list_networks`, `get_network` | The network segments, and how many devices are on each. | | `list_groups` | The groups you filed devices under. | | `get_stats` | How many devices were seen recently, are quiet, are ignored or were discovered in the last 24 hours. | | `get_port_overview` | Open ports across the network, and the commonest services. | | `list_scans` | When scans ran, and what they found. | | `list_traffic` | Who devices exchanged data with. Needs traffic collection. | | `list_dns` | The names a device looked up, and which were blocked. Needs [lookups recorded](). | | `get_topology` | What is plugged in where, and how fast each link runs. Needs [topology set up](). | | `update_device_curation` | Sets a device's label, group, type, notes and ignored flag. `read_write` tokens only. | | `watch_device` | Watches a device, or stops, so you are told when it goes quiet or comes back. `read_write` tokens only. | ## Prompts A prompt is a saved procedure that you start yourself. The agent then does the procedure with the tools. In Claude Code, prompts show as slash commands, such as `/mcp__jocasta__weekly_report`. | Prompt | What it does | | --- | --- | | `triage_devices` | Finds the devices that need attention: unlabelled, doubtfully classified, or likely duplicates left by a randomised hardware address. Proposes a label, group and type for each. With a `read_write` token the agent applies the proposals you confirm; with a `read` token it only proposes. | | `weekly_report` | Summarises what changed over the last 7 days: new devices, devices gone quiet, organisations a device reached for the first time (when traffic collection is set up), port changes, identity changes, and what may need a look. Takes an optional `days` argument, from 1 to 90. It can reach back only as far as `retention.history` keeps the change log. | ## Errors An error is an [RFC 9457]() problem document, as in the HTTP API. A failed tool call returns this document as the text of an error result. The `instance` field has the tool name. Jocasta writes the cause of a 500 only to the server log. # JSON API Jocasta serves its inventory as JSON under `/api`, for scripts and dashboards. The API uses the same API tokens as the [MCP server](). ## Authenticate Create an [API token]() and send it with each request: ``` curl -H "Authorization: Bearer " http://:8080/api/devices ``` A `read` token can make only `GET` requests. A `read_write` token can also change devices. A token never expires, unless you selected 30 days, 90 days or 1 year when you created it. Jocasta refuses an expired token, as it refuses a revoked token. The expired token stays in the list until you revoke it. For health checks, `GET /api/livez` answers without a token. ## Endpoints | Request | Returns | | --- | --- | | `GET /api/livez` | Whether the server is up. | | `GET /api/stats` | Counts across the inventory. | | `GET /api/groups` | The device groups in use. | | `GET /api/devices` | The device list. | | `GET /api/devices/{id}` | One device. | | `PATCH /api/devices/{id}` | Changes a device's label, group, type, notes or ignored flag. Needs a `read_write` token. | | `PATCH /api/devices/{id}/watch` | Starts or stops watching a device. Needs a `read_write` token. | | `GET /api/devices/{id}/events` | A device's change log. | | `GET /api/devices/{id}/traffic` | Who a device talked to. Needs [traffic recorded](). | | `GET /api/devices/{id}/dns` | What a device looked up. Needs [lookups recorded](). | | `GET /api/events` | The change log across all devices. | | `GET /api/scans` | Past scans. | Jocasta returns an error as an [RFC 9457]() problem document. Its `title` and `detail` tell you what to change. ## Browser access A page on another origin can read `/api` only if `server.cors.allowed_origins` lists its origin. See the [configuration reference](). # Operate # Manage users and sign-in ## Roles | Role | Can | | --- | --- | | Viewer | Read the inventory. Create read tokens. | | Editor | Also edit devices. Create read/write tokens. | | Admin | Also add users and set what each notification destination gets. | The account that you create on the first run is the only admin. ## Add a user 1. Open the account menu, top right, and select **Users**. 2. Select **Add user**. 3. Enter a username (3 to 100 characters), a first password (8 or more), and a role. 4. Select **Create user**. 5. Give the password to the person. The web UI cannot yet change a password or role, remove an account, or reset a forgotten password. ## Sessions A sign-in lasts up to 7 days, or 24 hours without use. If you do not tick **Remember me**, it also ends when the browser closes. To change the limits, set `server.auth.session_lifetime` and `server.auth.idle_timeout`. After 10 wrong passwords, an account gets one try each minute. ## Two-factor authentication 1. Open the account menu and select **Security**. 2. Select **Turn on two-factor authentication**. 3. Scan the QR code with an authenticator app. 4. Enter the 6-digit code from the app. 5. Select **Turn on**. 6. Save the 10 recovery codes. Jocasta shows them only once. Each code signs you in once without the app. On the same page, you can make new recovery codes or turn two-factor off. Both actions ask for your password. ## API tokens Scripts and agents use a token to reach the [JSON API]() and the [MCP server](). 1. Open the account menu and select **API tokens**. 2. Select **Create a token**. 3. Enter a name. 4. Select **Viewer** (`read`) or **Editor** (`read_write`). 5. Select an expiry. By default, a token never expires. 6. Select **Create token**. 7. Copy the token. Jocasta shows it only once. A token cannot do more than its account. When you revoke a token, it stops working immediately. # Serve over HTTPS Jocasta serves only plain HTTP. Put a reverse proxy with a certificate in front of it. By default, the session cookie works only over HTTPS. So to sign in over plain HTTP from an address other than `localhost`, set `server.auth.cookie_secure: false`. When the proxy works, remove this setting. ## Caddy [Caddy]() gets and renews the certificate. On the same machine: ``` jocasta.example.com { reverse_proxy localhost:8080 } ``` If only your network resolves the name, add `tls internal`. Caddy then signs the certificate itself. Each browser must trust it once. ## Other proxies Pass the `Host` header through without a change. Jocasta compares it with `Origin`. This lets Jocasta refuse cross-site form posts from browsers that do not send `Sec-Fetch-Site`. In nginx, use `proxy_set_header Host $host;`. ## Name the proxy Jocasta limits sign-in attempts for each client address. Behind a proxy, every request comes from the address of the proxy. So all clients share one limit. To prevent this, tell Jocasta to read the client address from `X-Forwarded-For`: ``` server: trusted_proxies: - "127.0.0.1" ``` List the address or range of each proxy. Caddy, nginx and Traefik set the header. ## Close the plain port If the proxy is on the same machine, listen only on loopback: ``` server: host: "127.0.0.1" ``` The container image sets `JOCASTA_SERVER__HOST=0.0.0.0`, and this value wins over the file. So in a container, pass `-e JOCASTA_SERVER__HOST=127.0.0.1` to `docker run` instead. # Back up and upgrade Jocasta keeps all its data in one SQLite database: inventory, change log, traffic, accounts, tokens and notification choices. Back up this database and your config file. ## Back up In the container, the database is `/data/jocasta.db`. Recent writes can be in `jocasta.db-wal`, next to it. So stop Jocasta and copy the whole directory: ``` docker stop jocasta docker run --rm -v jocasta-data:/data -v "$PWD":/backup alpine \ cp -a /data /backup/jocasta-backup docker start jocasta ``` The copy contains password hashes and two-factor secrets. Keep it safe. To restore: 1. Stop Jocasta. 2. Put the files back. 3. Start Jocasta. ## Upgrade Jocasta is pre-1.0. A release can change the config format or the schema with no upgrade path. 1. Read the [upgrade notes]() for every release after yours. 2. Back up. 3. Pull the new tag, such as `:v0.11.1`. 4. Recreate the container. Jocasta updates the schema when it starts. ## Downgrade Restore the backup from before the upgrade. If you start an older release on a newer database, it migrates the schema down. This deletes the newer data. # Upgrade notes What to change or expect when moving to a release, newest first. Read every section after the release you run, and [back up]() first. A release not listed here needs only the [usual steps](). ## 0.4.0 Released 2026-09-25. ### Breaking changes - config: Group retention, location, server settings ([\#205]()) a config that sets inventory.retention, auth.\* or mcp.enabled, or the matching JOCASTA\_ variables, now has those settings ignored. Move them to the new paths. # Fix common problems If your problem is not on this page, ask in [GitHub issues](). Include the log lines around the problem. ## Devices have no hardware address or vendor On a bridge network, the container reads its own neighbour table. This table contains none of your devices. Use `--network host`. For devices on other segments, add a [router plugin](). ## One phone shows up as several devices The phone makes up a new hardware address from time to time. Each address is a new device. Jocasta deletes the devices that you did not edit after 90 days. On your own phone, turn off the private address for your home Wi-Fi. ## A device is missing The sweep finds only devices that answer ping. By default, Windows does not answer ping. A [router plugin]() finds such devices in the tables of the router. ## "no usable ICMP socket" If you run the binary without root, its group must be allowed to use unprivileged ICMP. To see the allowed groups, run: ``` sysctl net.ipv4.ping_group_range ``` Make the range include the group of Jocasta, or use the container image. ## No mDNS, DNS-SD or SSDP names These lookups reach only the segment of Jocasta. DNS-SD and SSDP also need multicast. Use `--network host`. See [How names are chosen](). ## The sign-in page keeps coming back By default, the session cookie works only over HTTPS. See [Serve over HTTPS](). ## A form shows "Cross-origin request refused" Your reverse proxy changes the `Host` header. See [Serve over HTTPS](). ## A plugin reads as "source rejected credentials" Test the plugin alone: `jocasta plugin run gateway`. - RouterOS: the user needs read access. The `www-ssl` service must be on. Or the `www` service must be on, with `ssl: false`. - OpenWrt: the login needs the [ACL file](). - Technitium: the user of the token needs View on Logs. - An environment variable wins over the file. Check both. ## The Traffic page stays empty - `exporters` must list the address that the exports come from. If it does not, the log shows the warning "dropping datagrams from a sender not listed in exporters", with that address. - On a bridge network, publish the port as UDP: `-p 2055:2055/udp`. - Open the listen port in the firewall of the host. ## Every DNS lookup is counted against the router The router forwards the lookups of its clients. In the DHCP settings of the router, give out the address of the resolver. See [Record what devices look up](). ## Times are hours off The container runs in UTC. Set `location.timezone`, such as `Australia/Sydney`. ## A notification did not arrive In the account menu, the **Notifications** page shows the last error under the destination. To try again, select **Send test**. Jocasta does not send a failed message again. ## An MCP client gets a 404 Set `server.mcp.enabled: true`, then restart Jocasta. # Reference # Configuration reference This page lists every setting that Jocasta reads, with its type, default and allowed values. [Configure]() tells how Jocasta reads and overrides settings. [`jocasta.example.yaml`]() has the same settings, in a file that you can copy. Each setting can also come from an environment variable. The name is `JOCASTA_`, then the path of the setting in capitals, with a double underscore between levels. A single underscore stays part of the key. For example, `scan.devices.resolve_mdns` is `JOCASTA_SCAN__DEVICES__RESOLVE_MDNS`. For a plugin or notification setting, the instance or destination name is one level of the path. For example, `plugins.routeros.gateway.password` is `JOCASTA_PLUGINS__ROUTEROS__GATEWAY__PASSWORD`. Types: - **duration**: a number and a unit, such as `90s`, `15m` or `6h`. The units are `s`, `m` and `h`. There is no unit for days, so 30 days is `720h`. - **list**: a YAML list. - **map**: YAML keys and values. ## server | Key | Type | Default | Allowed values | Description | | --- | --- | --- | --- | --- | | `server.host` | string | `localhost` | An address or host name | Where the web server listens. `0.0.0.0` listens on every interface. The container image sets `0.0.0.0`. | | `server.port` | int | `8080` | 1 to 65535 | The port the web server listens on. | | `server.trusted_proxies` | list | empty | Addresses or CIDR prefixes, such as `192.0.2.1` or `10.0.0.0/8` | Reverse proxy addresses whose forwarding headers the server trusts to identify the client. Empty reads the client address from the connection directly. | | `server.cors.allowed_origins` | list | empty | Origins as `scheme://host[:port]` | Browser origins besides the server's own that may read `/api`. Empty sends no CORS headers. | | `server.auth.session_lifetime` | duration | `168h` | A duration | The longest a sign-in lasts, whatever the activity. | | `server.auth.idle_timeout` | duration | `24h` | A duration | How long a session survives with no requests. | | `server.auth.cookie_secure` | bool | `true` | `true`, `false` | Sends the session cookie over HTTPS only. Set `false` to sign in over plain HTTP from anywhere but `localhost`. | | `server.mcp.enabled` | bool | `false` | `true`, `false` | Serves the [MCP endpoint]() at `/mcp`. While off, `/mcp` answers 404. | ## db | Key | Type | Default | Allowed values | Description | | --- | --- | --- | --- | --- | | `db.path` | string | `.` | A directory | Where the SQLite file lives. The container image sets `/data`. | | `db.name` | string | `jocasta.db` | A file name | The SQLite file's name. | ## logger | Key | Type | Default | Allowed values | Description | | --- | --- | --- | --- | --- | | `logger.level` | string | `info` | `debug`, `info`, `warn`, `error` | The lowest level logged. Any other value logs at `info`. | | `logger.format` | string | `json` | `json`, `text` | How each log line is written. Any value but `text` writes JSON. | ## inventory | Key | Type | Default | Allowed values | Description | | --- | --- | --- | --- | --- | | `inventory.online_window` | duration | `15m` | A duration | How long after its last sighting a device counts as online. Past it, a [watched device]() goes quiet. | | `inventory.address_grace` | duration | `6h` | A duration | How long an address can go unseen before a later sweep that finds the device elsewhere retires it. Raise it on a network with long DHCP leases. | ## retention Once an hour, Jocasta deletes the records that are older than their window. `0s` keeps that kind of record forever. | Key | Type | Default | Allowed values | Description | | --- | --- | --- | --- | --- | | `retention.history` | duration | `2160h` (90 days) | A duration; `0s` keeps forever | How long Jocasta keeps the change log and the scan log. | | `retention.traffic` | duration | `720h` (30 days) | A duration; `0s` keeps forever | How long Jocasta keeps the hourly traffic totals. | | `retention.dns` | duration | `720h` (30 days) | A duration; `0s` keeps forever | How long Jocasta keeps the hourly lookup counts, and the addresses in the answers to lookups. | | `retention.devices` | duration | `2160h` (90 days) | A duration; `0s` keeps forever | How long Jocasta keeps a device after a scan last saw it. Jocasta keeps a device that you labelled, grouped, typed, noted, ignored or watched. | ## location | Key | Type | Default | Allowed values | Description | | --- | --- | --- | --- | --- | | `location.country` | string | empty | A two-letter country code, such as `AU` | The country from which the world map draws its lines. If empty, Jocasta uses the outside address of the router. This address cannot find the country of a network behind carrier-grade NAT. See [NetFlow](). | | `location.timezone` | string | empty | An IANA zone name, such as `Australia/Sydney` | The zone in which Jocasta shows times. If empty, Jocasta uses `TZ` or the zone of the system, which is UTC in a container. Jocasta always stores times in UTC. | ## networks | Key | Type | Default | Allowed values | Description | | --- | --- | --- | --- | --- | | `networks` | list | empty | Prefixes, such as `192.0.2.0/24` | The prefixes that the sweep covers at each interval. Jocasta records each address that it finds under the network that contains it. | ## scan | Key | Type | Default | Allowed values | Description | | --- | --- | --- | --- | --- | | `scan.source` | string | `sweep:` | Any name | The source name of the sweeps that run at each interval. Set it in a container, where the host name changes on every run. A new name starts a new source. Past scans stay under the old name. | | `scan.devices.enabled` | bool | `true` | `true`, `false` | Sweeps `networks` on a timer. | | `scan.devices.interval` | duration | `5m` | A duration | How often the sweep runs. | | `scan.devices.rate` | int | `1000` | A whole number | The most ICMP probes sent per second. | | `scan.devices.rounds` | int | `2` | A whole number | The number of probes to each address before Jocasta counts the address as down. | | `scan.devices.wait` | duration | `2s` | A duration | How long to wait for replies after the last probe. | | `scan.devices.resolve_names` | bool | `true` | `true`, `false` | Looks up reverse DNS for each address that answered. See [Name devices](). | | `scan.devices.resolve_macs` | bool | `true` | `true`, `false` | Reads the neighbour table for hardware addresses. | | `scan.devices.resolve_mdns` | bool | `true` | `true`, `false` | Asks each device without a reverse DNS name for its name over mDNS. | | `scan.devices.resolve_netbios` | bool | `true` | `true`, `false` | Asks each device still without a name over NetBIOS. | | `scan.devices.resolve_dnssd` | bool | `true` | `true`, `false` | Asks for the services that devices advertise. Names each device that still has no name after the name that it gives its services. Needs host networking. | | `scan.devices.resolve_ssdp` | bool | `true` | `true`, `false` | Searches over SSDP. Names each device that still has no name after its UPnP `friendlyName`. Needs host networking. | | `scan.ports.enabled` | bool | `false` | `true`, `false` | Probes every known address for open TCP ports on a timer. | | `scan.ports.interval` | duration | `6h` | A duration | How often the port scan runs. | | `scan.ports.custom` | string | empty | Ports and ranges, such as `22,80,443,8000-8100`; `1-65535` for every port | The ports to probe. If empty, Jocasta uses a preset. Jocasta also probes each address on the ports that its device advertises. | | `scan.ports.concurrency` | int | `64` | A whole number | The most connections a scan opens at once. Lower it on a router with a small connection table. | ## plugins By default, plugins are off. Each instance is under its kind, with a name that you choose as its key. An instance is off, unless it has `enabled: true`. See [Plugins](). ### plugins.routeros A [MikroTik RouterOS]() router, switch or access point. | Key | Type | Default | Allowed values | Description | | --- | --- | --- | --- | --- | | `plugins.routeros..enabled` | bool | `false` | `true`, `false` | Reads this source. | | `plugins.routeros..host` | string | none, required | An address or host name, without a port | Where the REST API is. | | `plugins.routeros..port` | int | `80`, or `443` with `ssl` | 1 to 65535 | The REST API's port. | | `plugins.routeros..user` | string | empty | A RouterOS user | A read-only user is enough. | | `plugins.routeros..password` | string | empty | Any | The user's password. | | `plugins.routeros..ssl` | bool | `false` | `true`, `false` | Uses HTTPS. Without it, the password goes over the network as plain text, and Jocasta logs a warning. | | `plugins.routeros..insecure` | bool | `false` | `true`, `false` | Skips certificate checks, for RouterOS's self-signed certificate. | | `plugins.routeros..timeout` | duration | `10s` | A duration | The longest one read may take. | | `plugins.routeros..topology_only` | bool | `false` | `true`, `false` | Reads only what is connected to this switch or access point. The router lists its devices. See [Show what is plugged in where](). | ### plugins.openwrt An [OpenWrt]() router or access point. | Key | Type | Default | Allowed values | Description | | --- | --- | --- | --- | --- | | `plugins.openwrt..enabled` | bool | `false` | `true`, `false` | Reads this source. | | `plugins.openwrt..host` | string | none, required | An address or host name, without a port | Where ubus is served. | | `plugins.openwrt..port` | int | `80`, or `443` with `ssl` | 1 to 65535 | uhttpd's port. | | `plugins.openwrt..user` | string | empty | An rpcd login | A login with the Jocasta ACL. | | `plugins.openwrt..password` | string | empty | Any | The login's password. | | `plugins.openwrt..ssl` | bool | `false` | `true`, `false` | Uses HTTPS. Needs `luci-ssl`. Without it, the password goes over the network as plain text, and Jocasta logs a warning. | | `plugins.openwrt..insecure` | bool | `false` | `true`, `false` | Skips certificate checks, for uhttpd's self-signed certificate. | | `plugins.openwrt..timeout` | duration | `10s` | A duration | The longest one read may take. | | `plugins.openwrt..topology_only` | bool | `false` | `true`, `false` | Reads only what is connected to this access point. The router lists its devices. See [Show what is plugged in where](). | ### plugins.netflow A [NetFlow]() listener for a router's flow exports. | Key | Type | Default | Allowed values | Description | | --- | --- | --- | --- | --- | | `plugins.netflow..enabled` | bool | `false` | `true`, `false` | Runs this listener. | | `plugins.netflow..listen` | string | `:2055` | A UDP address, such as `:2055` or `192.0.2.10:2055` | Where the listener binds. | | `plugins.netflow..exporters` | list | none, required | IP addresses | The routers that can send. Jocasta discards datagrams from all other addresses. | ### plugins.technitium A [Technitium DNS Server]() node. | Key | Type | Default | Allowed values | Description | | --- | --- | --- | --- | --- | | `plugins.technitium..enabled` | bool | `false` | `true`, `false` | Reads this server's query log. | | `plugins.technitium..host` | string | none, required | An address or host name, without a port | The address of this node. Do not use a virtual address that the nodes share. It reaches only one node at a time. | | `plugins.technitium..port` | int | `5380`, or `53443` with `ssl` | 1 to 65535 | The web service's port. | | `plugins.technitium..token` | string | empty | An API token | A token for a user granted Logs: View. | | `plugins.technitium..ssl` | bool | `false` | `true`, `false` | Uses HTTPS. Without it, the token goes over the network as plain text, and Jocasta logs a warning. | | `plugins.technitium..insecure` | bool | `false` | `true`, `false` | Skips certificate checks, for a self-signed certificate. | | `plugins.technitium..log_app` | string | empty | An app name, as the Apps page lists it | The app whose log Jocasta reads. Necessary only if more than one installed app logs queries. | | `plugins.technitium..timeout` | duration | `10s` | A duration | The longest one read may take. | ### plugins.pihole A [Pi-hole]() v6. | Key | Type | Default | Allowed values | Description | | --- | --- | --- | --- | --- | | `plugins.pihole..enabled` | bool | `false` | `true`, `false` | Reads this server's query log. | | `plugins.pihole..host` | string | none, required | An address or host name, without a port | Where Pi-hole's web server is. | | `plugins.pihole..port` | int | `80`, or `443` with `ssl` | 1 to 65535 | The web server's port. | | `plugins.pihole..password` | string | empty | An app password | Empty reads a Pi-hole with no password set. | | `plugins.pihole..ssl` | bool | `false` | `true`, `false` | Uses HTTPS. Without it, the password goes over the network as plain text, and Jocasta logs a warning. | | `plugins.pihole..insecure` | bool | `false` | `true`, `false` | Skips certificate checks, for Pi-hole's self-signed certificate. | | `plugins.pihole..timeout` | duration | `10s` | A duration | The longest one read may take. | ### plugins.adguard An [AdGuard Home](). | Key | Type | Default | Allowed values | Description | | --- | --- | --- | --- | --- | | `plugins.adguard..enabled` | bool | `false` | `true`, `false` | Reads this server's query log. | | `plugins.adguard..host` | string | none, required | An address or host name, without a port | Where the web interface is. | | `plugins.adguard..port` | int | `80`, or `443` with `ssl` | 1 to 65535 | The web interface's port. | | `plugins.adguard..user` | string | empty | The web interface login | Empty reads a server with no login set. | | `plugins.adguard..password` | string | empty | Any | The login's password. | | `plugins.adguard..ssl` | bool | `false` | `true`, `false` | Uses HTTPS. Without it, the password goes over the network as plain text, and Jocasta logs a warning. | | `plugins.adguard..insecure` | bool | `false` | `true`, `false` | Skips certificate checks, for a self-signed certificate. | | `plugins.adguard..timeout` | duration | `10s` | A duration | The longest one read may take. | ## notify Each destination is under `notify`, with a name that you choose as its key. It has exactly one service block: `ntfy`, `webhook` or `http`. To select the changes that a destination gets, use the Notifications page in the account menu. See [Notifications](). | Key | Type | Default | Allowed values | Description | | --- | --- | --- | --- | --- | | `notify..enabled` | bool | `true` | `true`, `false` | Sends to this destination. A destination is on unless it says `false`. | | `notify..timeout` | duration | `10s` | A duration | The longest one send may take. | ### ntfy | Key | Type | Default | Allowed values | Description | | --- | --- | --- | --- | --- | | `notify..ntfy.url` | string | none, required | The topic's URL, as ntfy shows it | Where Jocasta publishes messages, such as `https://ntfy.example.com/jocasta`. | | `notify..ntfy.token` | string | empty | An ntfy access token | For a server that requires one. | | `notify..ntfy.priority` | int | `0` | `0` to `5` | ntfy's priority, from 1 (min) to 5 (max). `0` leaves it to the server. | ### webhook | Key | Type | Default | Allowed values | Description | | --- | --- | --- | --- | --- | | `notify..webhook.url` | string | none, required | An HTTP or HTTPS URL | Where the signed JSON POST goes. | | `notify..webhook.secret` | string | none, required | Any | Keys the signature on each request. See [Check a webhook's signature](). | ### http | Key | Type | Default | Allowed values | Description | | --- | --- | --- | --- | --- | | `notify..http.url` | string | none, required | An HTTP or HTTPS URL | Where the request goes. It can contain the token of the service. Jocasta never logs or shows it. | | `notify..http.headers` | map | empty | Header names and values | Sent with every request, such as `Authorization`. A `Content-Type` here replaces `application/json`. | | `notify..http.body` | string | none, required | A Go template | The request body. See [Send to another service]() for recipes. | # CLI ``` jocasta [flags] serve Start the web server and the sweep poller. (default) scan Sweep a prefix once and print the result. ports [target] Probe TCP ports on an address, a prefix, or the inventory. plugin run Read one configured source and print what it claims. version Print build information. -c, --config Path to the config file (default "jocasta.yaml") --log-level debug | info | warn | error --log-format text | json ``` To see the flags of a command, run it with `--help`. ## serve ``` jocasta serve # host and port from config jocasta serve -p 9000 # override the port ``` Starts the HTTP server. It also starts these tasks: - If `scan.devices.enabled` is set, a poller sweeps every network in `networks` at each `scan.devices.interval`. - If `scan.ports.enabled` is set, a second poller port-scans every address that the inventory has on your networks, at each `scan.ports.interval`. It also port-scans each new device when a sweep or a router read finds it. - At each `scan.devices.interval`, Jocasta reads each enabled `routeros` and `openwrt` instance for what is connected to it. - Each enabled `netflow` instance starts a listener for the flow exports of its router. - Each hour, a prune deletes records that are older than their `retention` window. It also deletes devices that you never edited and that no scan saw within `retention.devices`. ## scan Runs one sweep and prints a table, or JSON with `--json`. With `--save`, it also records the sweep in the inventory. The source name comes from `--source`. If that is not set, it comes from `scan.source`, and then from the name of this host. ``` $ jocasta scan 192.0.2.0/24 IP MAC VENDOR HOSTNAME RTT DETAILS 192.0.2.1 00:00:5e:00:53:01 - gateway.example 1.2ms 192.0.2.10 00:00:5e:00:53:02 - nas.example 0.8ms 192.0.2.20 00:00:5e:00:53:04 - - 3.1ms self (eth0) $ jocasta scan 192.0.2.0/24 --save --rate 500 --no-resolve-names ``` The sweep uses a raw ICMP socket if it can. If not, it uses an unprivileged datagram socket. So it runs without root. If a host has no reverse DNS name, the sweep asks for its name over mDNS, then NetBIOS, then DNS-SD, then SSDP. It stops when one of them gives a name. To turn each one off, use `--no-resolve-mdns`, `--no-resolve-netbios`, `--no-resolve-dnssd` or `--no-resolve-ssdp`. With `--json`, each host also lists the services that it advertises over DNS-SD. ## ports Probes TCP ports with a plain `connect()`. So it needs no privileges and cannot change the target. With no argument, it scans every current address that the inventory has on your networks. Your networks are those in `networks` and the segments that a router serves. It does not scan a neighbour outside them, such as the gateway of your ISP. It also scans each address on these TCP ports: - the ports on which its device advertises a service - the ports recorded as open on that device To scan only one address or prefix, give it as the argument. `--ports` takes a list such as `22,80,443,8000-8100`. By default, it scans a preset of about a hundred ports that homelabs often use. `--concurrency` sets the maximum number of open connections at one time (64 by default). `--save` records the results on the matching devices. ``` $ jocasta ports 192.0.2.10 ADDRESS PORT SERVICE 192.0.2.10 22 ssh 192.0.2.10 443 https $ jocasta ports --save # every known address, recorded $ jocasta ports 192.0.2.0/24 --ports 1-1024 --json ``` ## plugin run Reads one configured source without starting the server. Use it to test a credential or a firewall rule against the real router. It reads the instance even when the config disables it. ``` $ jocasta plugin run gateway NETWORK VLAN NAME 192.0.2.0/24 - Trusted 198.51.100.0/24 20 IoT ADDRESS MAC VENDOR HOSTNAME STANDING PRESENT 192.0.2.1 00:00:5e:00:53:01 - gateway DHCP_STATIC true 198.51.100.10 00:00:5e:00:53:09 - thermostat DHCP_LEASE true $ jocasta plugin run gateway --json --save ``` After the devices, it prints the topology of the source: - its ports, and the VLANs on each port - the hardware addresses that it learned on each port - its neighbours A `topology_only` instance prints only the topology. `--save` records both. # Development # Development ## Make targets ``` make build # build to bin/ make test # go test ./... make e2e # browser tests a pull request must pass (needs Chrome) make e2e-full # browser tests on every screen and account make smoke # read the router, DNS and NetFlow sources in containers (needs Docker) make lint # golangci-lint (installs it to bin/ on first run) make dev # hot-reload server via air make gen # regenerate sqlc models make new_migration name= make oui # rebuild the embedded MAC-vendor table make htmx # refresh the vendored htmx make docs # build the docs site and check every link (needs uv) make docs-serve # preview the docs site at http://localhost:8000 ``` ## Browser tests `internal/web/e2e` drives Chrome over the real server. It builds with the `e2e` tag, so `go test ./...` leaves it out. It needs Google Chrome or Chromium on `PATH`, or `E2E_CHROME` set to one of them. The tests draw each page over four fabricated inventories: empty, a home network, a network with extreme data, and a network with no accounts. They also draw each dialog, menu and inline edit that the page opens. Each drawing is checked for these problems: - sideways scrolling - clipped or overlapping text - small tap targets and small text - WCAG 2.2 AA failures (axe-core) - script errors and failed requests `make e2e` draws every page as admin on a phone, a tablet and a laptop, in both themes. It takes about two minutes. CI does not run it. So run it before you open a pull request that changes the UI. `make e2e-full` adds every other screen and account. `scripts/release.sh` runs it before it tags a release. A finding fails the run, unless `testdata/known.json` lists it under an open issue. A fix deletes its entries. `make e2e-full` also fails on an entry that no longer matches a finding. The run also compares the first screen of every page on the home network with a stored picture in `testdata/golden/`. It does this on a phone, a tablet and a laptop in the light theme, and on the laptop in the dark theme. A picture fails when more than 64 of its pixels changed. One added or removed word changes more pixels than that. If you change the UI on purpose, store the new pictures and commit them with the change: ``` E2E_UPDATE=1 make e2e ``` To see what changed, set `E2E_ARTIFACTS` to a directory. The run writes each picture that differs there, next to a map of the changed pixels. To look at a fixture in a browser: ``` E2E_SERVE=weird go test -tags e2e -run TestServe -timeout 0 ./internal/web/e2e/ ``` To capture every page and read the findings, without a pass or fail: ``` E2E_AUDIT=$PWD/tmp/audit TMPDIR=$PWD/tmp go test -tags e2e -run TestAudit -timeout 0 ./internal/web/e2e/ ``` To limit either run, set `E2E_FIXTURES`, `E2E_ROLES`, `E2E_VIEWPORTS`, `E2E_THEMES` or `E2E_PAGES` to a comma-separated list. ## Smoke tests `make smoke` reads real OpenWrt, RouterOS, Pi-hole, AdGuard Home and Technitium servers and a real NetFlow exporter, each in a container. It checks what jocasta says about them. The plugin tests use canned replies. The smoke tests find a server that changed what it sends. They need Docker and Go. CI does not run them. So run them before you open a pull request that changes one of these plugins or its client under `pkg/`. `scripts/smoke/smoke.sh up` starts the servers on their own network, `198.51.100.0/24`: - OpenWrt has the setup that [the OpenWrt page]() gives, with the ACL in `docs/openwrt/jocasta.json`. It also gets leases and static hosts. - RouterOS CHR runs in QEMU, and `scripts/smoke/routeros.sh` boots it. Its `ether1` gets an address over DHCP, as a WAN port does. Its `ether2` is on the smoke network. It gets a read-only `jocasta` user, a bridge with its own address, a DHCP server with a static lease, and a static ARP entry. It also gets Traffic Flow that exports IPFIX, with the setup from [the RouterOS page](). - Pi-hole, AdGuard Home and Technitium each block a name. AdGuard Home also rewrites one name. Technitium has the setup that [the Technitium page]() gives: a user that can view Logs, its API token, and the Query Logs (Sqlite) app. Pi-hole is the upstream of the other two, so no lookup leaves the network. - pmacct runs in the network namespace of the OpenWrt container. It exports NetFlow v9 to this host, as the router would. - The OpenWrt container sends lookups to each resolver. Each server publishes its web interface on `127.0.0.1`, and jocasta reads it there. `scripts/smoke/smoke.sh check` runs `jocasta plugin run` against each server and checks the rows that it prints. It also runs these tests: - `TestNetFlowLive` in `internal/plugin`, two times. The first time, the OpenWrt container makes a lookup. The second time, this host makes a request to the router. The script checks the flows that each run decodes. - The live tests in `pkg/openwrt`, `pkg/routeros` and `internal/plugin`. The script writes its config to `$TMPDIR/jocasta-smoke/jocasta.yaml`. So you can point `jocasta serve` at the same servers. `make smoke-down` removes the servers. Some services need more than Docker. To leave a service out, add it to `SMOKE_SKIP`, such as `SMOKE_SKIP="technitium netflow" make smoke`: - `routeros`: RouterOS runs under QEMU. If the host has no KVM, QEMU emulates the CPU. In both cases, it boots in less than a minute. Its container joins a second network, which needs Docker 28 or later. - `technitium`: the server downloads the Query Logs app from the app store of Technitium. So its container needs internet access. - `netflow`: the exporter sends to the gateway address of the network. Only Docker on Linux puts this address on the host. To change a setting, set the matching variable: - `SMOKE_REGISTRY` pulls the images from a mirror, such as `mirror.gcr.io`. Use it when Docker Hub limits your pulls. - `SMOKE_PIHOLE_IMAGE`, `SMOKE_ADGUARD_IMAGE`, `SMOKE_OPENWRT_IMAGE`, `SMOKE_ROUTEROS_IMAGE`, `SMOKE_TECHNITIUM_IMAGE` and `SMOKE_PMACCT_IMAGE` test a different release. MikroTik publishes no image. The RouterOS image is `evilfreelancer/docker-routeros`, which contains the CHR disk and QEMU. - `SMOKE_PIHOLE_PORT`, `SMOKE_ADGUARD_PORT`, `SMOKE_ADGUARD_SETUP_PORT`, `SMOKE_OPENWRT_PORT`, `SMOKE_TECHNITIUM_PORT` and `SMOKE_ROUTEROS_PORT` change the published ports, 18081 to 18086 by default. `SMOKE_FLOW_PORT` and `SMOKE_ROUTEROS_FLOW_PORT` change the ports for the NetFlow and IPFIX exports, 2055 and 2056 by default. netifd cannot create the `br-lan` bridge in a container. So the `lan` of the OpenWrt container is on `eth0`. Its bridge table and VLANs are empty. The unit tests cover the reads of these tables. ## Docs [Zensical]() publishes the pages in `docs/` to GitHub Pages. There is one version for each minor release and one for `main`. The `Docs` workflow builds every pull request that changes the docs with `make docs`. This build fails on a broken link or anchor. A merge to `main` publishes the `dev` version. A `v*` tag publishes its minor version as `latest`. The Zensical fork of [mike]() pushes both to the `gh-pages` branch. When you change the docs: - Add a new page to `nav` in `mkdocs.yml`, and to the `llmstxt` sections there. If you do not, the site leaves it out of the sidebar and `llms.txt`. - A new plugin gets a page under `docs/plugins/` and a line in the table on the plugins overview. A DNS resolver also gets a column on [Record what devices look up](). - A new setting gets a row in the [configuration reference](). `TestReferenceDocumentsEveryKey` fails until the setting has a row. It also fails on a row for a setting that no longer exists. - Keep `docs/setup.md` and `docs/mcp.md`. Earlier releases link to their sections on GitHub. Each section there points to the new location. ## Release After CI passes on `main`, run the command below. It runs `make e2e-full` first, so Chrome must be installed. ``` ./scripts/release.sh v0.10.0 ``` The script needs a clean working tree and a local `main` that matches `origin/main`. It checks that the tag is not used. Then it creates an annotated tag and pushes it, which starts the release workflow. It does not wait for the build. git-cliff writes the GitHub release and the changelog page from the commit messages, as `cliff.toml` sets. To give a minor release highlights, merge `docs/releases/.md` before you tag its `.0`. ## Layout - `cmd/jocasta`: CLI entry point. - `internal/scanner`: the ICMP sweep, and the reverse DNS, mDNS, NetBIOS, DNS-SD and SSDP lookups that name the hosts that answer. - `internal/plugin`: sources other than the sweep (RouterOS, OpenWrt, NetFlow, Technitium, Pi-hole, AdGuard Home). - `internal/inventory`: the store. It covers identity resolution, address handling, device classification and the change log. - `internal/classify`: the rules that guess the type of a device from its vendor, name and open ports. - `internal/notify`: sends the changes of each finished scan to ntfy, a webhook or a templated HTTP request. - `internal/web`, `internal/api`: the HTML UI and the JSON API. - `internal/db`: connection, migrations, generated queries. SQLC generates the queries from `internal/db/queries/`. The schema is in `internal/db/migrations/`. ## Add a notification service Most services accept an HTTP request. The `http` provider reaches them with a body template and no new code. For such a service, add a recipe to [Send to another service](). `TestRecipesInTheDocsRender` renders every recipe on that page. So a broken recipe fails the tests. A service gets its own provider only when the `http` provider cannot reach it. For example, the service needs more than one request, a protocol other than HTTP, or a signature that a template cannot make. Each provider is in its own file in `internal/notify`, as `ntfy.go` and `webhook.go` are: 1. Add a struct with `koanf` tags for its settings. Give it `Validate`, `Host` and `Send`. `Send` must not put a credential in its error. For a service that accepts an HTTP request, use `post`. 2. Add a field for it to `notify.Config`, and a line to `Config.provider`. 3. Add an example to `jocasta.example.yaml`. Add a row for each setting to the [configuration reference](). The notifier, the settings page, the store and `cmd` need no change. ## Add a name source Each name has a standing, `dbtype.HostnameSource`, which tells how Jocasta learned it. If sources give different names, Jocasta shows the name with the higher standing. 1. Add a constant for it in `internal/db/dbtype/enum.go`, and add it to `hostnameSources`. 2. Give it a place in `HostnameSource.Rank`. Give the reason in the comment there. A name that resolves ranks above a name that a device only claims. 3. Write its device page text in `standing`, in `internal/web/funcs.go`. 4. List it in [Name devices](). If a sweep asks devices for their names, do two more steps: 1. Add its lookup in its own file in `internal/scanner`. Ask it in `enrich`, in rank order. A query that goes to each host is a `nameProtocol`, as in `mdns.go` and `netbios.go`. Any other lookup is a `nameLookup`, as `askSSDP` in `ssdp.go` is. Or it names hosts itself, as `browseServices` does with `dnssd.go`. 2. Add it to `HostnameSource.Swept`. Then a lookup with a lower rank never replaces its name on the claim of the sweep. The column has no CHECK, so a new standing needs no migration. ## Screenshots The images in [`docs/ui.md`]() come from a database of fabricated devices, never real network data: addresses from RFC 5737, hardware addresses from RFC 7042, and invented names. The internet peers are well-known public services, so Jocasta can find their organisations and countries.