Files
ducoterra e360546de9
Build and Push Container / build-and-push (push) Successful in 19s
init
2026-08-01 19:07:05 -04:00

125 lines
4.6 KiB
Markdown

# UniFi Firewall Updater
Automatically updates UniFi Dream Machine firewall rules with your current public IPv4 and IPv6 addresses via the UniFi Network API.
## Overview
This tool:
- Fetches your public IPv4 and IPv6 addresses
- Reads firewall rule definitions from a YAML config file
- Creates or updates policies on your UniFi controller to use those addresses
- Optionally sends notifications via NTFY when rules change
Designed to run as a one-shot script (e.g., via cron or systemd timer) whenever your public IP changes.
## Quick Start
1. Copy `.env.example` to `.env` and fill in your UniFi credentials
2. Edit `config/rules.yaml` to define your firewall rules
3. Install dependencies and run:
```bash
uv sync
uv run main.py
```
## Configuration
### Environment Variables
| Variable | Required | Description |
| ------------------ | -------- | -------------------------------------------------------- |
| `UNIFI_HOST` | Yes | UniFi controller URL (e.g., `https://10.1.0.1`) |
| `UNIFI_SITE_ID` | Yes | UniFi site ID |
| `UNIFI_API_TOKEN` | Yes | UniFi Network API token |
| `UNIFI_VERIFY_SSL` | No | Verify SSL certificates (default: `false`) |
| `CONFIG_FILE` | No | Path to rules YAML (default: `config/rules.yaml`) |
| `LOG_LEVEL` | No | Log level: DEBUG, INFO, WARNING, ERROR (default: `INFO`) |
| `DEBUG` | No | Attach debugpy on port 5678 (default: `false`) |
| `NTFY_URL` | No | NTFY server URL for notifications |
| `NTFY_TOPIC` | No | NTFY topic to publish to |
| `NTFY_API_KEY` | No | NTFY API key for authentication |
### Rules YAML
Each rule defines a firewall policy that will use your public IP as the source:
```yaml
rules:
- name: "Allow External to Gateway HTTP(S)"
source_zone: "External"
dest_zone: "DMZ"
ip_version: "IPV6"
action: "ALLOW"
allow_return_traffic: true
protocol: "tcp"
dest_ports: [80, 443]
dest_port_ranges:
- start: 8000
stop: 8080
logging_enabled: false
enabled: true
```
| Field | Required | Description |
| ---------------------- | -------- | ------------------------------------------------ |
| `name` | Yes | Unique policy name |
| `source_zone` | Yes | Source zone name (e.g., `WAN`, `External`) |
| `dest_zone` | Yes | Destination zone name (e.g., `LAN`, `DMZ`) |
| `ip_version` | No | `IPV4` or `IPV6` (default: `IPV4`) |
| `action` | No | `ALLOW`, `BLOCK`, or `REJECT` (default: `ALLOW`) |
| `allow_return_traffic` | No | Allow return traffic (default: `true`) |
| `protocol` | No | Protocol filter (e.g., `tcp`, `udp`) |
| `dest_ports` | No | List of destination ports |
| `dest_port_ranges` | No | List of `{start, stop}` port ranges |
| `logging_enabled` | No | Enable policy logging (default: `false`) |
| `enabled` | No | Enable the policy (default: `true`) |
## Development
```bash
uv run pytest -v # Run tests (all mocked, no live API calls)
uv run ruff check # Lint
uv run pyright # Type check (strict mode)
```
Run a single test:
```bash
uv run pytest tests/test_unifi_firewall.py::TestBuildPolicyPayload::test_with_dest_ports -v
```
## Container
Build and run with Podman:
```bash
podman build -t unifi-firewall .
podman run --env-file .env -v $(pwd)/config:/app/config unifi-firewall
```
Config is mounted at runtime via volume. Environment variables are passed at runtime.
## How It Works
1. Loads rules from `config/rules.yaml`
2. Fetches public IPv4/IPv6 via `curl ifconfig.me`
3. Lists zones from UniFi to resolve zone names to IDs
4. For each rule:
- Finds existing policy by name
- Skips if IP already matches
- Updates if policy exists with different IP
- Creates if policy does not exist
5. Sends NTFY notification if rules were created, updated, or failed
## API Details
Uses UniFi Network API v1 at:
```
{UNIFI_HOST}/proxy/network/integration/v1/sites/{UNIFI_SITE_ID}
```
Authentication via `X-API-Key` header. See `network_v10.4.57_openapi.json` for the full schema.