Files
vacuum-wall/docs/config.md
T
mteehan ca110c321d style: format docs, fix user_permissions variable scoping in auth middleware
Apply ruff line-wrapping formatting to docs and test files.
Clarify auth middleware: extract user_permissions once before
subsystem check, removing conditional variable scoping.
2026-07-27 18:37:11 +00:00

34 KiB

Configuration Reference

This document describes the JSON configuration files used by Vacuum Wall to manage each subsystem. All persistent configuration is stored in the config/ directory as declarative JSON. Runtime artifacts and generated files live in data/. The application renders these declarations into the format expected by each underlying service.

DHCP/DNS Configuration

File: config/dnsmasq/config.json

This file defines all DHCP server settings and DNS resolution behavior for the dnsmasq service. The application renders it into /etc/dnsmasq.d/vacuum-wall.conf.

{
  "dhcp": {
    "ranges": [
      {
        "interface": "eth1",
        "start": "192.168.2.100",
        "end": "192.168.2.200",
        "lease_time": "12h",
        "gateway": "192.168.2.1",
        "dns": "192.168.2.1"
      }
    ],
    "static_leases": [
      {
        "mac": "aa:bb:cc:dd:ee:ff",
        "ip": "192.168.2.50",
        "hostname": "printer"
      }
    ]
  },
  "dns": {
    "upstreams": ["8.8.8.8", "1.1.1.1"],
    "domain": "lan",
    "custom_records": [
      {
        "name": "nas.lan",
        "address": "192.168.2.10",
        "hostname": "nas"
      }
    ]
  }
}

DHCP Fields

Field Type Required Description
ranges array No One or more DHCP address pools. Each range defines a subnet from which addresses are leased. Default: [].
ranges[].interface string Yes Network interface on which to serve this DHCP range (e.g., eth1).
ranges[].start string Yes First IP address in the pool.
ranges[].end string Yes Last IP address in the pool.
ranges[].lease_time string No DHCP lease duration. Accepts values like 12h, 1d, 30m. Default: 12h.
ranges[].gateway string No Default gateway advertised to DHCP clients. Typically the router's LAN IP.
ranges[].dns string No DNS server address advertised to DHCP clients. Typically the Vacuum Wall host's LAN IP.
static_leases array No Fixed IP assignments tied to MAC addresses. Clients with matching MACs always receive the specified IP. Default: [].
static_leases[].mac string Yes MAC address of the client (colon-separated lowercase hex).
static_leases[].ip string Yes The IP address to assign to this MAC. Must be outside the dynamic pool ranges.
static_leases[].hostname string No Hostname to associate with the lease. Used for reverse DNS and mDNS.

DNS Fields

Field Type Required Description
upstreams array Yes Upstream DNS servers to forward unresolved queries to. Supports IPv4 and IPv6 addresses. Default: ["8.8.8.8", "1.1.1.1"].
domain string No Local domain suffix. Hostnames without a FQDN are resolved within this domain (e.g., printer becomes printer.lan). Default: null.
custom_records array No Static DNS A records for internal services and devices. Default: [].
custom_records[].name string Yes Fully qualified domain name (e.g., nas.lan).
custom_records[].address string Yes The IP address to resolve the name to.
custom_records[].hostname string No Short hostname without the domain suffix. Adds a reverse DNS entry as well.

Additional dnsmasq directives can be appended verbatim by placing plain-text files in data/dnsmasq/fragments/. Each file's contents are concatenated into the generated config. This is useful for advanced options not covered by the JSON schema (e.g., bogus-priv, cache-size, log-queries).

Nginx Configuration

File: config/nginx/config.json

This file defines reverse proxy domains with path-based routing, and global SSL settings. The application renders it into per-domain server block files in data/nginx/sites-enabled/ and into the shared SSL snippet at /etc/nginx/snippets/vacuum-wall-ssl.conf.

