[SOLVED] How to Force Update a Single Firewall State in OPNsense?

Started by bugz000, August 29, 2025, 01:50:38 AM

Previous topic - Next topic
Hi all,

I've recently switched to updating my OPNsense firewall via the API, adding/removing IPs from an alias and refreshing the rules proactively rather than polling. My floating firewall rule drops all packets to/from IPs in that alias.

The problem is that even with API-driven updates, the firewall can take upwards of 20 minutes to actually reflect changes. I've seen similar reports in:

https://www.reddit.com/r/OPNsenseFirewall/comments/13l6alq/opnsense_states/
https://forum.opnsense.org/index.php?topic=31995.0
https://forum.opnsense.org/index.php?topic=21074.0
https://forum.opnsense.org/index.php?topic=30962.0
https://forum.opnsense.org/index.php?topic=38305.0
https://forum.opnsense.org/index.php?topic=31706.0
https://github.com/opnsense/core/issues/6404

From what I understand, using flush_states or kill_states via the API can force a refresh, but both seem to disconnect all active connections, which isn't a viable solution for me.

My question:

Is there a way to force a single firewall state (or a set of states related to a specific IP) to refresh without dropping all other connections?

I haven't found any official documentation on this, so any guidance, undocumented API endpoints, or workarounds would be greatly appreciated.

Thanks!

well it seems it's all boiled down to one thing

https://docs.opnsense.org/development/api/core/diagnostics.html



https://forum.opnsense.org/index.php?topic=31706.0

how to operate this api

the documentation on it is... well, it's totally non existent.

so...
does anyone have a clue how this works?


[solved]

del url is not del_state it's delstate, docs are wrong.

Code (php) Select
    $del_url  = $opnsense_host . "/api/diagnostics/firewall/delState";
query states first
Code (php) Select
    $query_url = $opnsense_host . "/api/diagnostics/firewall/query_states";

get response
Code (json) Select
{
  "label": "21d61fb65e9a253dc46b79181dc7044c",
  "descr": "Default allow LAN to any rule",
  "nat_addr": "",
  "nat_port": "",
  "gateway": "",
  "iface": "all",
  "proto": "udp",
  "ipproto": "ipv4",
  "flags": [],
  "direction": "in",
  "dst_addr": "185.211.73.104",
  "dst_port": "41677",
  "src_addr": "192.168.2.189",
  "src_port": "3232",
  "state": "MULTIPLE:MULTIPLE",
  "age": "32:21:44",
  "expires": "00:00:58",
  "pkts": {
    "out": 18123,
    "in": 24102
  },
  "bytes": {
    "out": 2349563,
    "in": 2946399
  },
  "rule": 59,
  "id": "09048e6900000000/f1c4a296",
  "interface": "all"
}

id field:
Code (php) Select
  "id": "09048e6900000000/f1c4a296",
is actually the stateid and creator id (anemic docs, non existent/wrong.)
Code (php) Select
                list($stateid, $creatorid) = explode('/', $full_id);
pass as url params (anemic docs, non existent/wrong, says post, wrong.)
Code (php) Select
$del_url_full = $del_url . '/' . $stateid . '/' . $creatorid;
$ch = curl_init($del_url_full);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_USERPWD, $opnsense_key . ':' . $opnsense_secret);
curl_setopt($ch, CURLOPT_POSTFIELDS, '');
$del_resp = curl_exec($ch);
curl_close($ch);

attention; setting length, despite blank body (not mentioned in docs/non existent/wrong.)
Code (php) Select
                curl_setopt($ch, CURLOPT_POSTFIELDS, '');
and by parsing through and deleting this way you can purge the states by ip.

Code (php) Select
function purgeStatesByIP($opnsense_host, $opnsense_key, $opnsense_secret, $ip) {
    // Query all states
    $query_url = $opnsense_host . "/api/diagnostics/firewall/query_states";

    $ch = curl_init($query_url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_USERPWD, $opnsense_key . ':' . $opnsense_secret);
    $resp = curl_exec($ch);
    curl_close($ch);

    if (!$resp) {
        throw new Exception("Failed to query states.");
    }

    $states = json_decode($resp, true);

    if (!is_array($states)) {
        throw new Exception("Invalid response: " . $resp);
    }

    $del_url = $opnsense_host . "/api/diagnostics/firewall/delState";
    $deleted = [];

    foreach ($states as $state) {
        if ($state['src_addr'] === $ip || $state['dst_addr'] === $ip) {
            // Split the "id" field into stateid + creatorid
            list($stateid, $creatorid) = explode('/', $state['id']);
            $del_url_full = $del_url . '/' . $stateid . '/' . $creatorid;

            $ch = curl_init($del_url_full);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_POST, true);
            curl_setopt($ch, CURLOPT_USERPWD, $opnsense_key . ':' . $opnsense_secret);
            curl_setopt($ch, CURLOPT_POSTFIELDS, ''); // Required even if empty
            $del_resp = curl_exec($ch);
            curl_close($ch);

            $deleted[] = [
                'state' => $state,
                'response' => $del_resp
            ];
        }
    }

    return $deleted;
}

// Example usage:
try {
    $deletedStates = purgeStatesByIP(
        "https://firewall.example.com",
        "your_api_key",
        "your_api_secret",
        "185.211.73.104"
    );

    echo "Deleted " . count($deletedStates) . " states.\n";
    foreach ($deletedStates as $d) {
        echo " - " . $d['state']['src_addr'] . " → " . $d['state']['dst_addr'] . "\n";
    }
} catch (Exception $e) {
    echo "Error: " . $e->getMessage() . "\n";
}


hope this helps someone...

https://docs.opnsense.org/development/api/core/diagnostics.html
this page needs serious work
issue since 2021