- Fixed network topology diagram in pair README: 192.168.10.x -> 192.168.1.x to match the actual code (OPNsense boots on .1 of 192.168.1.0/24) - Added explanation of NIC juggling to the diagram section - Updated single-VM "What's next" to link to pair example (was "in progress") - Added opnsense_pair_integration to examples/README.md table and category
235 lines
9.6 KiB
Markdown
235 lines
9.6 KiB
Markdown
# Use Case: OPNsense VM Integration
|
|
|
|
Boot a real OPNsense firewall in a local KVM virtual machine and configure it entirely through Harmony — load balancer, DHCP, TFTP, VLANs, firewall rules, NAT, VIPs, and link aggregation. Fully automated, zero manual steps, CI-friendly.
|
|
|
|
This is the best way to discover Harmony: you'll see 11 different Scores configure a production firewall through type-safe Rust code and the OPNsense REST API.
|
|
|
|
## What you'll have at the end
|
|
|
|
A local OPNsense VM fully configured by Harmony with:
|
|
- HAProxy load balancer with health-checked backends
|
|
- DHCP server with static host bindings and PXE boot options
|
|
- TFTP server serving boot files
|
|
- Prometheus node exporter enabled
|
|
- 2 VLANs on the LAN interface
|
|
- Firewall filter rules, outbound NAT, and bidirectional NAT
|
|
- Virtual IPs (IP aliases)
|
|
- Port forwarding (DNAT) rules
|
|
- LAGG interface (link aggregation)
|
|
|
|
All applied idempotently through the OPNsense REST API — the same Scores used in production bare-metal deployments.
|
|
|
|
## Prerequisites
|
|
|
|
- **Linux** with KVM support (Intel VT-x/AMD-V enabled in BIOS)
|
|
- **libvirt + QEMU** installed and running (`libvirtd` service active)
|
|
- **~10 GB** free disk space
|
|
- **~15 minutes** for the first run (image download + OPNsense firmware update)
|
|
- Docker running (if installed — the setup handles compatibility)
|
|
|
|
Supported distributions: Arch, Manjaro, Fedora, Ubuntu, Debian.
|
|
|
|
## Quick start (single command)
|
|
|
|
```bash
|
|
# One-time: install libvirt and configure permissions
|
|
./examples/opnsense_vm_integration/setup-libvirt.sh
|
|
newgrp libvirt
|
|
|
|
# Verify
|
|
cargo run -p opnsense-vm-integration -- --check
|
|
|
|
# Boot + bootstrap + run all 11 Scores (fully unattended)
|
|
cargo run -p opnsense-vm-integration -- --full
|
|
```
|
|
|
|
That's it. No browser clicks, no manual SSH configuration, no wizard interaction.
|
|
|
|
## What happens step by step
|
|
|
|
### Phase 1: Boot the VM
|
|
|
|
Downloads the OPNsense 26.1 nano image (~350 MB, cached after first run), injects a `config.xml` with virtio NIC assignments, creates a 4 GiB qcow2 disk, and boots the VM with 4 NICs:
|
|
|
|
```
|
|
vtnet0 = LAN (192.168.1.1/24) -- management
|
|
vtnet1 = WAN (DHCP) -- internet access
|
|
vtnet2 = LAGG member 1 -- for aggregation test
|
|
vtnet3 = LAGG member 2 -- for aggregation test
|
|
```
|
|
|
|
### Phase 2: Automated bootstrap
|
|
|
|
Once the web UI responds (~20 seconds after boot), `OPNsenseBootstrap` takes over:
|
|
|
|
1. **Logs in** to the web UI (root/opnsense) with automatic CSRF token handling
|
|
2. **Aborts the initial setup wizard** via the OPNsense API
|
|
3. **Enables SSH** with root login and password authentication
|
|
4. **Changes the web GUI port** to 9443 (prevents HAProxy conflicts on standard ports)
|
|
5. **Restarts lighttpd** via SSH to apply the port change
|
|
|
|
No browser, no Playwright, no expect scripts — just HTTP requests with session cookies and SSH commands.
|
|
|
|
### Phase 3: Run 11 Scores
|
|
|
|
Creates an API key via SSH, then configures the entire firewall:
|
|
|
|
| # | Score | What it configures |
|
|
|---|-------|--------------------|
|
|
| 1 | `LoadBalancerScore` | HAProxy with 2 frontends (ports 16443 and 18443), backends with health checks |
|
|
| 2 | `DhcpScore` | DHCP range, 2 static host bindings (MAC-to-IP), PXE boot options |
|
|
| 3 | `TftpScore` | TFTP server serving PXE boot files |
|
|
| 4 | `NodeExporterScore` | Prometheus node exporter on OPNsense |
|
|
| 5 | `VlanScore` | 2 test VLANs (tags 100 and 200) on vtnet0 |
|
|
| 6 | `FirewallRuleScore` | Firewall filter rules (allow/block with logging) |
|
|
| 7 | `OutboundNatScore` | Source NAT rule for outbound traffic |
|
|
| 8 | `BinatScore` | Bidirectional 1:1 NAT |
|
|
| 9 | `VipScore` | Virtual IPs (IP aliases for CARP/HA) |
|
|
| 10 | `DnatScore` | Port forwarding rules |
|
|
| 11 | `LaggScore` | Link aggregation group (failover on vtnet2+vtnet3) |
|
|
|
|
Each Score reports its status:
|
|
|
|
```
|
|
[LoadBalancerScore] SUCCESS in 2.2s -- Load balancer configured 2 services
|
|
[DhcpScore] SUCCESS in 1.4s -- Dhcp Interpret execution successful
|
|
[VlanScore] SUCCESS in 0.2s -- Configured 2 VLANs
|
|
...
|
|
PASSED -- All OPNsense integration tests successful
|
|
```
|
|
|
|
### Phase 4: Verify
|
|
|
|
After all Scores run, the integration test verifies each configuration via the REST API:
|
|
- HAProxy has 2+ frontends
|
|
- Dnsmasq has 2+ static hosts and a DHCP range
|
|
- TFTP is enabled
|
|
- Node exporter is enabled
|
|
- 2+ VLANs exist
|
|
- Firewall filter rules are present
|
|
- VIPs, DNAT, BINAT, SNAT rules are configured
|
|
- LAGG interface exists
|
|
|
|
## Explore in the web UI
|
|
|
|
After the test completes, open https://192.168.1.1:9443 (login: root/opnsense) and explore:
|
|
|
|
- **Services > HAProxy > Settings** -- frontends, backends, servers with health checks
|
|
- **Services > Dnsmasq DNS > Settings** -- host overrides (static DHCP entries)
|
|
- **Services > TFTP** -- enabled with uploaded files
|
|
- **Interfaces > Other Types > VLAN** -- two tagged VLANs
|
|
- **Firewall > Automation > Filter** -- filter rules created by Harmony
|
|
- **Firewall > NAT > Port Forward** -- DNAT rules
|
|
- **Firewall > NAT > Outbound** -- SNAT rules
|
|
- **Firewall > NAT > One-to-One** -- BINAT rules
|
|
- **Interfaces > Virtual IPs > Settings** -- IP aliases
|
|
- **Interfaces > Other Types > LAGG** -- link aggregation group
|
|
|
|
## Clean up
|
|
|
|
```bash
|
|
cargo run -p opnsense-vm-integration -- --clean
|
|
```
|
|
|
|
Destroys the VM and virtual networks. The cached OPNsense image is kept for next time.
|
|
|
|
## How it works
|
|
|
|
### Architecture
|
|
|
|
```
|
|
Your workstation OPNsense VM (KVM)
|
|
+--------------------+ +---------------------+
|
|
| Harmony | | OPNsense 26.1 |
|
|
| +---------------+ | REST API | +---------------+ |
|
|
| | OPNsense |----(HTTPS:9443)---->| | API + Plugins | |
|
|
| | Scores | | | +---------------+ |
|
|
| +---------------+ | SSH | +---------------+ |
|
|
| +---------------+ |----(port 22)----->| | FreeBSD Shell | |
|
|
| | OPNsense- | | | +---------------+ |
|
|
| | Bootstrap | | HTTP session | |
|
|
| +---------------+ |----(HTTPS:443)--->| (first-boot only) |
|
|
| +---------------+ | | |
|
|
| | opnsense- | | | LAN: 192.168.1.1 |
|
|
| | config | | | WAN: DHCP |
|
|
| +---------------+ | +---------------------+
|
|
+--------------------+
|
|
```
|
|
|
|
The stack has four layers:
|
|
|
|
1. **`opnsense-api`** -- auto-generated typed Rust client from OPNsense XML model files
|
|
2. **`opnsense-config`** -- high-level configuration modules (DHCP, firewall, load balancer, etc.)
|
|
3. **`OPNsenseBootstrap`** -- first-boot automation via HTTP session auth (login, wizard, SSH, webgui port)
|
|
4. **Harmony Scores** -- declarative desired-state descriptions that make the firewall match
|
|
|
|
### The Score pattern
|
|
|
|
```rust
|
|
// 1. Declare desired state
|
|
let score = VlanScore {
|
|
vlans: vec![
|
|
VlanDef { parent: "vtnet0", tag: 100, description: "management" },
|
|
VlanDef { parent: "vtnet0", tag: 200, description: "storage" },
|
|
],
|
|
};
|
|
|
|
// 2. Execute against topology -- queries current state, applies diff
|
|
score.interpret(&inventory, &topology).await?;
|
|
// Output: [VlanScore] SUCCESS in 0.9s -- Created 2 VLANs
|
|
```
|
|
|
|
Scores are idempotent: running the same Score twice produces the same result.
|
|
|
|
## Network architecture
|
|
|
|
```
|
|
Host (192.168.1.10) --- virbr-opn bridge --- OPNsense LAN (192.168.1.1)
|
|
192.168.1.0/24 vtnet0
|
|
NAT to internet
|
|
|
|
--- virbr0 (default) --- OPNsense WAN (DHCP)
|
|
192.168.122.0/24 vtnet1
|
|
NAT to internet
|
|
```
|
|
|
|
## Available commands
|
|
|
|
| Command | Description |
|
|
|---------|-------------|
|
|
| `--check` | Verify prerequisites (libvirtd, virsh, qemu-img) |
|
|
| `--download` | Download the OPNsense image (cached) |
|
|
| `--boot` | Create VM + automated bootstrap |
|
|
| (default) | Run integration test (assumes VM is bootstrapped) |
|
|
| `--full` | Boot + bootstrap + integration test (CI mode) |
|
|
| `--status` | Show VM state, ports, and connectivity |
|
|
| `--clean` | Destroy VM and networks |
|
|
|
|
## Environment variables
|
|
|
|
| Variable | Default | Description |
|
|
|----------|---------|-------------|
|
|
| `RUST_LOG` | (unset) | Log level: `info`, `debug`, `trace` |
|
|
| `HARMONY_KVM_URI` | `qemu:///system` | Libvirt connection URI |
|
|
| `HARMONY_KVM_IMAGE_DIR` | `~/.local/share/harmony/kvm/images` | Cached disk images |
|
|
|
|
## Troubleshooting
|
|
|
|
**VM won't start / permission denied**
|
|
Ensure your user is in the `libvirt` group and that the image directory is traversable by the qemu user. Run `setup-libvirt.sh` to fix.
|
|
|
|
**192.168.1.0/24 conflict**
|
|
If your host network already uses this subnet, the VM will be unreachable. Edit the constants in `src/main.rs` to use a different subnet.
|
|
|
|
**Web GUI didn't come up after bootstrap**
|
|
The bootstrap runs `diagnose_via_ssh()` automatically when the web UI doesn't respond. Check the diagnostic output for lighttpd status and listening ports. You can also access the serial console: `virsh -c qemu:///system console opn-integration`
|
|
|
|
**HAProxy install fails**
|
|
OPNsense may need a firmware update. The integration test handles this automatically but it may take a few minutes for the update + reboot cycle.
|
|
|
|
## What's next
|
|
|
|
- **[OPNsense Firewall Pair](../../examples/opnsense_pair_integration/README.md)** -- boot two VMs, configure CARP HA failover with `FirewallPairTopology` and `CarpVipScore`. Uses NIC link control to bootstrap both VMs sequentially despite sharing the same default IP.
|
|
- [OKD on Bare Metal](./okd-on-bare-metal.md) -- the full 7-stage OKD installation pipeline using OPNsense as the infrastructure backbone
|
|
- [PostgreSQL on Local K3D](./postgresql-on-local-k3d.md) -- a simpler starting point using Kubernetes
|