Skip to main content

Router-agnostic network configuration manager

Project description

routerless

CI PyPI Python 3.13+

Infrastructure-as-Code for your home network.

Declare DHCP reservations, port forwards, and firewall rules in YAML — plan to preview changes, apply to sync them to your router. Supports Bbox Ultim, OpenWrt, and QNAP Qhora out of the box.

Like Terraform, but for your home network:

$ routerless plan --target bbox
Section: dhcp  (+3  ~1)
  + ADD     lease "NAS"   AA:BB:CC:DD:EE:FF  →  192.168.1.20
  ~ CHANGE  lease "Pi"    ip: 192.168.1.50 → 192.168.1.51

Section: nat   ✓ no changes

Plan: 3 to add, 1 to change.

Supported Routers

Type Authentication Protocol
Bbox Ultim (bbox_ultim) Cookie (password) HTTPS REST
OpenWrt (openwrt) SSH key or password SSH + UCI
QNAP Qhora 301W (qnap_qhora) SSH password SSH + UCI

Installation

Requirements: Python 3.13+

# Install from PyPI
pip install routerless

# Or install from source (for development)
git clone https://github.com/you/routerless
cd routerless
pip install -e ".[dev]"

Quick Start

# 1. Create a new config from template
routerless init ~/my-network
cd ~/my-network

# 2. Fill in your router credentials
cp secrets.yaml.example secrets.yaml
# edit secrets.yaml

# 3. Validate
routerless validate

# 4. Preview what would change (like terraform plan)
routerless plan --target bbox

# 5. Apply
routerless apply --target bbox

Configuration path resolution

All commands accept an optional CONFIG argument. Routerless resolves it as follows:

Argument Resolved file
(omitted) ./configuration.yaml (current directory)
./my-network ./my-network/configuration.yaml
./my-network/prod.yaml ./my-network/prod.yaml
# All equivalent when run from the config directory
routerless plan --target bbox
routerless plan --target bbox .
routerless plan --target bbox configuration.yaml

# Point at a different directory
routerless plan --target bbox ~/my-network
routerless plan --target bbox ~/my-network/configuration.yaml

Commands

init

Scaffold a new configuration directory with ready-to-edit template files.

routerless init ~/my-network
routerless init ./config --force   # overwrite existing files

Creates:

my-network/
  configuration.yaml   — main config (targets + !include sections)
  secrets.yaml.example — credential template (copy to secrets.yaml)
  dhcp.yaml            — DHCP settings and static leases
  nat.yaml             — port-forwarding rules
  firewall.yaml        — firewall rules
  .gitignore           — pre-configured to ignore secrets.yaml

Existing files are never overwritten unless --force is passed.

validate

Parse and validate the configuration file and all its includes.

routerless validate
routerless validate ~/my-network
routerless validate ~/my-network/configuration.yaml

plan

Preview what apply would add, change, or delete — no writes to the device.

routerless plan --target bbox
routerless plan --target bbox ~/my-network
routerless plan --target openwrt --section dhcp --section nat ~/my-network/configuration.yaml

Output format:

Comparing local config against target 'bbox' (bbox_ultim)…

Section: dhcp  (+1  ~2)
  + ADD     lease "NAS-Server"       AA:BB:CC:DD:EE:FF  →  192.168.1.20
  ~ CHANGE  lease "Hub"              BB:CC:DD:EE:FF:00  ip: 192.168.1.10 → 192.168.1.11
  ~ CHANGE  lease "Home Assistant"   CC:DD:EE:FF:00:11  hostname: 'homeassistant' → 'home-assistant'

Section: nat  ✓ no changes

Plan: 1 to add, 2 to change.
      Run routerless apply --target bbox --section dhcp to apply.

apply

Apply one or more sections to a target device.

# Apply all sections (from current directory)
routerless apply --target bbox

# Apply specific sections only
routerless apply --target openwrt --section dhcp
routerless apply --target openwrt --section nat --section firewall

# Point at a specific directory or file
routerless apply --target openwrt ~/my-network
routerless apply --target openwrt --section nat --section firewall ~/my-network/configuration.yaml

import

Read the current device configuration and write it to section files.

# Import all sections to current directory
routerless import --target bbox

# Import only NAT rules
routerless import --target bbox --section nat

# Import from a specific config directory
routerless import --target bbox --output-dir ./imported ~/my-network

Writes dhcp.yaml, nat.yaml, and firewall.yaml into the output directory (. by default). If a file already exists, a unified diff is displayed and you are prompted:

