Skip to content

Network Configuration API

The /network endpoints manage the DTVSOL Super Router’s own network: physical ports, bonds, addresses, VLANs, default and static routes. The stored configuration lives in the router’s database (the network document); dtvsold renders one netplan file from it (/etc/netplan/90-dtvsol.yaml). These are the calls the monitor’s Settings tab uses.

Authentication, error format and status codes are described in the API overview.

Every request is logged with who made it. POST bodies may carry an optional by field: the name of the monitor user (letters, digits and _ . @ -, at most 64 characters). The log then reads monitor <by> (api <caller-ip>); without it, api <caller-ip>.

  • Read first. GET /network returns a version (a short hash of the stored network document).
  • Plan (dry run). POST /network/plan with the changes and that version says what would change, shows the netplan diff and lists anything that refuses the change. Nothing is touched.
  • Apply. POST /network/apply saves the new document, saves a configuration version, backs up the netplan directory, writes the netplan file, checks it with netplan generate and runs netplan apply. It then arms a 120-second rollback timer.
  • Confirm or roll back. Unless POST /network/confirm arrives within 120 s, the backup is put back and the stored document restored. POST /network/rollback does that immediately. The pending state survives a reboot: a router power-cycled while an apply is waiting rolls back when the daemon starts.
  • Stale version → 409. If the stored document changed since you read it (another admin, the CLI), plan and apply answer 409 with "the network configuration changed since the page read it: read it again" and the current version. apply requires version; plan checks it only when you send it.
  • One apply at a time. While an apply waits for its confirm, another apply answers 409 ("a change is waiting for its confirm (N s left): confirm or roll it back first").
  • Cut-off protection. Changes that would take away the address of an SSH session, the API’s listen address, or (when you send it as local) the address the caller came to are refused.

VLAN, address and protection operations (vlan-*, ip-*, protect-*, pf-*, f2b-unban, antispoof-*, cgnat-set) take effect at once. They are not covered by the rollback timer and do not check version; instead, whatever could cut the router off is refused before anything runs (409 with a refused list).

The stored network configuration and the kernel side by side: every interface as the kernel has it, every address and where it comes from, the service VLANs, default and static routes, the differences between configuration and kernel (drift) and any apply waiting for its confirm. Read-only.

Terminal window
curl -s http://ROUTER-IP:8880/network -H "X-API-Key: YOUR_API_KEY"
{
"ok": true,
"generated": "2026-09-28 10:15:02",
"managed": true,
"version": "3f9a1c0d2b7e4a55",
"config": {
"ports": [{"name": "eno1", "mac": "aa:bb:cc:00:00:01", "mtu": 1500, "up": true}],
"bonds": [{"name": "bond0", "members": ["eno2", "eno3"], "mode": "802.3ad",
"params": {"lacp-rate": "fast", "mii-monitor-interval": 100}}]
},
"uplinks": ["eno1"],
"interfaces": [
{"name": "eno1", "kind": "port", "mac": "aa:bb:cc:00:00:01", "mtu": 1500, "up": true,
"carrier": true, "master": null, "parent": null, "vid": null, "protocol": null,
"bond_mode": null, "speed_mbps": 10000, "driver": "ixgbe",
"addresses": ["XXX.XXX.XXX.2/24"], "configured": true, "owner": "network",
"shapes": null, "uplink": true},
{"name": "vlan100", "kind": "vlan", "parent": "bond0", "vid": 100, "protocol": "802.1Q",
"addresses": ["100.64.0.1/22"], "configured": false, "owner": "vlans", "uplink": false}
],
"addresses": [
{"iface": "eno1", "cidr": "XXX.XXX.XXX.2/24", "family": 4, "source": "network",
"configured": true, "live": true, "dynamic": false}
],
"service_vlans": [
{"iface": "v400.101", "olt": "olt-1", "pon": "0/1/0", "svlan": 400, "cvlan": 101,
"ipv4": "100.64.8.1/24", "ipv6": null}
],
"routes": {
"default_kernel": [{"to": "default", "via": "XXX.XXX.XXX.1", "dev": "eno1", "protocol": "static", "metric": null}],
"static": []
},
"drift": ["vlan vlan100: 10.2.0.1/24 configured, not live"],
"pending": null
}

Notes:

  • interfaces[].kind is port, bond, vlan, ifb, bmc (a server management USB link, not a router port) or other. owner says which part of DTVSOL made it: network, vlans, services, shaper, or "" for anything DTVSOL did not create.
  • addresses[].source is where the address is configured; kernel means it is live but not configured anywhere.
  • pending is null, or {"deadline": <unix time>, "left_s": <seconds>} while an apply waits for its confirm.
  • managed is false when no network configuration is stored yet (dtvsol netcfg import --save creates it).
  • Status 500 with {"ok": false, "error": …} when the kernel state cannot be read.

