feat: add networkd subsystem and fix code review issues
Phase 1-4: Networkd subsystem - lib/network.py: systemd-networkd config renderer (.network INI files) with full schema support: [Match], [Link], [Network], [Address], [Route], [DHCPv4], [DHCPv6] sections. One Address/=DNS= line per value per spec. Route sections use #N suffix per systemd.syntax(7). - lib/network.py: generate_network_files() with 50-<name>.network prefix and stale file cleanup - lib/network.py: collect_upstream_dns() filters local/private DNS - lib/network.py: infer_dhcp_ranges() and infer_zones() helpers - daemon/handlers/network.py: routes for GET/POST /network/interfaces and full apply with DNS upstream sync to dnsmasq - webui/api/network.py: Flask blueprint for /api/network/* endpoints - webui/api: interfaces page updated with IP config inline editing - lib/state.py: networkd collector using parse_networkctl_status() - system/sudoers.d/vacuum-walld: networkctl + systemd-network rules - system/systemd/vacuum-walld.service: ReadWritePaths for /etc/systemd/network - install.sh: ACME email now optional, configured from WebUI - lib/acme.py: get_email() falls back to declarative config Phase 5: Code review fixes - daemon/server.py: path params now win over JSON body and query params in request body merge (prevents config save name override) - daemon/server.py: remove dead 'import re' - daemon/handlers/network.py: replace Path.mkdir() with sudo mkdir for /etc/systemd/network (ProtectSystem=strict compatibility) - system/sudoers.d/vacuum-walld: pin systemctl to specific commands (reload/is-active dnsmasq instead of wildcard) - system/sudoers.d/vacuum-walld: restore !requiretty and section comment - lib/network.py: remove unused _MANAGEMENT_PORTS constant - webui/api/network.py: remove redundant body[\name\] = name in save_interface Tests: 332 passing (110 new/updated), ruff clean
This commit is contained in:
+89
-1
@@ -291,4 +291,92 @@ The `zones` object maps zone names (keys) to zone configurations. Each zone corr
|
||||
|
||||
### Applying Firewall Configuration
|
||||
|
||||
The `config_pending()` function compares the declarative config in `config/firewall/config.json` against the live firewalld state returned by the daemon (via `daemon.handlers.firewall.get_state()`). It returns a diff indicating which zones have pending changes for interfaces, services, target, masquerade, forward ports, and rich rules. Zones that exist live but not in config are reported as `unmanaged_zones`.
|
||||
The `config_pending()` function compares the declarative config in `config/firewall/config.json` against the live firewalld state returned by the daemon (via `daemon.handlers.firewall.get_state()`). It returns a diff indicating which zones have pending changes for interfaces, services, target, masquerade, forward ports, and rich rules. Zones that exist live but not in config are reported as `unmanaged_zones`.
|
||||
|
||||
## Networkd (IP Configuration)
|
||||
|
||||
**File**: `config/network/config.json`
|
||||
|
||||
This file defines static IP configuration for network interfaces managed by systemd-networkd. The application renders each interface entry into a `50-<name>.network` INI file in `data/networkd/`, which the handler copies to `/etc/systemd/network/`.
|
||||
|
||||
```json
|
||||
{
|
||||
"interfaces": {
|
||||
"eth0": {
|
||||
"addresses": ["192.168.1.1/24"],
|
||||
"gateway": "192.168.1.254",
|
||||
"dns": ["8.8.8.8", "1.1.1.1"],
|
||||
"dhcp": "no"
|
||||
},
|
||||
"eth1": {
|
||||
"dhcp": "ipv4",
|
||||
"dns_default_route": true,
|
||||
"dhcp_client": {
|
||||
"hostname": "router",
|
||||
"use_dns": true
|
||||
}
|
||||
},
|
||||
"wg0": {
|
||||
"addresses": [{"address": "10.137.0.1/24"}],
|
||||
"routes": [
|
||||
{
|
||||
"destination": "10.0.0.0/8",
|
||||
"gateway": "10.137.0.2"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Interface Entry Fields
|
||||
|
||||
Each key in the `interfaces` object is an interface name (e.g., `eth0`, `eth1`, `wg0`). The value is a dict with the following keys:
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `addresses` | `array` | IPv4 addresses. Each item is either a bare CIDR string (`"192.168.1.1/24"`) or a dict with `address`, `label`, `scope`, `route_metric`, `duplicate_address_detection`, `manage_temporary_address`, `add_prefix_route`. Renders to `[Address]` sections. |
|
||||
| `ipv6_addresses` | `array` | Same as `addresses`, but for IPv6. |
|
||||
| `gateway` | `string` | Default IPv4 gateway (`[Network] Gateway=`). |
|
||||
| `ipv6_gateway` | `string` | Default IPv6 gateway (`[Network] IPv6Gateway=`). |
|
||||
| `dns` | `array` | IPv4 DNS servers (`[Network] DNS=`, one per line). |
|
||||
| `ipv6_dns` | `array` | IPv6 DNS servers (`[Network] IPv6DNS=`). |
|
||||
| `domains` | `array` | Search domains (`[Network] Domains=`). |
|
||||
| `ipv6_domains` | `array` | IPv6 search domains (`[Network] IPv6Domains=`). |
|
||||
| `dns_default_route` | `boolean` | Whether DNS is the default route for resolution (`[Network] DNSDefaultRoute=`). |
|
||||
| `dhcp` | `string` | DHCP mode: `"yes"`, `"ipv4"`, `"ipv6"`, `"no"`. Controls `[Network] DHCP=` and whether `[DHCPv4]`/`[DHCPv6]` sections are rendered. |
|
||||
| `routes` | `array` | Static routes. Each dict has `destination`, `gateway`, `metric`, `table`, `type`, `scope`, `gateway_on_link`, `ipv6_preference`, `initial_congestion_window`, `initial_advertised_receive_window`, `quick_ack`, `fast_open_no_cookie`, `mtu_bytes`, `protocol`, `next_hop`, `multi_path_route`. Renders to `[Route#N]` sections. |
|
||||
| `link` | `object` | Link settings: `mtu_bytes`, `mac_address`, `arp`, `multicast`, `all_multicast`, `promiscuous`, `unmanaged`, `activation_policy`, `required_for_online`. Renders to `[Link]` section. |
|
||||
| `dhcp_client` | `object` | DHCP client settings. Shared keys for both `[DHCPv4]` and `[DHCPv6]`: `hostname`, `duid_type`, `duid_raw_data`, `iaid`, `client_identifier`, `rapid_commit`, `anonymize`, `use_dns`, `use_ntp`, `use_sip`, `use_captive_portal`, `use_mtu`, `use_hostname`, `use_domains`, `use_routes`, `route_metric`, `send_decline`, `net_label`, `nft_set`, `ip_service_type`, `socket_priority`, `bootp`, `label`, `max_attempts`, `listen_port`, `server_port`, `mud_url`, `boot_filename`, `send_option`, `send_vendor_option`, `user_class`, `vendor_class_identifier`, `request_options`. |
|
||||
| `bind_carrier` | `array` | Carrier interfaces to bind to. |
|
||||
| `ignore_carrier_loss` | `boolean` | Ignore carrier loss events. |
|
||||
| `keep_configuration` | `boolean` | Keep configuration on stop. |
|
||||
| `configure_without_carrier` | `boolean` | Configure even without carrier. |
|
||||
| `link_local_addressing` | `string` | Link-local addressing mode. |
|
||||
| `ipv6_link_local_address_generation_mode` | `string` | IPv6 link-local address generation mode. |
|
||||
| `ipv6_stable_secret_address` | `string` | Stable secret for IPv6 address generation. |
|
||||
| `ipv4_ll_start_address` | `string` | Link-local IPv4 start address. |
|
||||
| `ipv4_ll_route` | `boolean` | Add route to link-local IPv4 address. |
|
||||
| `default_route_on_device` | `boolean` | Always add default route via this device. |
|
||||
| `ipv6_hop_limit` | `int` | IPv6 hop limit. |
|
||||
| `ipv6_retransmission_time_sec` | `string` | IPv6 retransmission timeout. |
|
||||
| `ipv4_duplicate_address_detection_timeout_sec` | `string` | IPv4 DAD timeout. |
|
||||
| `ipv4_reverse_path_filter` | `string` | IPv4 reverse path filtering mode. |
|
||||
| `ipv4_accept_local` | `boolean` | Accept packets to local addresses as non-local. |
|
||||
| `ipv4_route_localnet` | `boolean` | Route local network traffic. |
|
||||
| `ipv4_proxy_arp` | `boolean` | Enable proxy ARP. |
|
||||
| `ipv4_proxy_arp_private_vlan` | `boolean` | Private VLAN proxy ARP. |
|
||||
| `ipv6_proxy_ndp` | `boolean` | Enable IPv6 proxy NDP. |
|
||||
| `ipv6_proxy_ndp_address` | `string` | IPv6 proxy NDP address. |
|
||||
| `ipv6_send_ra` | `boolean` | Send IPv6 Router Advertisements. |
|
||||
| `m_pls_routing` | `boolean` | Enable MPLS routing. |
|
||||
| `keep_master` | `boolean` | Keep master on stop. |
|
||||
| `ip_family` | `string` | IP family to use. |
|
||||
|
||||
### DNS Upstream Sync
|
||||
|
||||
When `POST /api/network/apply` is called, the handler automatically collects public DNS servers from all networkd interface configs (via `collect_upstream_dns()`), filters out local/private-range addresses, and syncs the deduplicated list to dnsmasq's upstream DNS configuration. This keeps dnsmasq's upstream resolvers in sync with whatever DNS the WAN interface receives (whether statically configured or via DHCP).
|
||||
|
||||
### Generated Files
|
||||
|
||||
Each interface config entry produces a `50-<name>.network` file in `data/networkd/`. During apply, these are copied to `/etc/systemd/network/` and stale files (not matching any config entry) are removed. File generation uses the `systemd.syntax(7)` naming convention: first section is bare (`[Address]`, `[Route]`), subsequent sections use `#` suffix (`[Address#1]`, `[Route#2]`).
|
||||
Reference in New Issue
Block a user