Section: dhcp  file: ./dhcp.yaml
--- dhcp.yaml (current)
+++ dhcp.yaml (device)
@@ -1,4 +1,4 @@
 static_leases:
-  - name: OldName
+  - name: NAS

Action [o=override / a=append / s=skip] > 
  • Override — replace the file with the device content.
  • Append — keep existing entries and add new ones from the device (deduplicated by MAC / port+protocol / rule name). When importing to a new directory, import also generates a configuration.yaml with the target block and !include references for each imported section.
# Bootstrap a new config dir from a live device
routerless import --target bbox ../new-site
# → creates new-site/dhcp.yaml, nat.yaml, firewall.yaml, configuration.yaml

dump

Read the current configuration from a device and print it as YAML.

routerless dump --target bbox
routerless dump --target openwrt --output backup.yaml ~/my-network

diff

Show a unified diff between the local config file and the running device config.

routerless diff --target bbox --section dhcp
routerless diff --target bbox ~/my-network

status

Show general device status (WAN/LAN IPs, WiFi state, uptime, connected devices).

routerless status --target bbox
routerless status --target openwrt ~/my-network
Model             : Bbox Ultim  (ABC123)
LAN IP            : 192.168.1.254
WAN IP            : 82.x.x.x
Internet          : Connected
WiFi 2.4 GHz      : ON
WiFi 5 GHz        : ON
Devices           : 14
Uptime            : 12d 3h 42m

devices

List connected (or all known) devices.

routerless devices --target bbox
routerless devices --target openwrt --all ~/my-network
Device IP        MAC                Hostname          Type Link
----------------------------------------------------------------
192.168.1.10     AA:BB:CC:DD:EE:FF  mynas                  Ethernet port 1
192.168.1.20     11:22:33:44:55:66  laptop                 Wifi 5 RSSI -62

wifi status / on / off

Inspect or toggle WiFi radios.

routerless wifi status --target bbox
routerless wifi on  --target bbox
routerless wifi off --target openwrt ~/my-network

Configuration

config/configuration.yaml

version: "1.0"

targets:
  bbox:
    type: bbox_ultim
    host: !secret bbox_host
    password: !secret bbox_password

  openwrt:
    type: openwrt
    host: !secret openwrt_host
    ssh_user: root
    ssh_key: !secret openwrt_ssh_key
    ssh_port: 22

  qhora:
    type: qnap_qhora
    host: !secret qhora_host
    ssh_user: admin
    ssh_password: !secret qhora_ssh_password
    ssh_port: 22200

dhcp:     !include dhcp.yaml
nat:      !include nat.yaml
firewall: !include firewall.yaml

config/secrets.yaml (never committed)

bbox_host: 192.168.1.254
bbox_password: your_password

openwrt_host: 192.168.1.1
openwrt_ssh_key: ~/.ssh/id_ed25519

qhora_host: 192.168.1.2
qhora_ssh_password: your_password

Copy config/secrets.yaml.example to get started.

config/dhcp.yaml

subnet: "192.168.1.0/24"
gateway: "192.168.1.1"
static_leases: !include_dir_merge_list static_leases

Each file in static_leases/ is a YAML list:

# static_leases/servers.yaml
- name: "Home Assistant"          # friendly display label (any characters)
  mac: "AA:BB:CC:DD:EE:FF"
  ip: "192.168.1.20"
  hostname: home-assistant         # DNS-safe identifier sent to the router API

- name: NAS                       # if hostname is omitted, name is used as-is
  mac: "11:22:33:44:55:66"
  ip: "192.168.1.21"

hostname is the value pushed to the router (e.g. Bbox hostname field) and is used by plan / apply to detect changes. It must be DNS-safe (letters, digits, hyphens — no spaces or accents). When omitted, name is used in its place. name is a human-readable label used only for display in CLI output.

config/nat.yaml

port_forwards:
  - name: "Home Assistant"
    protocol: tcp
    external_port: 8123
    internal_ip: "192.168.1.21"
    internal_port: 8123

config/firewall.yaml

rules:
  - name: "Block IoT to WAN"
    direction: forward
    src: iot
    dest: wan
    action: DROP

YAML Custom Tags

Tag Behaviour
!include <file> Inline another YAML file (path relative to the current file)
!include_dir_merge_list <dir> Merge all *.yaml files in a directory as a single list
!include_dir_named <dir> Dict keyed by filename stem
!secret <key> Look up a value from secrets.yaml (always resolved from config root)

Project Structure

