- New endpoints: GET /api/v1/pools, GET /api/v1/pools/{slug}/next,
GET/POST /api/v1/allocations, GET /api/v1/allocations/{id},
POST /api/v1/allocations/{id}/release
- Bearer token auth; only SHA-256 hashes stored, plaintext shown once
- Token management GUI at /tokens (create, revoke, delete) linked in footer
- API actions audited with actor api:<token name>
- Fix latent transaction leak: replace manual BEGIN IMMEDIATE/COMMIT with
begin_with(), which rolls back on drop; previously a failed allocation
(e.g. disallowed prefix or full pool) returned a connection with an open
transaction to the pool, causing 'database is locked' on later writes
- README: API documentation with curl examples
242 lines
8.8 KiB
Markdown
242 lines
8.8 KiB
Markdown
# SUB-ALIGATOR
|
||
|
||
A simple internal web application for tracking and automatically allocating IPv4 subnets.
|
||
|
||
## Purpose
|
||
|
||
This application is intended solely for internal tracking and automatic allocation of IPv4 subnets. It is not a general IPAM, monitoring tool, device inventory, VLAN manager, or network topology viewer.
|
||
|
||
It is designed to run only inside a LAN/VPN. **The application does not include authentication** and must be placed behind a firewall/reverse proxy in a trusted network.
|
||
|
||
## Features
|
||
|
||
- Manage one or more IP pools manually through the web UI.
|
||
- First-run setup wizard: create the first pool before allocating subnets.
|
||
- Automatic subnet allocation using a **first-fit** algorithm for prefixes /20 through /30.
|
||
- Atomic allocation writes using SQLite `BEGIN IMMEDIATE` transactions.
|
||
- Retro TUI/terminal-style web UI, server-side rendered with Askama.
|
||
- Dynamic preview of the next free subnet via HTMX.
|
||
- Allocation history with filtering, search, and pagination.
|
||
- Release subnets (status `released`) with a mandatory note, directly from the history table or detail page.
|
||
- Released subnets free up address space and can be reused by the first-fit algorithm.
|
||
- Delete pools without losing their allocations: the subnets stay in history and keep blocking the address range if the same or an overlapping pool is added again.
|
||
- Light and dark (amber terminal) theme, switchable in the page header and remembered per browser.
|
||
- JSON REST API (`/api/v1/`) with Bearer token authentication; tokens are managed in the web UI.
|
||
- Audit log of all operations.
|
||
- Markdown, CSV, and JSON exports.
|
||
- SQLite migrations run automatically on startup.
|
||
- Backup and restore scripts.
|
||
|
||
## Architecture
|
||
|
||
- **Rust** edition 2024
|
||
- **Axum** + **Tokio**
|
||
- **SQLx** with **SQLite**
|
||
- **Askama** for HTML templates
|
||
- **HTMX** from a local static file
|
||
- Custom CSS without external frameworks
|
||
- CIDR/IP calculations using `ipnet`
|
||
|
||
## Quick start
|
||
|
||
```bash
|
||
cd sub-aligator
|
||
cp .env.example .env
|
||
docker compose up -d --build
|
||
```
|
||
|
||
Open a browser at:
|
||
|
||
```
|
||
http://SERVER_IP:8080
|
||
```
|
||
|
||
On first launch you will be asked to create the first pool. The default port can be changed in `.env` via the `APP_PORT` variable.
|
||
|
||
## Persistent data
|
||
|
||
Docker Compose creates local directories:
|
||
|
||
- `./data` – SQLite database
|
||
- `./exports` – generated exports (Markdown, CSV, JSON)
|
||
- `./backups` – database backups
|
||
|
||
These directories are bind-mounted into the container, so data persists even if the container is removed.
|
||
|
||
## Backup and restore
|
||
|
||
### Backup
|
||
|
||
```bash
|
||
./scripts/backup.sh
|
||
```
|
||
|
||
The script creates a consistent database backup using `sqlite3 .backup` with a UTC timestamp in the filename. Backups are stored in `./backups`.
|
||
|
||
### Restore
|
||
|
||
```bash
|
||
./scripts/restore.sh ./backups/subnet-allocator.db.20240930T120000Z.backup
|
||
```
|
||
|
||
**Warning:** restore overwrites the current database. Stop the container and back up the current state before restoring.
|
||
|
||
### Automating backups
|
||
|
||
On the host you can use a systemd timer or cron. Example cron entry:
|
||
|
||
```cron
|
||
0 2 * * * /path/to/sub-aligator/scripts/backup.sh >> /var/log/subnet-allocator-backup.log 2>&1
|
||
```
|
||
|
||
## Exports
|
||
|
||
After every successful allocation or release, read-only exports are regenerated:
|
||
|
||
- `/app/exports/allocations.md`
|
||
- `/app/exports/allocations.csv`
|
||
- `/app/exports/allocations.json`
|
||
|
||
They are also available for download via endpoints:
|
||
|
||
- `GET /exports/allocations.md`
|
||
- `GET /exports/allocations.csv`
|
||
- `GET /exports/allocations.json`
|
||
|
||
The Markdown export contains a separate section for each pool with a table of active and reserved allocations. CSV and JSON include all allocations, including released ones.
|
||
|
||
## REST API
|
||
|
||
The application provides a JSON REST API under `/api/v1/` for integrations. All API requests are authenticated with a Bearer token:
|
||
|
||
```
|
||
Authorization: Bearer sa_...
|
||
```
|
||
|
||
### Managing API tokens
|
||
|
||
Tokens are managed in the web UI on the **API tokens** page (link in the footer):
|
||
|
||
- **Create** – enter a descriptive name (e.g. `monitoring-integration`). The plaintext token is shown **only once** right after creation; only its SHA-256 hash is stored in the database.
|
||
- **Revoke** – disables the token immediately. The record is kept for audit purposes.
|
||
- **Delete** – permanently removes a revoked token.
|
||
|
||
Every API action is recorded in the audit log with the actor `api:<token name>`.
|
||
|
||
### Endpoints
|
||
|
||
| Method | Path | Description |
|
||
|--------|------|-------------|
|
||
| `GET` | `/api/v1/pools` | List enabled pools with allocation counts |
|
||
| `GET` | `/api/v1/pools/{slug}/next?prefix_length=30` | Preview the next free subnet (informational only) |
|
||
| `GET` | `/api/v1/allocations` | List allocations; filters: `status`, `pool`, `q`, `limit` (max 500), `offset` |
|
||
| `GET` | `/api/v1/allocations/{id}` | Allocation detail including address range |
|
||
| `POST` | `/api/v1/allocations` | Allocate the next free subnet |
|
||
| `POST` | `/api/v1/allocations/{id}/release` | Release an allocation |
|
||
|
||
The allocation is always recalculated inside a write transaction – the preview from `/next` may differ from the actually allocated subnet under concurrent use.
|
||
|
||
### Examples
|
||
|
||
```bash
|
||
TOKEN="sa_your_token_here"
|
||
BASE="http://SERVER_IP:8080"
|
||
|
||
# List pools
|
||
curl -s -H "Authorization: Bearer $TOKEN" $BASE/api/v1/pools
|
||
|
||
# Preview the next free /30 in pool "vpn"
|
||
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/api/v1/pools/vpn/next?prefix_length=30"
|
||
|
||
# Allocate the next free /30 (note is mandatory, min. 3 characters)
|
||
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"pool_slug":"vpn","prefix_length":30,"note":"customer X edge","ticket":"INC-123"}' \
|
||
$BASE/api/v1/allocations
|
||
|
||
# List active allocations in pool "vpn"
|
||
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/api/v1/allocations?status=active&pool=vpn"
|
||
|
||
# Release allocation 42 (release note is mandatory)
|
||
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"release_note":"no longer needed"}' \
|
||
$BASE/api/v1/allocations/42/release
|
||
```
|
||
|
||
Errors are returned as `{"error":"message"}` with an appropriate status code (`400` invalid request, `401` missing/invalid token, `404` not found, `409` pool full or allocation already released).
|
||
|
||
## Security notes
|
||
|
||
- The web UI **has no authentication**. Use it only behind a VPN/LAN/firewall.
|
||
- The REST API requires a Bearer token; only token hashes (SHA-256) are stored.
|
||
- CSRF tokens are required for POST forms in the web UI.
|
||
- Security headers are set: `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, `Content-Security-Policy`.
|
||
- The application has no default Internet exposure; port binding is handled by Docker Compose.
|
||
- `/healthz` returns only `{"status":"ok"}`.
|
||
|
||
## Adding authentication later via Caddy + Authentik
|
||
|
||
The application is prepared for authentication through a trusted reverse proxy. The function `get_actor_from_request()` in `src/audit.rs` currently returns `local`, but can be updated to read a header such as `X-Authentik-Username`.
|
||
|
||
Example Caddy configuration with Authentik:
|
||
|
||
```caddy
|
||
subnet.example.com {
|
||
reverse_proxy /outpost.goauthentik.io/* http://authentik:9000
|
||
forward_auth http://authentik:9000 {
|
||
uri /outpost.goauthentik.io/auth/caddy
|
||
copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email
|
||
}
|
||
reverse_proxy subnet-allocator:8080
|
||
}
|
||
```
|
||
|
||
After updating `get_actor_from_request()`, the audit log will record the real user.
|
||
|
||
## First-fit allocation example
|
||
|
||
1. First request `/30` receives `10.91.0.0/30`.
|
||
2. Next request `/24` receives `10.91.1.0/24` (avoids the occupied `/30`).
|
||
3. Another request `/30` fills the gap and receives `10.91.0.4/30`.
|
||
|
||
The algorithm always respects CIDR alignment and finds the first free space large enough.
|
||
|
||
## Endpoints
|
||
|
||
- `GET /` – main page
|
||
- `GET /healthz` – healthcheck
|
||
- `GET /setup` – first-run pool setup
|
||
- `GET /pools` – pool management
|
||
- `POST /pools/new` – create a new pool
|
||
- `POST /pools/{id}/delete` – delete a pool (allocations are kept)
|
||
- `GET /tokens` – API token management
|
||
- `POST /tokens/new` – create an API token
|
||
- `POST /tokens/{id}/revoke` – revoke an API token
|
||
- `POST /tokens/{id}/delete` – delete a revoked API token
|
||
- `GET /partials/pools/{slug}/preview?prefix_length=24` – HTMX preview
|
||
- `POST /allocations` – create an allocation
|
||
- `GET /allocations/{id}` – allocation detail
|
||
- `POST /allocations/{id}/release` – release an allocation
|
||
- `GET /exports/allocations.{md,csv,json}` – exports
|
||
- `GET|POST /api/v1/...` – JSON REST API (see above)
|
||
|
||
## Troubleshooting
|
||
|
||
### Container does not start
|
||
|
||
```bash
|
||
docker compose logs -f subnet-allocator
|
||
```
|
||
|
||
### Database is locked
|
||
|
||
If the process terminates unexpectedly, a `-wal` or `-shm` file may remain. Stop the container and check `./data`.
|
||
|
||
### Exports are not generated
|
||
|
||
Check the permissions of the `./exports` directory and the `APP_EXPORTS_DIR` variable.
|
||
|
||
## Licence
|
||
|
||
MIT. See the `LICENSE` file.
|