VLANs, IPs and routes API
These endpoints manage the router’s VLAN interfaces, the addresses on its interfaces, the static routes it keeps across reboots and its dynamic (source) NAT pools. Base URL, authentication and the error format are described in the API overview.
Everything created here is persisted by the router and restored at boot. Changes are logged with the caller’s address. A 5xx answer can carry broken_data_files, naming router data files that do not parse.
A VLAN’s interface name follows from its parent and protocol: vlan100 for an 802.1Q VLAN on a port or bond, svlan500 for an 802.1ad S-VLAN, svlan500.20 for a C-VLAN inside an S-VLAN.
GET /vlans
Section titled “GET /vlans”Every VLAN the router manages, with its live state.
curl -H "X-API-Key: YOUR_API_KEY" http://ROUTER-IP:8880/vlans{ "count": 1, "vlans": [ { "name": "vlan110", "parent": "bond0", "vlan_id": 108, "protocol": "802.1Q", "ips": ["100.64.8.1/24"], "ips6": ["XXXX:XXXX:110::1/64"], "label": "OLT 1 PON 0/1/3", "added": "2026-09-01 10:00:00", "enabled": true, "live": true, "status": "UP", "live_ips": ["100.64.8.1/24"], "live_ips6": ["XXXX:XXXX:110::1/64"] } ]}status is the link state, DISABLED for a disabled VLAN, or NOT CREATED when the interface does not exist.
POST /vlans
Section titled “POST /vlans”Creates the VLAN, brings it up and adds its addresses. Unless serve is off, each address is then served: IPv4 with DHCP (POST /net), IPv6 with DHCPv6 and a pool (POST /net6, POST /net6/pool) and prefix delegation (POST /pd). Each serving step is reported on its own; a failed step does not undo the VLAN.
| Name | In | Type | Notes |
|---|---|---|---|
parent |
body | string | Existing parent interface: a port, bond or S-VLAN. |
vlan_id |
body | integer | 1–4094. |
protocol |
body | string | 802.1Q (default) or 802.1ad. |
ip |
body | CIDR | Optional IPv4 gateway address, e.g. 100.64.8.1/24. A network address is corrected to the first host (with a note). |
ipv6 |
body | CIDR | Optional IPv6 address, e.g. XXXX:XXXX:110::1/64. |
label |
body | string | Optional description. |
serve |
body | boolean | 0, false or no creates the link only. Default on. |
force |
body | boolean | Allow an address that overlaps one already on another interface. |
curl -X POST -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \ -d '{"parent":"bond0","vlan_id":108,"ip":"100.64.8.1/24","ipv6":"XXXX:XXXX:110::1/64","label":"OLT 1 PON 0/1/3"}' \ http://ROUTER-IP:8880/vlans{ "ok": true, "code": 201, "message": "VLAN vlan110 created on bond0 (100.64.8.1/24) (XXXX:XXXX:110::1/64)", "name": "vlan110", "parent": "bond0", "vlan_id": 108, "protocol": "802.1Q", "ip": "100.64.8.1/24", "ipv6": "XXXX:XXXX:110::1/64", "served": { "net": { "network": "100.64.8.0/24" }, "net6": { "network": "XXXX:XXXX:110::/64", "pool": "XXXX:XXXX:110::1000 - XXXX:XXXX:110::ffff" }, "pd": { "pool": "XXXX:XXXX:a:1000:: - XXXX:XXXX:a:10ff::", "capacity": 256 } }}A failed serving step appears as {"error": "…", "retry": "dtvsol net add vlan110"} under its key, and warning says the VLAN is not fully served. With serve off, served is null and note says which commands serve it later.
Errors: 400 invalid ID, address or protocol; 404 parent not found; 409 VLAN already exists, tag already used on that parent, or address conflict (the answer carries conflict; pass force to override).
DELETE /vlans
Section titled “DELETE /vlans”The VLAN name is taken from the body.
| Name | In | Type | Notes |
|---|---|---|---|
name |
body | string | VLAN interface name, e.g. vlan110. |
curl -X DELETE -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \ -d '{"name":"vlan110"}' http://ROUTER-IP:8880/vlans{ "ok": true, "code": 200, "message": "VLAN vlan110 deleted", "name": "vlan110", "removed_subnets": ["subnet 100.64.8.0"], "removed_pd_pool": null, "removed_routes": [], "removed_nat": null, "removed_forwards": [], "kept": { "rrd": "data/rrd/iface_vlan110.rrd (traffic history; delete by hand if not wanted)" }}Errors: 404 not a managed VLAN; 409 when another VLAN uses it as parent, clients are still registered on it, or CGNAT translates through it (the answer carries a fix).
POST /vlans/disable
Section titled “POST /vlans/disable”Switches a VLAN off without releasing anything; POST /vlans/enable restores it exactly. GET /vlans/disable takes the parameters from the query string.
| Name | In | Type | Notes |
|---|---|---|---|
name |
body | string | VLAN interface name. |
reason |
body | string | Optional note kept with the VLAN. |
curl -X POST -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \ -d '{"name":"vlan110","reason":"OLT maintenance"}' http://ROUTER-IP:8880/vlans/disable{ "ok": true, "code": 200, "message": "VLAN vlan110 disabled", "name": "vlan110", "enabled": false, "disabled_at": "2026-09-27 10:00:00", "reason": "OLT maintenance", "took_offline": ["dhcp4", "dhcp6", "radvd", "link down"], "kept": "delegation range, subnet blocks, NAT pool, port forwards, routes, client reservations and addresses — all restored by: dtvsol vlan enable vlan110", "forwards_still_active": []}Errors: 404 not found; 409 already disabled, an enabled VLAN runs on top of it, or CGNAT translates through it.
POST /vlans/enable
Section titled “POST /vlans/enable”| Name | In | Type | Notes |
|---|---|---|---|
name |
body | string | VLAN interface name. |
curl -X POST -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \ -d '{"name":"vlan110"}' http://ROUTER-IP:8880/vlans/enable{ "ok": true, "code": 200, "message": "VLAN vlan110 enabled", "name": "vlan110", "enabled": true, "restored": ["link up", "addresses", "dhcp4", "dhcp6 + radvd"], "forwards_active": [], "was_disabled_at": "2026-09-27 10:00:00", "was_disabled_reason": "OLT maintenance"}Errors: 404 not found; 409 already enabled or its parent VLAN is disabled.
Interface addresses
Section titled “Interface addresses”POST /ip
Section titled “POST /ip”Adds an IPv4 or IPv6 address to an existing interface. On a managed VLAN it is stored with the VLAN; on a physical port or bond it is stored in the router’s network configuration. Adding an address that is already live only persists it. The address is not served automatically — use POST /net or POST /net6.
| Name | In | Type | Notes |
|---|---|---|---|
iface |
body | string | Interface name. |
ip |
body | CIDR | IPv4 or IPv6 address with prefix length. A network address is corrected to the first host. |
curl -X POST -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \ -d '{"iface":"vlan110","ip":"100.64.9.1/24"}' http://ROUTER-IP:8880/ip{ "ok": true, "code": 201, "message": "IP 100.64.9.1/24 added to vlan110", "persisted": true }When the address was corrected the answer also carries requested, assigned and note. Errors: 400 invalid address; 404 interface not found; 409 when the subnet is already on another interface (over REST there is no override; the action API accepts force=1).
DELETE /ip
Section titled “DELETE /ip”| Name | In | Type | Notes |
|---|---|---|---|
iface |
body | string | Interface name. |
ip |
body | CIDR | Address to remove, as assigned. |
curl -X DELETE -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \ -d '{"iface":"vlan110","ip":"100.64.9.1/24"}' http://ROUTER-IP:8880/ip{ "ok": true, "code": 200, "message": "IP 100.64.9.1/24 removed from vlan110", "warning": "no interface is served in that network any more, so subnet 100.64.9.0 in dhcpd.conf is now unused — kept because it may hold hand-tuned options; remove it deliberately if not wanted", "unused_subnet": { "kind": "subnet", "block": "100.64.9.0", "file": "/etc/dhcp/dhcpd.conf" }}A DHCP subnet or delegation range left unused is named (unused_subnet, unused_pd_pool, pd_warning), never removed. Errors: 400 invalid address; 404 interface not found; 409 when the address is the gateway of a registered client. Other methods on /ip answer 400 Use POST /ip or DELETE /ip.
Static routes
Section titled “Static routes”GET /routes
Section titled “GET /routes”The routes the router manages (restored at boot) and the kernel’s live IPv4 and IPv6 routing tables.
curl -H "X-API-Key: YOUR_API_KEY" http://ROUTER-IP:8880/routes{ "count": 1, "managed": [ { "prefix": "default", "via": "XXX.XXX.XXX.1", "dev": "vlan90", "family": "ipv4", "comment": "upstream", "created": "2026-09-01 10:00:00" } ], "live_ipv4": ["default via XXX.XXX.XXX.1 dev vlan90"], "live_ipv6": []}POST /routes
Section titled “POST /routes”| Name | In | Type | Notes |
|---|---|---|---|
prefix |
body | string | CIDR (10.50.0.0/24, XXXX:XXXX::/48) or default. |
via |
body | IP | Gateway, same family as the prefix. via and/or dev is required. |
dev |
body | string | Outgoing interface. |
comment |
body | string | Optional. |
curl -X POST -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \ -d '{"prefix":"default","via":"XXX.XXX.XXX.1","dev":"vlan90","comment":"upstream"}' \ http://ROUTER-IP:8880/routes{ "ok": true, "code": 201, "message": "Route added", "route": { "prefix": "default", "via": "XXX.XXX.XXX.1", "dev": "vlan90", "family": "ipv4", "comment": "upstream", "created": "2026-09-28 09:00:00" }}Errors: 400 invalid prefix, gateway or interface name; 409 Route already managed; 500 when the kernel refuses the route.
DELETE /routes
Section titled “DELETE /routes”The first managed route with this prefix (and, when given, this via/dev) is removed.
| Name | In | Type | Notes |
|---|---|---|---|
prefix |
body | string | Prefix of the route, or default. |
via |
body | IP | Optional, to pick one of several routes. |
dev |
body | string | Optional, to pick one of several routes. |
curl -X DELETE -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \ -d '{"prefix":"default"}' http://ROUTER-IP:8880/routes{ "ok": true, "code": 200, "message": "Route removed", "route": { "prefix": "default", "via": "XXX.XXX.XXX.1", "dev": "vlan90", "family": "ipv4" }, "kernel_removed": true}Errors: 404 Route … is not managed by DTVSOL.
Dynamic NAT pools
Section titled “Dynamic NAT pools”Source NAT for traffic leaving an interface, translated to one public address or a range. For carrier-grade NAT with deterministic port blocks, see the CGNAT API.
GET /nat
Section titled “GET /nat”The configured pools and the live NAT POSTROUTING rules.
curl -H "X-API-Key: YOUR_API_KEY" http://ROUTER-IP:8880/nat{ "count": 1, "pools": [ { "iface": "vlan90", "pool_start": "XXX.XXX.XXX.10", "pool_end": "XXX.XXX.XXX.20", "exempt": ["10.0.0.0/8"], "comment": "", "created": "2026-09-01 10:00:00" } ], "live_postrouting": ["-P POSTROUTING ACCEPT", "-A POSTROUTING -o vlan90 -j SNAT --to-source XXX.XXX.XXX.10-XXX.XXX.XXX.20 …"]}POST /nat
Section titled “POST /nat”| Name | In | Type | Notes |
|---|---|---|---|
iface |
body | string | Outgoing interface. |
pool_start |
body | IPv4 | First public address. |
pool_end |
body | IPv4 | Optional last public address; omit for a single address. |
exempt |
body | list or string | Optional IPv4 networks (CIDR) not translated; a list or a comma-separated string. |
comment |
body | string | Optional. |
curl -X POST -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \ -d '{"iface":"vlan90","pool_start":"XXX.XXX.XXX.10","pool_end":"XXX.XXX.XXX.20","exempt":["10.0.0.0/8"]}' \ http://ROUTER-IP:8880/nat{ "ok": true, "code": 201, "message": "Dynamic NAT pool configured", "pool": { "iface": "vlan90", "pool_start": "XXX.XXX.XXX.10", "pool_end": "XXX.XXX.XXX.20", "exempt": ["10.0.0.0/8"], "comment": "", "created": "2026-09-28 09:00:00" }}Errors: 400 invalid interface name, pool address, reversed range or exempt network; 404 interface not found; 500 when a rule cannot be installed.
DELETE /nat
Section titled “DELETE /nat”| Name | In | Type | Notes |
|---|---|---|---|
iface |
body | string | Interface whose pool is removed. |
curl -X DELETE -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \ -d '{"iface":"vlan90"}' http://ROUTER-IP:8880/nat{ "ok": true, "code": 200, "message": "NAT pool removed", "iface": "vlan90", "rules_removed": 1, "pool": { "iface": "vlan90", "pool_start": "XXX.XXX.XXX.10", "pool_end": "XXX.XXX.XXX.20" }}Errors: 400 without iface; 404 No DTVSOL NAT pool on ….
Action API equivalents
Section titled “Action API equivalents”| Action | Query parameters | Same as |
|---|---|---|
action=vlan-list |
— | GET /vlans |
action=vlan-add |
parent, vlan_id, protocol, ip, ipv6, label, force=1, serve=0 |
POST /vlans |
action=vlan-del |
name |
DELETE /vlans |
action=vlan-disable |
name, reason |
POST /vlans/disable |
action=vlan-enable |
name |
POST /vlans/enable |
action=ip-add |
iface, ip, force=1 |
POST /ip (with override) |
action=ip-del |
iface, ip |
DELETE /ip |
action=route-list |
— | GET /routes |
action=route-add |
prefix, via, dev, comment |
POST /routes |
action=route-del |
prefix, via, dev |
DELETE /routes |
action=nat-list |
— | GET /nat |
action=nat-add |
iface, pool_start, pool_end, exempt, comment |
POST /nat |
action=nat-del |
iface |
DELETE /nat |