routerless/
├── cli.py                  # Click CLI entry point
├── yaml_loader.py          # Custom YAML tags
├── models/
│   ├── config.py           # Pydantic v2 models (NetworkConfig, DHCPConfig…)
│   └── status.py           # Read-only dataclasses (AdapterStatus, WifiRadio…)
└── adapters/
    ├── base.py             # BaseAdapter ABC
    ├── bbox_ultim.py       # Bbox Ultim — HTTPS REST
    ├── openwrt.py          # OpenWrt — SSH + UCI
    └── qnap_qhora.py       # QNAP Qhora — delegates to OpenWrtAdapter
config/
├── configuration.yaml
├── secrets.yaml.example
├── dhcp.yaml
├── nat.yaml
├── firewall.yaml
└── static_leases/
tests/                      # pytest — all device I/O mocked
.github/
└── prompts/
    ├── add-adapter.prompt.md          # Guide: add a new router adapter
    ├── add-feature.prompt.md          # Guide: add a new CLI command/feature
    └── discover-bbox-interface.prompt.md  # Capture Bbox XHR via mitmproxy

Development

# Install in editable mode with dev dependencies
pip install -e ".[dev]"

# Lint
ruff check .

# Run all tests
pytest

# Run a specific test file
pytest tests/test_import.py -v
pytest tests/test_bbox.py -v

All tests mock device I/O — no real router needed. Tests must stay green before and after every change.

Contributing

Contributions are welcome! Please read CONTRIBUTING.md for the full guide covering workflow, code style, test conventions, commit messages, and how to add new adapters or CLI features.

Adding a New Adapter

Use the built-in prompt:

# In VS Code with GitHub Copilot, reference:
# .github/prompts/add-adapter.prompt.md

Or follow these steps manually:

  1. Add a TargetType enum value in routerless/models/config.py
  2. Create routerless/adapters/my_router.py extending BaseAdapter
  3. Register it in _ADAPTER_MAP in routerless/cli.py
  4. Add tests in tests/test_my_router.py

Adapter Notes

Bbox Ultim

  • Auth is cookie-based. Login requires Referer and Origin headers; after login a CSRF btoken is fetched from GET /device/token and appended to every mutating request.
  • The Bbox redirects local HTTP to https://mabbox.bytel.fr/api/v1 — this is the effective base URL.
  • Rate limit: 3 failed logins → up to 1200 s lockout.
  • Firewall endpoint unknown — apply_firewall() raises NotImplementedError.
  • DHCP hostname field: GET /dhcp/clients returns hostname but not device. The adapter sends hostname = lease.hostname or lease.name (DNS-safe, no spaces) to the Bbox API. The friendly name is sent as device (write-only, not returned by GET). plan and apply both compare on hostname to avoid phantom changes.

OpenWrt / QNAP Qhora

  • Uses UCI over SSH (paramiko). Host key policy: RejectPolicy.
  • Qhora default SSH port: 22200. Enable SSH by holding the WPS button for 12 s.
  • apply_* methods are idempotent: they read current state before writing.

License

MIT — see the LICENSE file for details.


Contributing · Security · Code of Conduct

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

routerless-0.1.0.tar.gz (56.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

routerless-0.1.0-py3-none-any.whl (35.6 kB view details)

Uploaded Python 3

File details

Details for the file routerless-0.1.0.tar.gz.

File metadata

  • Download URL: routerless-0.1.0.tar.gz
  • Upload date:
  • Size: 56.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for routerless-0.1.0.tar.gz
Algorithm Hash digest
SHA256 2bff24863f3f76343451daaf106cbd2d9f1ce9bc47d7bf6d4b5fb1fb36f3f50d
MD5 ee6885c7f5691f76b72fdc3e62ec0bcb
BLAKE2b-256 5142f8a2bd0145229b55af448b49fb696ef46e617d76c4d2acaaa98336246b73

See more details on using hashes here.

Provenance

The following attestation bundles were made for routerless-0.1.0.tar.gz:

Publisher: release.yml on grogui42/routerless

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file routerless-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: routerless-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 35.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for routerless-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 13981e7bef13b878f0d752e740192e8ef7f7ce289d38a022fabc0dc8a8879a99
MD5 76901bb1d9100aba6503f0f633bafe90
BLAKE2b-256 52bbea9a8b10adf66acf44a9785d7b3220c47b6d0e5c527366683ffab6627989

See more details on using hashes here.

Provenance

The following attestation bundles were made for routerless-0.1.0-py3-none-any.whl:

Publisher: release.yml on grogui42/routerless

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page