This repository focuses specifically on configuring OpenWRT routers at scale, addressing the gap we found in existing solutions for flexibly managing dozens or hundreds of OpenWRT devices.
It is part of our mesh overlay network. The solution consists of three core components: Tailscale as the client and tunnel interface, Headscale as the self-hosted control plane, and OpenWRT as the router operating system. Both Tailscale and Headscale are well-documented and straightforward to deploy—their upstream projects provide excellent guidance for deployment and configuration.
This overlay network creates secure, encrypted WireGuard tunnels between distributed OpenWRT routers using direct peer-to-peer connections with automatic NAT traversal and fallback relay servers. This architecture eliminates central bottlenecks and enables high-throughput, low-latency communication between sites. The routing is fully automated—each router automatically advertises its local subnets to the mesh network, so devices across different physical locations can communicate without manual route configuration.
Key advantages of this architecture:
- Security: End-to-end encrypted WireGuard tunnels with centralized access control
- Flexibility: Direct peer-to-peer connections with automatic NAT traversal
- Scalability: Peer-to-peer connections avoid bottlenecks and enable high-throughput, low-latency communication
- Cost: No per-device licensing fees, open-source components throughout
This is particularly useful for distributed infrastructure and multicloud setups where traditional VPN solutions become complex, expensive, and difficult to manage.
The configuration management system is built on three pillars:
- Jinja2 Templates: Reusable configuration templates that define the structure of OpenWRT configurations while allowing for device-specific variables
- YAML Configuration Files: Human-readable configuration data that defines router-specific parameters, network settings, and deployment variables
- Ansible Logic: Automation that orchestrates the building, validation, importing, and applying of configurations to target routers
This separation of concerns ensures that configuration templates remain clean and reusable, while device-specific data is maintained separately. The Ansible logic renders the templates and imports them to the OpenWRT routers using the existing UCI. Furthermore, it renders readable diffs to examine changes before applying.
The mesh overlay network is designed for scenarios requiring secure connectivity across distributed infrastructure:
- Multi-site deployments: Connect branch offices, retail locations, or warehouses without complex site-to-site VPN configurations
- Remote management: Secure access to distributed infrastructure without exposing management interfaces to the public internet
- Hybrid environments: Bridge on-premises and cloud infrastructure seamlessly
- IoT and edge computing: Create secure overlay networks for distributed IoT devices or edge computing nodes
By combining Tailscale's ease of use, Headscale's self-hosted control plane, and OpenWRT's flexibility, this solution provides enterprise-grade mesh networking with full control over your infrastructure.
In the Role Pretasks we make sure, that there are no invalid values set in the host- and groups vars that are not supported by the OpenWRT config or your generate a conflict when starting the services on OpenWRT (like duplicated static ips)
The role Config is used for configuring OpenWRT (as system itself) and the packages running on it. For each uci based package you need to configure, you can create a jinja2 templated config in the templates folder. The templated config allows you to have a standardized config across all OpenWRT gateways, have less config duplication, but also have different values for each gateway via host- and group vars.
To get a clean diff to the current config, we render the jinja templates into a temp folder and filter out all package names and comments, as UCI removes them after an apply before comparing it to the current config in /etc/config/ During a ansible run every jinja2 file in the templates folder will by applied using uci import and reloaded into the running config. This allows to apply changes with mostly no impact to the business, as it only loads differentiating config and reloads the needed services instead of restarting it. (Only the firewall service will be restarted as a reload sometimes does not work as intended)
As the first run on a clean/empty OpenWRT Gateway will make many config changes and some settings affect multiple packages (e.g. new interfaces --> dhcp, network, firewall ) we check if it's the first run by checking if there is a "ansible_first_run_complete" file. If not, the config files will only be copied into the config directory, but not loaded. After the last config was copied, the system reboots to load all the config files at once.
This role adds our own wan failover logic as service on OpenWRT Normally you would use MWAN3 as WAN failover service on OpenWRT. But we had some compatibility problems together with tailscale. The wan_failover script checks if it can access the internet via the two wan interfaces and switches the wan interface by deleting and adding route table entries if the "main" interface is considered offline.
No matter how small, we value every contribution! If you wish to contribute,
- Please create an issue first - this way, we can discuss the feature and flesh out the nitty-gritty details
- Fork the repository, implement the feature and submit a pull request
- Your feature will be added once the pull request is merged