174 lines
7.3 KiB
Python
174 lines
7.3 KiB
Python
"""NAT rule tools: source NAT, destination NAT, 1:1, NPT."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any
|
|
|
|
from mcp.server.fastmcp import FastMCP
|
|
|
|
from opnsense_mcp.audit import audit_log
|
|
from opnsense_mcp.client import OpnsenseAPIError, get_client
|
|
from opnsense_mcp.config import settings
|
|
from opnsense_mcp.guards import is_dry_run_response, require_writes
|
|
|
|
|
|
def _search_nat(nat_type: str, search_phrase: str, row_count: int) -> str:
|
|
client = get_client()
|
|
try:
|
|
result = client.search_get("firewall", nat_type, "searchRule", search_phrase, row_count)
|
|
except OpnsenseAPIError:
|
|
result = client.search_grid("firewall", nat_type, "searchRule", search_phrase, row_count)
|
|
return json.dumps(result, indent=2, default=str)
|
|
|
|
|
|
def _get_nat(nat_type: str, uuid: str) -> str:
|
|
client = get_client()
|
|
result = client.get("firewall", nat_type, "getRule", uuid)
|
|
return json.dumps(result, indent=2, default=str)
|
|
|
|
|
|
def _add_nat(nat_type: str, action: str, rule: dict[str, Any]) -> str:
|
|
payload = {"rule": rule}
|
|
if settings.dry_run:
|
|
return json.dumps(is_dry_run_response(action, payload), indent=2)
|
|
require_writes(action, payload)
|
|
client = get_client()
|
|
result = client.post("firewall", nat_type, "addRule", data=payload)
|
|
audit_log(action, {"rule": rule, "result": result})
|
|
return json.dumps(result, indent=2, default=str)
|
|
|
|
|
|
def _update_nat(nat_type: str, action: str, uuid: str, rule: dict[str, Any]) -> str:
|
|
payload = {"rule": rule}
|
|
if settings.dry_run:
|
|
return json.dumps(is_dry_run_response(action, {"uuid": uuid, **payload}), indent=2)
|
|
require_writes(action, {"uuid": uuid, **payload})
|
|
client = get_client()
|
|
result = client.post("firewall", nat_type, "setRule", uuid, data=payload)
|
|
audit_log(action, {"uuid": uuid, "rule": rule, "result": result})
|
|
return json.dumps(result, indent=2, default=str)
|
|
|
|
|
|
def _delete_nat(nat_type: str, action: str, uuid: str) -> str:
|
|
if settings.dry_run:
|
|
return json.dumps(is_dry_run_response(action, {"uuid": uuid}), indent=2)
|
|
require_writes(action, {"uuid": uuid})
|
|
client = get_client()
|
|
result = client.post("firewall", nat_type, "delRule", uuid)
|
|
audit_log(action, {"uuid": uuid, "result": result})
|
|
return json.dumps(result, indent=2, default=str)
|
|
|
|
|
|
def _toggle_nat(nat_type: str, action: str, uuid: str, enabled: bool) -> str:
|
|
payload = {"uuid": uuid, "enabled": enabled}
|
|
if settings.dry_run:
|
|
return json.dumps(is_dry_run_response(action, payload), indent=2)
|
|
require_writes(action, payload)
|
|
client = get_client()
|
|
flag = "1" if enabled else "0"
|
|
result = client.post("firewall", nat_type, "toggleRule", uuid, flag)
|
|
audit_log(action, payload | {"result": result})
|
|
return json.dumps(result, indent=2, default=str)
|
|
|
|
|
|
def register(mcp: FastMCP) -> None:
|
|
# ── Source NAT ──────────────────────────────────────────────────────
|
|
@mcp.tool()
|
|
def opnsense_search_source_nat_rules(search_phrase: str = "", row_count: int = 100) -> str:
|
|
"""Search outbound / source NAT rules."""
|
|
return _search_nat("source_nat", search_phrase, row_count)
|
|
|
|
@mcp.tool()
|
|
def opnsense_get_source_nat_rule(uuid: str) -> str:
|
|
"""Get an outbound NAT rule by UUID."""
|
|
return _get_nat("source_nat", uuid)
|
|
|
|
@mcp.tool()
|
|
def opnsense_add_source_nat_rule(rule: dict[str, Any]) -> str:
|
|
"""Add an outbound / source NAT rule."""
|
|
return _add_nat("source_nat", "add_source_nat_rule", rule)
|
|
|
|
@mcp.tool()
|
|
def opnsense_update_source_nat_rule(uuid: str, rule: dict[str, Any]) -> str:
|
|
"""Update an outbound NAT rule by UUID."""
|
|
return _update_nat("source_nat", "update_source_nat_rule", uuid, rule)
|
|
|
|
@mcp.tool()
|
|
def opnsense_delete_source_nat_rule(uuid: str) -> str:
|
|
"""Delete an outbound NAT rule by UUID."""
|
|
return _delete_nat("source_nat", "delete_source_nat_rule", uuid)
|
|
|
|
@mcp.tool()
|
|
def opnsense_toggle_source_nat_rule(uuid: str, enabled: bool = True) -> str:
|
|
"""Enable or disable an outbound NAT rule."""
|
|
return _toggle_nat("source_nat", "toggle_source_nat_rule", uuid, enabled)
|
|
|
|
# ── Destination NAT / Port Forwards ───────────────────────────────────
|
|
@mcp.tool()
|
|
def opnsense_search_d_nat_rules(search_phrase: str = "", row_count: int = 100) -> str:
|
|
"""Search destination NAT / port forward rules."""
|
|
return _search_nat("d_nat", search_phrase, row_count)
|
|
|
|
@mcp.tool()
|
|
def opnsense_search_port_forwards(search_phrase: str = "", row_count: int = 100) -> str:
|
|
"""Search port forward rules (alias for destination NAT search)."""
|
|
return _search_nat("d_nat", search_phrase, row_count)
|
|
|
|
@mcp.tool()
|
|
def opnsense_get_d_nat_rule(uuid: str) -> str:
|
|
"""Get a destination NAT / port forward rule by UUID."""
|
|
return _get_nat("d_nat", uuid)
|
|
|
|
@mcp.tool()
|
|
def opnsense_add_d_nat_rule(rule: dict[str, Any]) -> str:
|
|
"""Add a destination NAT / port forward rule."""
|
|
return _add_nat("d_nat", "add_d_nat_rule", rule)
|
|
|
|
@mcp.tool()
|
|
def opnsense_update_d_nat_rule(uuid: str, rule: dict[str, Any]) -> str:
|
|
"""Update a destination NAT rule by UUID."""
|
|
return _update_nat("d_nat", "update_d_nat_rule", uuid, rule)
|
|
|
|
@mcp.tool()
|
|
def opnsense_delete_d_nat_rule(uuid: str) -> str:
|
|
"""Delete a destination NAT rule by UUID."""
|
|
return _delete_nat("d_nat", "delete_d_nat_rule", uuid)
|
|
|
|
@mcp.tool()
|
|
def opnsense_toggle_d_nat_rule(uuid: str, enabled: bool = True) -> str:
|
|
"""Enable or disable a destination NAT rule."""
|
|
return _toggle_nat("d_nat", "toggle_d_nat_rule", uuid, enabled)
|
|
|
|
# ── 1:1 NAT ─────────────────────────────────────────────────────────
|
|
@mcp.tool()
|
|
def opnsense_search_one_to_one_rules(search_phrase: str = "", row_count: int = 100) -> str:
|
|
"""Search 1:1 NAT rules."""
|
|
return _search_nat("one_to_one", search_phrase, row_count)
|
|
|
|
@mcp.tool()
|
|
def opnsense_get_one_to_one_rule(uuid: str) -> str:
|
|
"""Get a 1:1 NAT rule by UUID."""
|
|
return _get_nat("one_to_one", uuid)
|
|
|
|
@mcp.tool()
|
|
def opnsense_add_one_to_one_rule(rule: dict[str, Any]) -> str:
|
|
"""Add a 1:1 NAT rule."""
|
|
return _add_nat("one_to_one", "add_one_to_one_rule", rule)
|
|
|
|
@mcp.tool()
|
|
def opnsense_delete_one_to_one_rule(uuid: str) -> str:
|
|
"""Delete a 1:1 NAT rule by UUID."""
|
|
return _delete_nat("one_to_one", "delete_one_to_one_rule", uuid)
|
|
|
|
# ── NPT (IPv6) ──────────────────────────────────────────────────────
|
|
@mcp.tool()
|
|
def opnsense_search_npt_rules(search_phrase: str = "", row_count: int = 100) -> str:
|
|
"""Search NPT (IPv6 prefix translation) rules."""
|
|
return _search_nat("npt", search_phrase, row_count)
|
|
|
|
@mcp.tool()
|
|
def opnsense_get_npt_rule(uuid: str) -> str:
|
|
"""Get an NPT rule by UUID."""
|
|
return _get_nat("npt", uuid)
|