{
  "domains": {
    "app.example.com": {
      "force_ssl": true,
      "cert": "acme",
      "auth": {
        "user": "admin",
        "htpasswd": "/home/wall/vacuum-wall/data/nginx/.htpasswd"
      },
      "paths": {
        "/": {
          "backend": {
            "host": "192.168.2.50",
            "port": 8080,
            "proto": "http"
          },
          "headers": {
            "X-Forwarded-Proto": "https"
          }
        },
        "/api": {
          "backend": {
            "host": "192.168.2.51",
            "port": 3000,
            "proto": "http"
          },
          "auth": null
        }
      }
    },
    "mgmt.example.com": {
      "force_ssl": true,
      "cert": "acme",
      "paths": {
        "/": {
          "backend": {
            "host": "127.0.0.1",
            "port": 9090,
            "proto": "http"
          },
          "is_management": true,
          "auth": {
            "user": "admin",
            "htpasswd": "/home/wall/vacuum-wall/data/nginx/.htpasswd"
          }
        },
        "/ws": {
          "backend": {
            "host": "127.0.0.1",
            "port": 9091,
            "proto": "http"
          },
          "is_websocket": true
        }
      }
    }
  },
  "ssl": {
    "protocols": "TLSv1.2 TLSv1.3",
    "ciphers": "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305",
    "prefer_server_ciphers": false
  }
}

Domain Entries

The domains object maps domain names (keys) to proxy configurations. Each entry produces a separate nginx server block. All routing is path-based — a domain can proxy multiple paths to different backends.

Field Type Required Description
paths object Yes Path-to-config map. Each key is a URL path (e.g., "/", "/api"). No catch-all unless "/" is explicitly defined.
force_ssl boolean No Enable HTTPS redirect. HTTP requests to this domain receive a 301 redirect to HTTPS. Default: true.
cert string No Certificate provisioning method. One of: "acme", "file", or "selfsigned". Omit for domains that don't need a dedicated cert.
auth object No Domain-level HTTP basic auth configuration ({ user, htpasswd }). Applies to all paths unless overridden at the path level.

Path Entries

Each entry under paths defines a location block and its proxy backend.

Field Type Required Description
backend object Yes The upstream service for this path.
backend.host string Yes IP address or hostname of the backend service.
backend.port integer Yes Port the backend service is listening on.
backend.proto string No Protocol: http or https. Default: http.
headers object No Custom proxy headers (key-value pairs). Supports nginx variable interpolation.
auth object | null No Path-level auth override. { user, htpasswd } replaces domain-level auth. null disables auth for this path.
is_management boolean No Marks this path as the Vacuum Wall WebUI backend. Suppresses security headers (X-Frame-Options, etc.) so the SPA works correctly.
is_websocket boolean No Marks this path as a WebSocket pass-through. Disables auth, sets Upgrade/Connection headers, uses extended timeouts.

Auth Inheritance Rules

  • Domain-level auth applies to all paths unless overridden.
  • Path-level auth: null means "no auth" for that path.
  • Path-level auth: { ... } overrides domain-level for that path.
  • No other domain-level settings inherit — headers is path-only.

Path ordering

Nginx evaluates location blocks by specificity: more specific prefixes (e.g., /api) always match before / by nginx's own priority rules. The order of keys in the paths dict does not affect routing behavior.

Certificate Types

The cert field is a string that selects the provisioning method:

Value Description
acme Vacuum Wall uses acme.sh to request and renew an ACME certificate via the HTTP-01 challenge. The nginx configuration serves ACME challenge files at /.well-known/acme-challenge/.
file Use a pre-existing certificate and private key from the local file system. Vacuum Wall will not attempt to renew these certificates.
selfsigned Vacuum Wall generates a self-signed certificate and private key on first apply. Useful for internal domains or testing. The generated certificate is stored at data/certs/.

Management Domain

The Vacuum Wall admin interface is configured as a regular domain entry under domains, with is_management: true on the path pointing to the Flask app. A second path (/ws) with is_websocket: true provides WebSocket pass-through for real-time state updates. This replaces the legacy management top-level key.