Dry run of a port/bond change: the changes in words, the netplan report and diff, what refuses it, and which addresses would go away. Nothing is changed.

Name In Type Notes
version body string Optional here; if sent and stale → 409.
ports body object name → {up, mtu, remove}. Only the fields that change.
bonds body object name → {members, mode, params, mtu}. Only the fields that change.
local body string Optional: the IP address the caller reached the router on; changes that remove it are refused.
by body string Optional: monitor user name for the log.

Field rules:

  • up: true/false. mtu: 576–9216, or null for the default.
  • remove: true stops managing a port (it leaves the configuration). Refused while the port is a bond member, carries addresses, or has VLANs on it.
  • members: list of configured ports, at least one; a member may not be in another bond, carry addresses or carry VLANs.
  • mode: 802.3ad, active-backup, balance-rr, balance-xor, balance-tlb, balance-alb, broadcast.
  • params: only lacp-rate (slow|fast, mode 802.3ad only), transmit-hash-policy (layer2, layer2+3, layer3+4, encap2+3, encap3+4) and mii-monitor-interval (0–10000); null resets a parameter to its default.
Terminal window
curl -s -X POST http://ROUTER-IP:8880/network/plan \
-H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"version":"3f9a1c0d2b7e4a55","ports":{"eno2":{"mtu":9000}},"bonds":{"bond0":{"params":{"lacp-rate":"slow"}}}}'
{
"ok": true,
"version": "3f9a1c0d2b7e4a55",
"changes": ["eno2: MTU default → 9000", "bond0: lacp-rate fast → slow"],
"report": "…the netplan file's diff and checks…",
"refused": [],
"addresses_gone": [],
"pending": false
}

Errors: 400 with {"ok": false, "error": "…", "problems": [...]} for invalid fields, for "nothing changes", or when no network configuration is stored; 409 for a stale version.

Applies a port/bond change. Same body as plan, but version is required. The change is validated again; if anything refuses it, nothing is applied (400, each reason prefixed refused:). On success the new document is saved, a configuration version is saved, the netplan directory is backed up, and the rollback timer is armed.

Name In Type Notes
version body string Required; must match the current version or 409.
ports body object As for plan.
bonds body object As for plan.
local body string Optional: the caller’s address to protect.
by body string Optional: monitor user name for the log.
Terminal window
curl -s -X POST http://ROUTER-IP:8880/network/apply \
-H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"version":"3f9a1c0d2b7e4a55","ports":{"eno2":{"mtu":9000}},"local":"XXX.XXX.XXX.2","by":"admin"}'
{
"ok": true,
"rc": 0,
"pending": 1790430302123,
"deadline": 1790430422,
"text": "…configuration version 42…\napplied. Confirm within 120 s: dtvsol netcfg confirm — else it is put back (…)\n",
"changes": ["eno2: MTU default → 9000"]
}

deadline is a Unix time. Errors: 400 (invalid, nothing changes, refused), 409 (stale version, or another apply waiting for its confirm), 500 if the apply itself failed — the netplan files are put back and the stored document is restored ("the apply failed; the configuration is as it was").

No body fields are needed (by is optional).

Terminal window
curl -s -X POST http://ROUTER-IP:8880/network/confirm -H "X-API-Key: YOUR_API_KEY"
{"ok": true, "text": "confirmed: the network stays as applied (the old files: …)\n", "rc": 0}

400 with ok: false when no apply is waiting ("no apply is waiting for a confirm").

Undoes the waiting apply immediately instead of waiting for the timer.

Terminal window
curl -s -X POST http://ROUTER-IP:8880/network/rollback -H "X-API-Key: YOUR_API_KEY"
{"ok": true, "text": "rolled back (by hand): netplan as it was (…)\nthe network configuration is as it was before the change\n", "rc": 0}

400 with ok: false when nothing is waiting ("no apply is waiting: nothing to roll back") or the rollback failed.

For all VLAN operations except vlan-add, the VLAN must exist in the configuration, otherwise 404 (no VLAN "…" in the configuration).

A disable or delete is refused (409) when: a default route (live or configured) goes out of the VLAN; the VLAN holds the address of an SSH session, the API’s listen address or the caller’s local address; or other interfaces run on top of it.

What disabling or deleting a VLAN would refuse and what it would take down. Changes nothing.

Name In Type Notes
name body string VLAN interface name, e.g. vlan100.
action body string delete, or anything else for a disable check.
local body string Optional: the caller’s address to protect.
Terminal window
curl -s -X POST http://ROUTER-IP:8880/network/vlan-check \
-H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"name":"vlan100","action":"delete"}'
{
"ok": true,
"name": "vlan100",
"refused": [],
"impact": [
"its address 100.64.0.1/22 stops",
"DHCP stops serving vlan100",
"its DHCP networks, NAT pool and port forwards are deleted with it"
]
}

