CGNAT and Anti-spoofing API
Two subscriber-protection features of the DTVSOL Super Router:
- CGNAT maps subscribers’ private addresses to a pool of public IPv4 addresses. Each subscriber gets a fixed slot: one public address and a fixed block of ports on it. Because the mapping is deterministic, a public address and port can always be traced back to one subscriber (
/cgnat/lookup). Assignments are also written to an audit log on the router. - Anti-spoofing binds every subscriber’s source MAC, IP address and VLAN. On every enforced access VLAN a packet is forwarded only when its source MAC and IP are a pair the router knows, and an ARP packet only when its sender MAC and IP are. Everything else is dropped and (rate-limited) logged with the MAC that sent it.
Authentication, error format and status codes are described in the API overview. The same features are available from the CLI as dtvsol cgnat … and dtvsol antispoof ….
GET /cgnat
Section titled “GET /cgnat”The CGNAT configuration, its capacity and a sample of the current assignments (the first five).
curl -H "X-API-Key: YOUR_API_KEY" http://ROUTER-IP:8880/cgnat{ "enabled": true, "iface": "bond0", "pool": "XXX.XXX.XXX.10-XXX.XXX.XXX.20", "pool_ips": 11, "port_range": "1024-65535", "block_size": 2048, "subs_per_ip": 31, "capacity": 341, "assigned": 2, "free": 339, "exempt": ["10.0.0.0/30"], "sample": [ { "mac": "AA:BB:CC:DD:EE:FF", "private_ip": "100.64.0.2", "public": "XXX.XXX.XXX.10:1024-3071", "slot": 0 } ], "audit_log": "/opt/dtvsol/log/cgnat-mappings.log"}subs_per_ip is (port_max − port_min + 1) / block_size; capacity is pool_ips × subs_per_ip.
POST /cgnat
Section titled “POST /cgnat”Change any subset of the CGNAT settings. Fields you leave out keep their current value; defaults are port_min 1024, port_max 65535, block_size 2048.
| Name | In | Type | Notes |
|---|---|---|---|
enabled |
body | boolean | true/false (also 1/0, on/off, yes/no). Enabling requires a valid, existing iface and a non-empty pool. |
iface |
body | string | The WAN interface the public pool lives on, e.g. bond0. |
pool |
body | string or array | Public IPv4 addresses: single addresses and first-last ranges, comma-separated or as an array. |
port_min |
body | integer | First port handed out (default 1024). |
port_max |
body | integer | Last port handed out (default 65535). |
block_size |
body | integer | Ports per subscriber (default 2048). |
exempt |
body | string or array | Networks (a.b.c.d/nn) that are never NATed, comma-separated or as an array. |
curl -X POST http://ROUTER-IP:8880/cgnat \ -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \ -d '{"enabled": true, "iface": "bond0", "pool": "XXX.XXX.XXX.10-XXX.XXX.XXX.20", "block_size": 2048, "exempt": "10.0.0.0/30"}'{ "ok": true, "code": 200, "message": "CGNAT updated", "status": { "enabled": true, "iface": "bond0", "capacity": 341, "assigned": 2, "free": 339 }}status is the full GET /cgnat answer. Errors: 400 — Enable requires a valid WAN iface, Enable requires a non-empty public IP pool, Invalid exempt network: ….
GET /cgnat/lookup
Section titled “GET /cgnat/lookup”Who held a public address and port: the subscriber whose slot covers them. Use it to answer abuse and law-enforcement requests.
| Name | In | Type | Notes |
|---|---|---|---|
public_ip |
query | string | The public address seen outside. Required. |
port |
query | integer | The public source port seen outside. |
curl -H "X-API-Key: YOUR_API_KEY" \ "http://ROUTER-IP:8880/cgnat/lookup?public_ip=XXX.XXX.XXX.10&port=2000"{ "ok": true, "code": 200, "found": true, "mac": "AA:BB:CC:DD:EE:FF", "private_ip": "100.64.0.2", "hostname": "client-aabbccddeeff", "public_ip": "XXX.XXX.XXX.10", "port_start": 1024, "port_end": 3071, "slot": 0}When no slot matches: {"ok": true, "code": 200, "found": false, "public_ip": "XXX.XXX.XXX.10", "port": 2000}. An invalid public_ip gives 400 Invalid public_ip. The lookup reflects the current assignments; for past times use the audit log.
Anti-spoofing
Section titled “Anti-spoofing”Modes:
| Mode | Behaviour |
|---|---|
strict |
Only registered clients pass, locked to their address. Unregistered devices still get DHCP (so they appear in the IP lists) but nothing else. The default. |
dynamic |
Registered clients are locked to their address, and unregistered devices are allowed on the address the DHCP server leased them. |
off |
Per-VLAN only: excludes that VLAN. |
default |
Per-VLAN only: removes the override so the VLAN follows the global mode. |
When enabled, every VLAN the DHCP server serves is enforced in the global mode; a per-VLAN override can change the mode, switch a VLAN off, or add a VLAN the DHCP server does not serve (static-only segments). For IPv6, a client’s reserved address, its DHCPv6 address and its delegated prefix are bound to its MAC; link-local always passes; a Router Advertisement or Redirect from a subscriber is dropped. Spoofing between subscribers inside the same VLAN never reaches the router and must be stopped by the OLT’s split-horizon.
GET /antispoof
Section titled “GET /antispoof”What is enforced: global settings, per-VLAN mode, binding counts, drop counters and the last hour’s drop summary.
curl -H "X-API-Key: YOUR_API_KEY" http://ROUTER-IP:8880/antispoof{ "enabled": true, "mode": "strict", "log": true, "exempt": ["10.0.0.0/30"], "overrides": { "vlan200": "dynamic" }, "served": ["vlan100", "vlan200"], "enforced": { "vlan100": { "mode": "strict", "exists": true, "bindings4": 120, "bindings6": 118, "dropped": { "ip4": 42, "ip6": 3, "arp": 7 }, "clients": 120, "service": false, "leases": 0 } }, "switched_off": [], "live": { "v4_chain": true, "v4_rules": 240, "v4_forward": true, "v4_input": true, "v6_chain": true, "v6_rules": 236, "v6_forward": true, "v6_input": true }, "last_hour": { "attempts": 5, "devices": 1 }, "log_prefixes": ["DTVSOL_SPOOF:", "DTVSOL_ARPSPOOF:"]}POST /antispoof
Section titled “POST /antispoof”Send at least one of the fields; the others keep their value. Disabling removes every rule but keeps the settings.
| Name | In | Type | Notes |
|---|---|---|---|
enabled |
body | boolean | true/false (also 1/0, on/off, yes/no); any other value is a 400. |
mode |
body | string | strict or dynamic. |
log |
body | boolean | Rate-limited kernel log of what was dropped. |
exempt |
body | string or array | Networks (CIDR) allowed from any MAC on every enforced VLAN — for example an OLT’s relay or management address. Comma-separated or an array; an empty value clears the list. |
curl -X POST http://ROUTER-IP:8880/antispoof \ -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \ -d '{"enabled": true, "mode": "strict", "exempt": "10.0.0.0/30"}'{ "ok": true, "code": 200, "message": "Anti-spoofing enabled", "apply": { "applied": true, "enabled": true, "bindings4": 120, "bindings6": 118 }, "status": { "enabled": true, "mode": "strict" }}message is one of Anti-spoofing enabled, Anti-spoofing updated, Anti-spoofing disabled — rules removed, Anti-spoofing is off. status is the full GET /antispoof answer. Errors: 400 for nothing to set, a bad boolean, a bad mode or an invalid exempt network; 500 when the rules could not be applied (ok: false, details in apply.errors).
POST /antispoof/{iface}
Section titled “POST /antispoof/{iface}”| Name | In | Type | Notes |
|---|---|---|---|
iface |
path | string | Interface name, e.g. vlan100 (up to 15 characters). Must exist or be served by DHCP. |
mode |
body | string | strict, dynamic, off or default. |
curl -X POST http://ROUTER-IP:8880/antispoof/vlan100 \ -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \ -d '{"mode": "dynamic"}'{ "ok": true, "code": 200, "message": "vlan100 set to dynamic", "iface": "vlan100", "requested": "dynamic", "effective": "dynamic", "note": null, "apply": { "applied": true }}When anti-spoofing is globally off, the mode is recorded, apply is {"applied": false, "action": "feature off"} and note says it takes effect when anti-spoofing is switched on. Errors: 400 invalid interface name or mode; 404 the interface does not exist and is not served by DHCP.
DELETE /antispoof/{iface}
Section titled “DELETE /antispoof/{iface}”Same as POST /antispoof/{iface} with mode default.
curl -X DELETE -H "X-API-Key: YOUR_API_KEY" http://ROUTER-IP:8880/antispoof/vlan100{ "ok": true, "code": 200, "message": "vlan100 follows the default mode again", "iface": "vlan100", "requested": "default", "effective": "strict", "note": null, "apply": { "applied": true } }GET /antispoof/log
Section titled “GET /antispoof/log”Dropped packets from the kernel log, newest first, and the devices that caused them grouped by VLAN + MAC + source address (most drops first).
| Name | In | Type | Notes |
|---|---|---|---|
since |
query | string | 30m, 2h, 1d, 45s, or a time like 2026-09-17 10:00. Default one hour. |
limit |
query | integer | How many entries to return, 1–500 (default 50). total always counts all. |
curl -H "X-API-Key: YOUR_API_KEY" "http://ROUTER-IP:8880/antispoof/log?since=2h&limit=20"{ "ok": true, "code": 200, "enabled": true, "since": "2 hours ago", "total": 3, "offenders": [ { "iface": "vlan100", "mac": "AA:BB:CC:DD:EE:FF", "src": "100.64.0.9", "count": 3, "last": "2026-09-27T10:00:02+0000", "ip": 2, "arp": 1 } ], "entries": [ { "time": "2026-09-27T10:00:02+0000", "kind": "ip", "iface": "vlan100", "mac": "AA:BB:CC:DD:EE:FF", "src": "100.64.0.9", "dst": "100.64.0.1", "proto": "UDP", "dport": 53 }, { "time": "2026-09-27T10:00:01+0000", "kind": "arp", "iface": "vlan100", "mac": "AA:BB:CC:DD:EE:FF", "src": "100.64.0.9", "dst": "100.64.0.1", "op": "reply" } ]}IP entries carry proto (ICMPv6 types named, e.g. ICMPv6/RA) and dport; ARP entries carry op (request or reply). A bad since gives 400.
POST /antispoof/sync
Section titled “POST /antispoof/sync”Normally not needed: every client or VLAN change, and every DHCP lease change, rebuilds the rules automatically.
curl -X POST -H "X-API-Key: YOUR_API_KEY" http://ROUTER-IP:8880/antispoof/sync{ "ok": true, "code": 200, "message": "Anti-spoofing rules rebuilt", "applied": true, "enabled": true }When anti-spoofing is off: "message": "Anti-spoofing is off; nothing to apply". 500 with ok: false when the rules could not be applied.
Action API equivalents
Section titled “Action API equivalents”The same operations through GET /api?action=…, with every parameter in the query string (see the action API).
| Action | Query parameters | Same as |
|---|---|---|
action=cgnat-status |
— | GET /cgnat |
action=cgnat-set |
enabled, iface, pool, port_min, port_max, block_size, exempt |
POST /cgnat |
action=cgnat-lookup |
public_ip, port |
GET /cgnat/lookup |
action=antispoof-status |
— | GET /antispoof |
action=antispoof-set |
enabled, mode, log, exempt |
POST /antispoof |
action=antispoof-iface |
iface, mode (strict, dynamic, off, default) |
POST /antispoof/{iface} |
action=antispoof-log |
since, limit |
GET /antispoof/log |
action=antispoof-sync |
— | POST /antispoof/sync |