Files
oott/README.md
T

250 lines
16 KiB
Markdown
Raw Normal View History

2026-01-27 18:27:17 -05:00
# OOTT
2026-05-28 17:00:26 -04:00
Easy to setup and use network device discovery and alert system
Licensed under AGPL v3. See [LICENSE](LICENSE).
2026-01-27 18:27:17 -05:00
* [What is OOTT?](#what-is-oott)
* [Getting started](#getting-started)
* [Simple installation with Docker](#simple-installation-with-docker)
* [Using the mobile apps](#using-the-mobile-apps)
* [Deeper dive](#deeper-dive)
* [Installing OOTT as a Nix flake](#installing-oott-as-a-nix-flake)
* [Full list of configuration options](#full-list-of-configuration-options)
* [Using HTTPS and domain names with the mobile apps](#using-https-and-domain-names-with-the-mobile-apps)
* [Authentication and access control](#authentication-and-access-control)
* [Things to keep in mind](#things-to-keep-in-mind)
* [Storage considerations](#storage-considerations)
* [Privileges and network ports](#privileges-and-network-ports)
2026-01-28 13:48:04 -05:00
## What is OOTT?
OOTT runs behind the scenes and monitors your local network, notifying you when something changes. Its most relevant features are:
* Scans the network regularly using ARP probes, with additional mDNS, SSDP, DHCP and (optionally) SNMP discovery
* Notifies you when a new device is found
* Notifies you when a device changes its IP address or network interface vendor (based on its MAC address)
* Notifies you when a device comes back online after a configurable period of being offline
2026-01-27 18:27:17 -05:00
You configure and browse the data it collects through a companion app available on the web, desktop, iOS and Android.
2026-01-27 18:27:17 -05:00
## Getting started
2026-01-28 13:45:37 -05:00
### Simple installation with Docker
This is the quickest way to get OOTT running. OOTT is published on Docker Hub as a pre-built image, [`rzuasti/oott`](https://hub.docker.com/repository/docker/rzuasti/oott/general), and the steps below get you to a running service with the smallest possible configuration.
2026-01-28 13:45:37 -05:00
You need somewhere on the host to store the configuration file and the database (OOTT uses SQLite, so the database folder should be on local — not remote — storage):
2026-01-28 13:45:37 -05:00
```bash
mkdir -p /docker/oott/config
mkdir -p /docker/oott/db
```
The user that runs Docker needs read access to the config folder and read/write access to the db folder. You can use any paths you like as long as you keep the volume mappings below in sync.
2026-01-28 13:45:37 -05:00
Create a minimal `oott.toml` at `/docker/oott/config/oott.toml`:
```toml
[database]
path = "/db/oott.db" # Must be "/db/oott.db" for the Docker image (the mounted volume)
2026-01-28 13:45:37 -05:00
[web_server]
api_key = "CHANGE_ME" # API key the app uses to talk to the backend — change this!
[notifications]
method = "pushover" # Use "none" to only log notifications instead of sending them
# Only needed when method = "pushover"; omit this section for any other method.
[notifications.pushover]
token = "" # Your Pushover application token
user_key = "" # Your Pushover user key
```
This is enough to start the service; every other option falls back to a sensible default. See [Full list of configuration options](#full-list-of-configuration-options) for everything you can tune, and the [full sample config](https://github.com/rzuasti/oott/blob/main/examples/sample_oott.toml) for a commented, all-options file.
Create a `docker-compose.yml` at `/docker/oott/docker-compose.yml`:
```yaml
name: oott
services:
oott:
image: rzuasti/oott:latest
volumes:
- /docker/oott/config:/config
- /docker/oott/db:/db
network_mode: host
```
Host networking is required so OOTT can see the multicast and broadcast traffic its scanners rely on (see [Privileges and network ports](#privileges-and-network-ports)).
Start the service from where you placed the compose file:
```bash
docker compose up -d
```
That's it. Check that everything is running with `docker ps`, and follow the logs with `docker logs CONTAINER_ID -f`.
### Using the mobile apps
The OOTT app (web, desktop, iOS and Android) talks to the backend exclusively over its REST API on port `3000`. Open the app's settings and point it at your backend's address on the local network over HTTP, for example `http://192.168.1.50:3000`, using the API key you set in `web_server.api_key`.
This is the simplest setup and needs no extra infrastructure, but it only works while your phone is on the same local network as the backend.
2026-01-28 13:45:37 -05:00
> [!IMPORTANT]
> On iOS, the direct HTTP connection only works when you use the backend's **private-range IP address** (`192.168.x.x`, `10.x.x.x`, `172.1631.x.x`) or a `*.local` (mDNS/Bonjour) name. A custom internal domain (e.g. `oott.mylan.com`) served over plain HTTP is blocked by iOS App Transport Security, even when it resolves to a private IP. Use the IP address directly, or set up HTTPS.
2026-01-28 13:45:37 -05:00
> [!NOTE]
> The first time the iOS app reaches your backend on the local network, iOS shows a one-time "find devices on your local network" prompt. This is expected — tap Allow to continue.
2026-01-28 13:45:37 -05:00
To use a domain name, or to reach the backend from outside the local network, set up TLS as described in [Using HTTPS and domain names with the mobile apps](#using-https-and-domain-names-with-the-mobile-apps).
2026-01-28 13:45:37 -05:00
## Deeper dive
2026-01-27 18:27:17 -05:00
### Installing OOTT as a Nix flake
OOTT comes with a pre-built NixOS flake that you can integrate into your configuration. If you are not using flakes you can use the [flake code](https://github.com/rzuasti/oott/tree/main/nix) as a baseline and write your own derivation.
2026-01-27 18:38:09 -05:00
To integrate the OOTT flake into your config you generally do three things: add OOTT to your inputs, add its module and overlay, then enable and configure the service.
2026-01-27 18:43:51 -05:00
**1. Add OOTT to your inputs** — in your `flake.nix` inputs section:
2026-01-27 18:43:51 -05:00
```nix
2026-01-27 18:38:09 -05:00
inputs = {
...
oott = {
url = "github:rzuasti/oott";
inputs.nixpkgs.follows = "nixpkgs";
};
...
};
```
**2. Add the OOTT module and overlay** — in your `flake.nix` modules section:
2026-01-27 18:43:51 -05:00
```nix
2026-01-27 18:38:09 -05:00
...
modules = [
...
oott.nixosModules.oott
({pkgs, ...}: {
nixpkgs.overlays = [
oott.overlays.default
];
})
...
];
...
```
**3. Enable and configure OOTT** — in your `configuration.nix` (or an imported file). Set all options through the service definition; the keys mirror the config file options listed [below](#full-list-of-configuration-options):
2026-01-27 18:43:51 -05:00
```nix
{
pkgs,
...
}
: {
environment.systemPackages = with pkgs; [
oott
];
services.oott = {
enable = true;
database.path = "/var/lib/oott.db";
# networking.interface = "eth0"; # Optional: auto-detected if not set
2026-06-02 18:05:59 -04:00
log.level = "warn";
arp_scanner.wait_between_scans = "30m";
arp_scanner.sender_timeout = "1m";
arp_scanner.scan_duration = "10m";
2026-01-27 18:43:51 -05:00
notifications.method = "pushover";
notifications.notify_when_not_seen_for = "1w";
notifications.pushover.token = "YOUR API TOKEN GOES HERE";
notifications.pushover.user_key = "YOUR USER TOKEN GOES HERE";
retention.window = "365d";
device_events.deduplication_window = "1m";
2026-01-27 18:43:51 -05:00
};
}
```
2026-01-27 18:27:17 -05:00
### Full list of configuration options
The system configuration lives in a single config file, which you can write in TOML, JSON or YAML. With Docker, TOML is recommended — the [full sample config](https://github.com/rzuasti/oott/blob/main/examples/sample_oott.toml) lists every supported option with comments. With the Nix flake, set the same options through the service definition (see above).
2026-01-27 18:27:17 -05:00
Options marked **Required** have no built-in default and must be set in your config file. Everything else falls back to the default shown if you omit it.
|Option|Default value|Description|
2026-01-27 18:27:17 -05:00
|------|-------------|-----------|
|`database.path`|**Required**|Location of the system database. Must be `/db/oott.db` when using the Docker image.|
|`networking.interface`|auto-detected|Network interface to use for scans. Optional — if not set, the first non-loopback connected interface is used automatically.|
2026-06-02 18:05:59 -04:00
|`log.level`|`warn`|Log level to use (off, error, warn, info, debug, trace)|
2026-06-02 18:33:35 -04:00
|`web_server.ip_address`|`0.0.0.0`|Address the API and web UI bind to. Use `0.0.0.0` to bind all interfaces.|
|`web_server.port`|`3000`|Port the API and web UI listen on.|
|`web_server.api_key`|**Required**|API key the app must present to use the backend.|
|`arp_scanner.enabled`|`true`|Whether to run the ARP scanner. The whole `[arp_scanner]` section is optional; omit it to use the defaults below. Set to `false` to turn it off.|
|`arp_scanner.wait_between_scans`|`30m`|Time to wait between each network scan (you can express it in seconds, minutes, hours, etc. as a suffix - for example: 30s, 10m, 1h)|
|`arp_scanner.sender_timeout`|`1m`|If the ARP sender process takes longer than this it will be stopped (for a class C network - 254 IPs - it should take less than a minute)|
|`arp_scanner.scan_duration`|`10m`|How long to wait for response packets on each scan (5m to 10m is a good timeframe for a class B or C network)|
|`mdns_scanner.enabled`|`true`|Whether to run the mDNS/Bonjour scanner. Set to `false` to turn it off.|
|`mdns_scanner.probe_timeout`|`2s`|When an mDNS-discovered IP is not in the OS ARP cache, how long to wait for a targeted ARP probe reply to resolve its MAC address|
|`ssdp_scanner.enabled`|`true`|Whether to run the SSDP/UPnP scanner. Set to `false` to turn it off.|
|`ssdp_scanner.probe_timeout`|`2s`|When an SSDP/UPnP-discovered IP is not in the OS ARP cache, how long to wait for a targeted ARP probe reply to resolve its MAC address|
|`dhcp_scanner.enabled`|`true`|Whether to run the DHCP scanner. Set to `false` to turn it off.|
|`snmp_scanner.enabled`|`false`|Whether to run the SNMP scanner. The whole `[snmp_scanner]` section is optional and the scanner stays off unless you add it; once the section is present it defaults to `true`.|
|`snmp_scanner.target`|**Required**|SNMP agent to poll, as `host:port` (e.g. your gateway: `192.168.1.1:161`). The scanner reads the agent's ARP table over SNMPv2c — no local probing. Required when the `[snmp_scanner]` section is present.|
|`snmp_scanner.community`|**Required**|SNMPv2c read-only community string. Use a read-only community and never commit a real secret. Required when the `[snmp_scanner]` section is present.|
2026-06-02 08:23:20 -04:00
|`snmp_scanner.wait_between_scans`|`10m`|Time to wait between polls. Keep it well under the agent's ARP cache timeout so active devices aren't missed.|
|`snmp_scanner.timeout`|`5s`|Per-poll SNMP request timeout.|
|`notifications.method`|**Required**|For now just pushover, you can set this to "none" to avoid sending notifications (it will just log)|
|`notifications.notify_when_not_seen_for`|`1w`|Send a notification if a device comes back online after not being seen for this timeframe (you can use hours, weeks, etc.)|
|`notifications.pushover.token`|**Required when `method` is `pushover`**|Your pushover token goes here, just copy&paste from their website after creating the app. The whole `[notifications.pushover]` section may be omitted when `method` is anything other than `pushover`|
|`notifications.pushover.user_key`|**Required when `method` is `pushover`**|User key goes here, this is the account wide code for pushover. The whole `[notifications.pushover]` section may be omitted when `method` is anything other than `pushover`|
|`retention.window`|`365d`|How long to retain device events and notifications. Records older than this are purged daily. Accepts duration strings (e.g. `90d`, `1y`, `6m`).|
|`device_events.deduplication_window`|`1m`|Suppress duplicate device events: if the same scanner sees the same device (same MAC and IPv4) again within this window, only one event is recorded. Accepts duration strings (e.g. `30s`, `1m`, `5m`).|
### Using HTTPS and domain names with the mobile apps
To reach the backend through a domain name, or from outside the local network, put it behind a reverse proxy (nginx, Caddy, Traefik, …) that terminates TLS with a **valid certificate from a trusted CA** (for example [Let's Encrypt](https://letsencrypt.org/)), then point the app at the HTTPS URL (e.g. `https://oott.example.com`). This works on every platform — including iOS — with no further configuration.
2026-06-01 18:02:07 -04:00
Connect using the **domain name the certificate is issued for**, not an IP address. Self-signed certificates are not supported unless they are manually trusted on the device.
2026-06-01 18:02:07 -04:00
### Authentication and access control
OOTT does not provide built-in user accounts or permission management. Access to the backend is gated only by the `web_server.api_key` you configure.
If you want proper user authentication (login pages, multiple users, single sign-on, etc.), add an authentication layer in front of OOTT — for example [TinyAuth](https://github.com/steveiliop56/tinyauth), [Authentik](https://goauthentik.io/) or a similar reverse-proxy authentication solution.
> [!IMPORTANT]
> Do **not** apply that authentication layer to the `/api` URIs. The mobile apps and web front-end talk to the backend over `/api` using the API key.
## Things to keep in mind
2026-06-01 18:02:07 -04:00
### Storage considerations
OOTT stores a timestamped event for every device *sighting*, so the database grows with the number of devices on your network and how long you keep history.
**Assumptions for the estimates below:**
- The four scanners that are **on by default** are running (ARP, mDNS, SSDP and DHCP). The SNMP scanner is disabled by default and not included.
- Default configuration, with `retention.window = 365d` (one year of history).
- Figures are typical averages, not worst case — real usage is often lower.
#### Estimated storage by network size
| Network size | Active devices | Estimated storage / year |
|---|---|---|
| Home | 2050 | ~150350 MB |
| Homelab | 50100 | ~350700 MB |
| Small office | 100200 | ~0.71.4 GB |
| Medium office | 200500 | ~1.43.5 GB |
#### Controlling the database size
You have three main levers, in rough order of impact:
**1. Retention window** (`retention.window`) — how far back history is kept. This scales storage linearly: halving the window halves the storage.
**2. Enabled scanners** — each scanner you turn off removes its share. The three passive scanners (mDNS/SSDP/DHCP) generate most of the events; disabling them leaves just the ARP baseline (≈1.9 MB/device/year).
**3. ARP scan interval** (`arp_scanner.wait_between_scans`) — a longer interval means fewer ARP events. This only affects the ARP share, so its overall impact is modest while the passive scanners run.
> The deduplication window (`device_events.deduplication_window`, default `1m`) caps how often a single device can be recorded — at most one event per scanner per minute. It's a safety limit for chatty devices, not a routine tuning knob.
### Privileges and network ports
OOTT binds to the following ports on the host where it runs:
|Port|Protocol|Configurable|Used by|
|----|--------|------------|-------|
|`3000`|TCP|Yes (`web_server.port`)|REST API and web UI|
|`5353`|UDP (multicast)|**No — fixed**|mDNS/Bonjour scanner|
|`1900`|UDP (multicast)|**No — fixed**|SSDP/UPnP scanner|
|`67`|UDP (broadcast)|**No — fixed**|DHCP scanner|
The mDNS (UDP `5353`), SSDP (UDP `1900`) and DHCP (UDP `67`) ports are fixed by their respective protocols and **cannot be changed**. They must be available on the server where OOTT runs.
OOTT binds these sockets with address/port reuse, so it can run alongside other responders already listening on them (for example `avahi` on `5353`, `minidlna` on `1900`, or a DHCP server/relay on `67`). However, the ports must not be blocked by a host firewall, and the corresponding multicast/broadcast traffic must be allowed to reach the host — otherwise the scanners will not discover any devices.
> [!IMPORTANT]
> OOTT needs elevated privileges: the ARP scanner requires raw-socket access, and port `67` is a privileged port. The pre-built Docker image and NixOS module already run with what they need.
>
> Under Docker, the scanners require **host networking** (or an equivalent setup that exposes the host's multicast and broadcast traffic to the container), as shown in the [Docker installation](#simple-installation-with-docker) above.