Same as POST /vlans (see VLANs, IP and routes).

Name In Type Notes
parent body string Parent interface, e.g. bond0.
vlan_id body integer 1–4094.
protocol body string 802.1ad for QinQ; anything else is 802.1Q.
ip body string Optional IPv4 address/prefix.
ipv6 body string Optional IPv6 address/prefix.
label body string Optional label.
serve body boolean false for a bare link (no DHCP/net6/PD). Default true.
Terminal window
curl -s -X POST http://ROUTER-IP:8880/network/vlan-add \
-H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"parent":"bond0","vlan_id":100,"ip":"100.64.0.1/22","label":"OLT 1"}'
{
"ok": true, "code": 201,
"message": "VLAN vlan100 created on bond0 (100.64.0.1/22)",
"name": "vlan100", "parent": "bond0", "vlan_id": 100, "protocol": "802.1Q",
"ip": "100.64.0.1/22", "ipv6": ""
}

The HTTP status is 200 on success; on failure it is the underlying error’s code (400–599).

Name In Type Notes
name body string VLAN interface name.
reason body string Optional reason, kept with the VLAN.
local body string Optional: the caller’s address to protect.
Terminal window
curl -s -X POST http://ROUTER-IP:8880/network/vlan-disable \
-H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"name":"vlan100","reason":"OLT maintenance"}'
{
"ok": true, "code": 200, "message": "VLAN vlan100 disabled", "name": "vlan100",
"enabled": false, "disabled_at": "2026-09-28 10:20:00", "reason": "OLT maintenance",
"took_offline": ["…"],
"kept": "delegation range, subnet blocks, NAT pool, port forwards, routes, client reservations and addresses — all restored by: dtvsol vlan enable vlan100"
}

409 when refused (see above).

Name In Type Notes
name body string VLAN interface name.
Terminal window
curl -s -X POST http://ROUTER-IP:8880/network/vlan-enable \
-H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"name":"vlan100"}'
{"ok": true, "code": 200, "message": "VLAN vlan100 enabled", "name": "vlan100"}
Name In Type Notes
name body string VLAN interface name.
local body string Optional: the caller’s address to protect.
Terminal window
curl -s -X POST http://ROUTER-IP:8880/network/vlan-delete \
-H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"name":"vlan100"}'
{"ok": true, "code": 200, "message": "VLAN vlan100 deleted", "name": "vlan100"}

409 when refused (see above). Run vlan-check with "action":"delete" first to see the impact.

An address change is refused (409) when the interface is a service VLAN (its addresses belong to the services), when the address was leased by DHCP rather than configured, when it is the address of an SSH session, the API or the caller’s local address, or when the default route’s gateway is reached through it. An add is refused only for a service VLAN; a delete for any of these.

What deleting an address would refuse and what it would affect. Changes nothing.

Name In Type Notes
iface body string Interface name, e.g. vlan100.
ip body string Address with prefix, e.g. 100.64.0.1/22.
local body string Optional: the caller’s address to protect.
Terminal window
curl -s -X POST http://ROUTER-IP:8880/network/ip-check \
-H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"iface":"vlan100","ip":"100.64.0.1/22"}'
{
"ok": true,
"refused": [],
"impact": [
"the clients of vlan100 whose gateway is 100.64.0.1 lose it",
"100.64.0.1/22 is taken off vlan100 now and from its configuration"
]
}

Same as POST /ip (see VLANs, IP and routes).

Name In Type Notes
iface body string Port, bond or VLAN.
ip body string Address with prefix.
Terminal window
curl -s -X POST http://ROUTER-IP:8880/network/ip-add \
-H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"iface":"vlan100","ip":"100.64.4.1/24"}'
{"ok": true, "code": 201, "message": "IP 100.64.4.1/24 added to vlan100", "persisted": true}

HTTP status 200 on success; 409 for a service VLAN.

Name In Type Notes
iface body string Interface name.
ip body string Address with prefix.
local body string Optional: the caller’s address to protect.
Terminal window
curl -s -X POST http://ROUTER-IP:8880/network/ip-delete \
-H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"iface":"vlan100","ip":"100.64.4.1/24"}'
{"ok": true, "code": 200, "message": "IP 100.64.4.1/24 removed from vlan100"}

409 when refused (see above).

These call the same functions as /protect, /pf, /fail2ban, /antispoof and /cgnat (see Protection and CGNAT and anti-spoofing); only the fields listed are accepted. The HTTP status is the underlying function’s (for example 201 for an add).

