Skip to content

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

The CGNAT configuration, its capacity and a sample of the current assignments (the first five).

Terminal window
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.

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.
Terminal window
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: ….

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.
Terminal window
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.

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.

What is enforced: global settings, per-VLAN mode, binding counts, drop counters and the last hour’s drop summary.

Terminal window
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:"]
}

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.
Terminal window
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).

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.
Terminal window
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.

Same as POST /antispoof/{iface} with mode default.

Terminal window
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 } }

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.
Terminal window
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.

Normally not needed: every client or VLAN change, and every DHCP lease change, rebuilds the rules automatically.

Terminal window
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.

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

This site was written with the help of AI and checked by our team.