The application can create the .htpasswd file programmatically via write_htpasswd() (using passlib's SHA-256 crypt). Manual creation is also possible:

htpasswd -bc data/nginx/.htpasswd admin yourpassword

Backward Compatibility

Config files using the legacy format are auto-migrated on first load:

  • Domain entries with a top-level backend key are wrapped into paths["/"].
  • A legacy management top-level key is migrated into domains[management.domain] with is_management on the root path and a /ws WebSocket path.

Global SSL Settings

The ssl block defines TLS parameters applied to all HTTPS server blocks via the shared snippet.

Field Type Required Description
protocols string No nginx ssl_protocols directive value. Default: TLSv1.2 TLSv1.3.
ciphers string No nginx ssl_ciphers directive value. Default is a curated AEAD-only cipher string.
prefer_server_ciphers boolean No Whether to prefer server cipher order. Default: false.

ACME (Certificate) Configuration

File: config/acme/config.json

This file stores the ACME account settings used by acme.sh for certificate provisioning. Account registration, modification, and deactivation are performed through the WebUI at the Certificates page — not by editing this file directly.

{
  "email": "admin@example.com",
  "ca": "letsencrypt"
}

ACME Fields

Field Type Required Description
email string No Contact email for the ACME account. Used for certificate expiry notifications and recovery. Populated automatically when an account is registered via the WebUI. Default: "".
ca string No ACME CA provider. One of: "letsencrypt" (Let's Encrypt), "zerossl" (ZeroSSL). Populated automatically when an account is registered. Default: "".

Account Registration

ACME account registration is handled entirely through the WebUI. When the user registers an account:

  1. The user navigates to the Certificates page and clicks "Register Account".
  2. Provides an email address and selects a CA provider (Let's Encrypt or ZeroSSL).
  3. The backend calls acme.sh --register-account with the provided parameters.
  4. On success, the email and ca fields in config/acme/config.json are populated, and acme.sh writes its .account.conf file under data/acme/.

Before any certificate can be issued, an ACME account must be registered. The certificate validation flow includes a blocking check (account_registered) that prevents issuance if no account exists.

Account Management

After registration, the account can be managed from the WebUI:

  • Update email: The Settings modal allows changing the contact email, which triggers an update via acme.sh --register-account -u.
  • Deactivate account: The Settings modal includes a button to deactivate the account via acme.sh --deactivate-account, which clears the email and ca fields and removes the ACME account.

ACME Home Directory

acme.sh stores its state under data/acme/ (the ACME home directory). Key files:

  • .account.conf — ACME account credentials and settings (contains ACME_LEEMAIL, ACME_MCA).
  • <domain>/ — Per-domain certificate and key files issued by acme.sh.

The application reads .account.conf to determine registration status. If the file is missing or lacks required keys, the account is considered unregistered.

Auth Configuration

File: config/auth/config.json

This file defines JWT settings and WebAuthn Relying Party configuration for the authentication system.

{
  "jwt": {
    "access_token_ttl": 900,
    "refresh_token_ttl": 604800,
    "algorithm": "HS256"
  },
  "webauthn": {
    "rp_name": "Vacuum Wall",
    "rp_id": "<management-domain>",
    "origin": "https://<management-domain>"
  }
}

JWT Fields

Field Type Required Description
access_token_ttl integer No Access token lifetime in seconds. Default: 900 (15 minutes).
refresh_token_ttl integer No Refresh token lifetime in seconds. Default: 604800 (7 days).
algorithm string No JWT signing algorithm. Default: "HS256".

Note: JWT signing secrets are per-user, not shared. Each user's secret is auto-generated as a 32-byte base64url token (secrets.token_urlsafe(32)) and stored in the users.jwt_secret database column. Secrets are rotated on password change to invalidate all prior sessions.

WebAuthn Fields

Field Type Required Description
rp_name string Yes Display name for the WebAuthn Relying Party. Shown during credential registration.
rp_id string Yes Domain for WebAuthn credential binding. Must match the management domain.
origin string Yes HTTPS URL for WebAuthn origin check. Must match https://<rp_id>.

Database Schema

The SQLite database at data/auth.db stores authentication data across four tables. Created automatically on first access via get_db().

users

Column Type Description
id INTEGER Auto-increment primary key
username TEXT Unique username
password_hash TEXT Argon2id password hash
jwt_secret TEXT Per-user JWT signing secret (32-byte base64url)
created_at INTEGER Unix timestamp (auto-set)

permissions

Column Type Description
id INTEGER Auto-increment primary key
username TEXT Foreign key to users.username (CASCADE on delete)
subsystem TEXT Subsystem name (e.g., "firewall", "dhcp", "auth")
level TEXT Permission level: "read" or "rw"

UNIQUE constraint on (username, subsystem).

token_blacklist

Column Type Description
jti TEXT Primary key — JWT unique identifier
token_type TEXT "access" or "refresh"
expires INTEGER Unix timestamp of token expiry

Used to invalidate tokens on logout and password change. Expired entries are cleaned on every refresh operation.

webauthn_creds

Column Type Description
id INTEGER Auto-increment primary key
username TEXT Foreign key to users.username (CASCADE on delete)
credential_id TEXT Base64url-encoded credential ID
public_key TEXT Base64url-encoded public key
sign_count INTEGER Signature counter (replay prevention)
name TEXT User-assigned display name
transports TEXT JSON array of transport types

UNIQUE constraint on (username, credential_id).

WireGuard Configuration

File: config/wireguard/config.json

This file defines the WireGuard server interface, access classes, and all connected peers. The application renders it into /etc/wireguard/wg0.conf and applies it with wg-quick. The file is created automatically when initialize() generates the server key pair and pre-seeds default access classes.

{
  "interface": {
    "name": "wg0",
    "listen_port": 51820,
    "private_key": "<generated>",
    "public_key": "<generated>",
    "addresses": ["10.137.0.1/24"],
    "server_endpoint": "vpn.example.com:51820",
    "description": "Main WireGuard server",
    "post_up": null,
    "post_down": null
  },
  "access_classes": {
    "full": {
      "name": "Full LAN Access",
      "description": "Peers get full access to internal networks",
      "subnet": "10.137.0.0/24",
      "listen_port": 51820,
      "lan_access": true,
      "private_key": "<generated>",
      "public_key": "<generated>"
    },
    "internet": {
      "name": "Internet Only",
      "description": "Peers can only reach the internet",
      "subnet": "10.137.1.0/24",
      "listen_port": 51821,
      "lan_access": false,
      "private_key": "<generated>",
      "public_key": "<generated>"
    }
  },
  "peers": {
    "alice": {
      "public_key": "<auto-generated>",
      "private_key": "<auto-generated>",
      "endpoint": "203.0.113.1:51820",
      "allowed_ips": ["0.0.0.0/0"],
      "persistent_keepalive": 25,
      "preshared_key": null,
      "description": "Alice's office laptop",
      "access_class": "full"
    }
  }
}

Interface Fields

Field Type Required Description
name string No WireGuard interface name. Default: wg0.
listen_port integer No Port the WireGuard interface listens on. Default: 51820. Must be opened in the firewall.
private_key string Yes (after init) Base64-encoded private key for the server interface. Generated automatically by initialize() via wg genkey.
public_key string Yes (after init) Corresponding public key. Generated automatically by initialize() via wg pubkey.
addresses array No IP address(es) assigned to the server interface in CIDR notation (e.g., 10.137.0.1/24). Default: ["10.137.0.1/24"].
server_endpoint string No External hostname:port for client connection. Used in generated client configs. Default: "".
description string No Free-text description of the WireGuard server. Default: "".
post_up string No Shell command to run after the interface is brought up. Common uses: adding NAT rules, enabling IP forwarding for the tunnel. Set to null to omit. Default: null.
post_down string No Shell command to run after the interface is brought down. Used to clean up rules added by post_up. Set to null to omit. Default: null.

Access Classes

Access classes define categories of VPN access. Each class gets its own WireGuard interface (wg-<key>), firewall zone (vpn-<key>), subnet, and listen port. Peers are assigned to a class and their config is rendered to that class's interface. Pre-seeded with full and internet defaults on first initialization. Manageable via GET/POST/PATCH/DELETE /api/wireguard/classes. Per-class tunnel lifecycle: POST /api/wireguard/classes/<key>/up, POST /api/wireguard/classes/<key>/down.

Field Type Required Description
name string Yes Human-readable display name for the class.
description string No Optional description of what access level this class provides. Default: "".
subnet string Yes CIDR subnet for the class's WireGuard interface (e.g., 10.137.0.0/24). Server address is derived as <base>.1/<prefix>.
listen_port integer Yes UDP port for the class's WireGuard interface. Must be unique per class.
lan_access boolean No When true, the sync subscriber adds inter-zone accept rules for internal subnets, allowing peers to reach the LAN. When false, peers can only reach the internet via masquerade. Default: false.
private_key string Yes (auto) Base64-encoded private key for the class's WireGuard interface. Auto-generated via POST /api/wireguard/classes/keys/<key>.
public_key string Yes (auto) Corresponding public key. Auto-generated with private_key.

Multi-Interface Behavior

When peers are assigned to an access class, the daemon:

  1. Renders a separate wg-<key>.conf for each class that has assigned peers.
  2. Each class interface gets its own private/public key pair.
  3. The sync subscriber creates a vpn-<key> firewall zone per class with masquerade enabled.
  4. Classes with lan_access=true get additional inter-zone rules for internal subnets.
  5. apply brings up all class interfaces independently. Per-class up/down endpoints control individual tunnels.

Legacy Single-Interface Mode

When no peers are assigned to any access class, the system falls back to the legacy single-interface mode where all peers share wg0.

Peer Fields

Peers are stored in an object keyed by a human-readable identifier (e.g., alice, office-laptop). Each peer entry defines a WireGuard peer configuration. When add_peer() is called, the peer's key pair is auto-generated. The private_key is stored for client configuration generation but stripped from all API responses.

Field Type Required Description
public_key string Yes The peer's public key. Auto-generated when the peer is added.
private_key string No The peer's private key, stored for generating downloadable client configuration files. Auto-generated when the peer is added. Stripped from all API responses — the WebUI never exposes peer private keys over the network.
endpoint string No The peer's public endpoint (IP:port). Required for server-initiated connections (e.g., the server reaching out to a peer behind a firewall). Leave empty or null for peer-initiated connections where the peer connects to the server. Default: null.
allowed_ips array No CIDR blocks that traffic from this peer is allowed to route. Default: [] (no routing restrictions from the server side). ["0.0.0.0/0"] allows all traffic. ["10.137.0.0/16"] restricts traffic to the VPN subnet.
persistent_keepalive integer No Keepalive interval in seconds. 25 is recommended for peers behind NAT. Set to 0 or null to disable. Default: null.
preshared_key string No Optional pre-shared key for post-quantum resistance. Use wg genpsk to generate. Default: null.
description string No Optional description for the peer. Default: "".
access_class string No Key of the access class this peer belongs to (e.g., "full", "internet"). null means unassigned. Default: null.

Client Configuration Generation

When a peer's private_key is set (which is the case when add_peer() auto-generates it), the WebUI can generate a complete WireGuard client configuration file that the user can download and import into their WireGuard client app. The generated config includes the peer's interface settings, the server as a [Peer] entry, and the appropriate Endpoint and AllowedIPs values. The private_key field is written into the client config file for download but is never returned by the API. generate_client_conf() computes the client IP address from the server's subnet and the peer's sorted index position.

Applying Configuration

When configuration is saved through the WebUI or API, the application:

  1. Renders the wg0.conf file from the JSON configuration.
  2. Writes the file to /etc/wireguard/wg0.conf with 600 permissions via sudo cp.
  3. Runs sudo wg-quick up <name> to apply the configuration.
  4. Returns success or error status to the caller.

If the interface is already up, wg-quick up will reconfigure it in place without dropping existing connections.

Firewall Configuration

File: config/firewall/config.json

This file defines the declarative firewalld zone configuration. The application compares it against the live firewalld state via _compute_pending_changes() and applies incremental changes. Runtime state backups are stored in data/firewall/rules.json.

{
  "zones": {
    "public": {
      "interfaces": ["eth0"],
      "services": ["dhcp", "dns", "https", "ssh"],
      "target": "DEFAULT",
      "masquerade": true,
      "forward_ports": [
        {
          "id": "abc123",
          "port": 443,
          "proto": "tcp",
          "toaddr": "192.168.2.50",
          "toport": 8080
        }
      ],
      "rich_rules": [
        {
          "rule": "rule family=\"ipv4\" source address=\"10.0.0.0/8\" reject"
        }
      ]
    }
  }
}

Zone Fields

The zones object maps zone names (keys) to zone configurations. Each zone corresponds to a firewalld zone applied via firewall-cmd.

Field Type Required Description
interfaces array No Network interfaces assigned to this zone. Computed against live state to detect pending changes. Default: [].
services array No Firewalld services to allow in this zone (e.g., ssh, https, dns, dhcp). Default: [].
target string No Zone target policy. One of: DEFAULT, ACCEPT, DROP, REJECT. The code maps these to firewalld's canonical target values (default, ACCEPT, DROP, REJECT). Default: DEFAULT.
masquerade boolean No Enable IP masquerading (NAT) for this zone. Default: false.
forward_ports array No Port forwarding rules. Each entry has an auto-generated id field and the standard firewalld forward-port fields. Default: [].
forward_ports[].id string No Auto-generated unique identifier for the port forwarding rule. Not user-settable.
forward_ports[].port integer Yes Destination port to forward.
forward_ports[].proto string Yes Protocol: tcp or udp.
forward_ports[].toaddr string No Internal IP address to forward to. Omit for broadcast forwarding.
forward_ports[].toport integer No Internal port to forward to. Omit to keep the same port.
rich_rules array No Rich rule entries for advanced firewall policies. Default: [].
rich_rules[].rule string Yes The full firewalld rich rule string, e.g., rule family="ipv4" source address="10.0.0.0/8" reject.

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.

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/.

{
  "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.

Keys not in the recognized set will be saved to config.json but won't be rendered to .network files. A warning is logged identifying any unrecognized keys.

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]).

Cross-Subsystem Dependencies

Some subsystems depend on each other. When you modify one, related subsystems are updated automatically through the event bus.

Trigger Subsystem Affected Subsystem What Happens
dnsmasq (DHCP range) firewall Zone gains dhcp/dns services. Removing the last range removes them. DHCP ranges also back-propagate gateway (interface IP) so clients receive their default route.
wireguard (peer add/remove) firewall Per-class vpn-<key> zones are created with wg-<key> interface, masquerade, UDP port rule, and inter-zone accept rules (only when lan_access=true). Falls back to single vpn zone in legacy mode. Cleanup removes stale rules when classes have no peers.
firewall (zone changes) dnsmasq Stale DHCP ranges (for interfaces no longer in any zone) are automatically removed. Masquerade-enabled zones ensure DHCP ranges carry the gateway. Zones with dhcp service but no range are logged as warnings.
network (interface config) firewall Zone interface assignments in firewall config are updated — new interfaces are flagged, stale ones removed.
network (interface config) dnsmasq Suggested DHCP ranges are logged when an interface has a static IP but no DHCP range.

Note: The firewall "Apply" button is still needed to push config changes to firewalld. Sync only updates the declarative JSON.

Additionally, on daemon startup, lib/system_import.py reconciles live system configs (dnsmasq, wireguard, networkd, nginx, firewall) with the declarative JSON. This prevents drift when configs were created by the install script or edited manually in system files. Reconciliation only writes when the existing JSON differs or is missing — no data is lost on re-run.