Name In Type Notes
network body string Address or CIDR, e.g. XXX.XXX.XXX.0/24.
comment body string Optional.
Terminal window
curl -s -X POST http://ROUTER-IP:8880/network/protect-add \
-H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"network":"XXX.XXX.XXX.0/24","comment":"NOC"}'
{"ok": true, "code": 201, "message": "Network XXX.XXX.XXX.0/24 allowed",
"networks": [{"network": "XXX.XXX.XXX.0/24", "comment": "NOC", "added": "2026-09-28 10:30:00"}]}

400 invalid network, 409 already allowed.

Refused (409) for 127.0.0.1 (always allowed) and when the caller’s own address (client) is allowed only through this entry — removing it would lock the caller out.

Name In Type Notes
network body string The entry to remove.
client body string Optional: the caller’s IP address, for the lock-out check.
Terminal window
curl -s -X POST http://ROUTER-IP:8880/network/protect-delete \
-H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"network":"XXX.XXX.XXX.0/24","client":"XXX.XXX.XXX.10"}'
{"ok": true, "code": 200, "message": "Network XXX.XXX.XXX.0/24 removed", "networks": []}

404 when the entry does not exist.

Name In Type Notes
proto body string tcp (default) or udp.
public_ip body string Optional public IPv4; empty means any.
public_port body integer 1–65535.
client_ip body string Inside IPv4 address.
client_port body integer 1–65535.
comment body string Optional.
Terminal window
curl -s -X POST http://ROUTER-IP:8880/network/pf-add \
-H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"proto":"tcp","public_ip":"XXX.XXX.XXX.5","public_port":8443,"client_ip":"100.64.1.20","client_port":443}'
{"ok": true, "code": 201, "message": "Port forward added",
"forward": {"proto": "tcp", "public_ip": "XXX.XXX.XXX.5", "public_port": 8443,
"client_ip": "100.64.1.20", "client_port": 443, "comment": "", "created": "2026-09-28 10:31:00"}}

400 invalid field, 409 a forward for that proto/public_ip:port already exists.

Name In Type Notes
proto body string tcp (default) or udp.
public_ip body string As added (empty for any).
public_port body integer As added.
Terminal window
curl -s -X POST http://ROUTER-IP:8880/network/pf-delete \
-H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"proto":"tcp","public_ip":"XXX.XXX.XXX.5","public_port":8443}'
{"ok": true, "code": 200, "message": "Port forward removed"}

404 when no forward matches.

Name In Type Notes
ip body string The banned address.
Terminal window
curl -s -X POST http://ROUTER-IP:8880/network/f2b-unban \
-H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"ip":"XXX.XXX.XXX.77"}'
{"ok": true, "code": 200, "message": "Unbanned XXX.XXX.XXX.77"}

400 invalid IP, 500 when the unban failed.

Only enabled, mode, log and exempt are passed on; at least one is required.

Name In Type Notes
enabled body boolean Turn anti-spoofing on or off.
mode body string strict or dynamic.
log body boolean Log dropped packets.
exempt body array Networks (CIDR) never checked.
Terminal window
curl -s -X POST http://ROUTER-IP:8880/network/antispoof-set \
-H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"enabled":true,"mode":"strict","log":true}'
{"ok": true, "code": 200, "message": "Anti-spoofing enabled", "apply": {"…": "…"}, "status": {"…": "…"}}

400 for nothing to set, an invalid boolean, mode or exempt network.

Name In Type Notes
iface body string VLAN interface, e.g. vlan100.
mode body string strict, dynamic, off, or default (follow the global mode).
Terminal window
curl -s -X POST http://ROUTER-IP:8880/network/antispoof-iface \
-H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"iface":"vlan100","mode":"dynamic"}'
{"ok": true, "code": 200, "message": "vlan100 set to dynamic", "iface": "vlan100",
"requested": "dynamic", "effective": "dynamic", "note": null, "apply": {"…": "…"}}

400 invalid interface name or mode.

Only the fields below are passed on.

Name In Type Notes
enabled body boolean Enabling requires a valid WAN iface and a non-empty pool.
iface body string WAN interface.
pool body array/string Public IPv4 pool.
port_min body integer Default 1024.
port_max body integer Default 65535.
block_size body integer Ports per subscriber; default 2048.
exempt body array Networks (CIDR) not translated.
Terminal window
curl -s -X POST http://ROUTER-IP:8880/network/cgnat-set \
-H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"enabled":true,"iface":"eno1","pool":["XXX.XXX.XXX.0/28"],"block_size":2048}'
{"ok": true, "code": 200, "message": "CGNAT updated", "status": {"…": "…"}}

400 when enabling without a valid WAN interface or pool, or with an invalid exempt network.

The /network endpoints have no /api?action= form. The underlying operations are also available through their own REST resources (/vlans, /ip, /protect, /pf, /fail2ban, /antispoof, /cgnat) and their action-API forms, which are documented on those pages. The CLI equivalent of apply/confirm/rollback is dtvsol netcfg apply | confirm | rollback.