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>.
How a change is made safely
Section titled “How a change is made safely”- Read first.
GET /networkreturns aversion(a short hash of the stored network document). - Plan (dry run).
POST /network/planwith the changes and thatversionsays what would change, shows the netplan diff and lists anything that refuses the change. Nothing is touched. - Apply.
POST /network/applysaves the new document, saves a configuration version, backs up the netplan directory, writes the netplan file, checks it withnetplan generateand runsnetplan apply. It then arms a 120-second rollback timer. - Confirm or roll back. Unless
POST /network/confirmarrives within 120 s, the backup is put back and the stored document restored.POST /network/rollbackdoes 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),
planandapplyanswer409with"the network configuration changed since the page read it: read it again"and the currentversion.applyrequiresversion;planchecks it only when you send it. - One apply at a time. While an apply waits for its confirm, another
applyanswers409("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).
Reading
Section titled “Reading”GET /network
Section titled “GET /network”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.
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[].kindisport,bond,vlan,ifb,bmc(a server management USB link, not a router port) orother.ownersays which part of DTVSOL made it:network,vlans,services,shaper, or""for anything DTVSOL did not create.addresses[].sourceis where the address is configured;kernelmeans it is live but not configured anywhere.pendingisnull, or{"deadline": <unix time>, "left_s": <seconds>}while an apply waits for its confirm.managedisfalsewhen no network configuration is stored yet (dtvsol netcfg import --savecreates it).- Status
500with{"ok": false, "error": …}when the kernel state cannot be read.
Ports and bonds
Section titled “Ports and bonds”POST /network/plan
Section titled “POST /network/plan”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, ornullfor the default.remove: truestops 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: onlylacp-rate(slow|fast, mode802.3adonly),transmit-hash-policy(layer2,layer2+3,layer3+4,encap2+3,encap3+4) andmii-monitor-interval(0–10000);nullresets a parameter to its default.
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.
POST /network/apply
Section titled “POST /network/apply”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. |
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").
POST /network/confirm
Section titled “POST /network/confirm”No body fields are needed (by is optional).
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").
POST /network/rollback
Section titled “POST /network/rollback”Undoes the waiting apply immediately instead of waiting for the timer.
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.
POST /network/vlan-check
Section titled “POST /network/vlan-check”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. |
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" ]}POST /network/vlan-add
Section titled “POST /network/vlan-add”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. |
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).
POST /network/vlan-disable
Section titled “POST /network/vlan-disable”| 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. |
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).
POST /network/vlan-enable
Section titled “POST /network/vlan-enable”| Name | In | Type | Notes |
|---|---|---|---|
name |
body | string | VLAN interface name. |
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"}POST /network/vlan-delete
Section titled “POST /network/vlan-delete”| Name | In | Type | Notes |
|---|---|---|---|
name |
body | string | VLAN interface name. |
local |
body | string | Optional: the caller’s address to protect. |
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.
Addresses
Section titled “Addresses”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.
POST /network/ip-check
Section titled “POST /network/ip-check”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. |
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" ]}POST /network/ip-add
Section titled “POST /network/ip-add”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. |
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.
POST /network/ip-delete
Section titled “POST /network/ip-delete”| 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. |
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).
Protection
Section titled “Protection”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).
POST /network/protect-add
Section titled “POST /network/protect-add”| Name | In | Type | Notes |
|---|---|---|---|
network |
body | string | Address or CIDR, e.g. XXX.XXX.XXX.0/24. |
comment |
body | string | Optional. |
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.
POST /network/protect-delete
Section titled “POST /network/protect-delete”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. |
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.
POST /network/pf-add
Section titled “POST /network/pf-add”| 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. |
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.
POST /network/pf-delete
Section titled “POST /network/pf-delete”| 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. |
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.
POST /network/f2b-unban
Section titled “POST /network/f2b-unban”| Name | In | Type | Notes |
|---|---|---|---|
ip |
body | string | The banned address. |
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.
POST /network/antispoof-set
Section titled “POST /network/antispoof-set”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. |
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.
POST /network/antispoof-iface
Section titled “POST /network/antispoof-iface”| Name | In | Type | Notes |
|---|---|---|---|
iface |
body | string | VLAN interface, e.g. vlan100. |
mode |
body | string | strict, dynamic, off, or default (follow the global mode). |
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.
POST /network/cgnat-set
Section titled “POST /network/cgnat-set”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. |
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.
Action API equivalents
Section titled “Action API equivalents”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.