summaryrefslogtreecommitdiff
path: root/plugins/modules/vyos_firewall_rules.py
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/modules/vyos_firewall_rules.py')
-rw-r--r--plugins/modules/vyos_firewall_rules.py492
1 files changed, 492 insertions, 0 deletions
diff --git a/plugins/modules/vyos_firewall_rules.py b/plugins/modules/vyos_firewall_rules.py
new file mode 100644
index 0000000..f535f3a
--- /dev/null
+++ b/plugins/modules/vyos_firewall_rules.py
@@ -0,0 +1,492 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: vyos_firewall_rules
+short_description: Manage firewall rules on VyOS via the REST API.
+description:
+ - Manages VyOS firewall rule sets and individual rules using the HTTPS REST API.
+ - Mirrors C(vyos.vyos.vyos_firewall_rules) but uses the HTTP API.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ config:
+ description: List of firewall rule set configurations.
+ type: list
+ elements: dict
+ suboptions:
+ afi:
+ description: Address family.
+ type: str
+ choices: [ipv4, ipv6]
+ required: true
+ rule_sets:
+ description: List of named rule sets.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: Rule set name.
+ type: str
+ required: true
+ default_action:
+ description: Default action for unmatched traffic.
+ type: str
+ choices: [drop, reject, accept]
+ default: drop
+ description:
+ description: Rule set description.
+ type: str
+ rules:
+ description: List of firewall rules.
+ type: list
+ elements: dict
+ suboptions:
+ number:
+ description: Rule number (1-999999).
+ type: int
+ required: true
+ action:
+ description: Rule action.
+ type: str
+ choices: [drop, reject, accept, inspect]
+ description:
+ description: Rule description.
+ type: str
+ protocol:
+ description: Protocol (e.g. C(tcp), C(udp), C(icmp), C(all)).
+ type: str
+ source:
+ description: Source match criteria.
+ type: dict
+ suboptions:
+ address:
+ description: Source IP address or network.
+ type: str
+ port:
+ description: Source port or port range.
+ type: str
+ group:
+ description: Address or network group.
+ type: dict
+ suboptions:
+ address_group:
+ type: str
+ network_group:
+ type: str
+ destination:
+ description: Destination match criteria.
+ type: dict
+ suboptions:
+ address:
+ description: Destination IP address or network.
+ type: str
+ port:
+ description: Destination port or port range.
+ type: str
+ group:
+ description: Address or network group.
+ type: dict
+ suboptions:
+ address_group:
+ type: str
+ network_group:
+ type: str
+ state:
+ description: Connection state matching.
+ type: dict
+ suboptions:
+ established:
+ type: bool
+ new:
+ type: bool
+ related:
+ type: bool
+ invalid:
+ type: bool
+ enabled:
+ description: Whether the rule is active.
+ type: bool
+ default: true
+ state:
+ description:
+ - C(merged): Add/update listed rule sets and rules.
+ - C(replaced): Replace listed rule sets entirely.
+ - C(overridden): Replace the entire firewall rule config.
+ - C(deleted): Remove listed (or all) rule sets.
+ - C(gathered): Read firewall rule config from device.
+ type: str
+ choices: [merged, replaced, overridden, deleted, gathered]
+ default: merged
+ hostname:
+ description: IP address or FQDN of the VyOS device.
+ type: str
+ required: true
+ port:
+ description: HTTPS port for the REST API.
+ type: int
+ default: 443
+ api_key:
+ description: API key configured on the device.
+ type: str
+ required: true
+ no_log: true
+ timeout:
+ description: Request timeout in seconds.
+ type: int
+ default: 30
+ verify_ssl:
+ description: Validate the device's TLS certificate.
+ type: bool
+ default: false
+requirements:
+ - VyOS 1.3+
+seealso:
+ - module: vyos.vyos.vyos_firewall_rules
+examples: |
+ - name: Create an IPv4 firewall rule set
+ vyos.rest.vyos_firewall_rules:
+ hostname: 192.168.1.1
+ api_key: MY-KEY
+ config:
+ - afi: ipv4
+ rule_sets:
+ - name: OUTSIDE-IN
+ default_action: drop
+ rules:
+ - number: 10
+ action: accept
+ protocol: tcp
+ destination:
+ port: "80,443"
+ state:
+ established: true
+ related: true
+ state: merged
+
+ - name: Delete all firewall rules
+ vyos.rest.vyos_firewall_rules:
+ hostname: 192.168.1.1
+ api_key: MY-KEY
+ state: deleted
+"""
+
+RETURN = r"""
+before:
+ description: Firewall rule config before the module ran.
+ returned: always
+ type: list
+after:
+ description: Firewall rule config after the module ran.
+ returned: when changed
+ type: list
+gathered:
+ description: Firewall config read from device (state=gathered).
+ returned: when state is gathered
+ type: list
+commands:
+ description: set/delete commands issued.
+ returned: always
+ type: list
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VYOS_REST_CONNECTION_ARGSPEC,
+ VyOSRestClient,
+ VyOSRestError,
+)
+
+
+_FW_PATH = {
+ "ipv4": ["firewall", "name"],
+ "ipv6": ["firewall", "ipv6-name"],
+}
+
+
+def _get_fw_rules(client):
+ try:
+ result = client.retrieve_show_config(["firewall"])
+ data = result.get("data") or {}
+ out = []
+ for afi, key in [("ipv4", "name"), ("ipv6", "ipv6-name")]:
+ sets_data = data.get(key, {})
+ if not isinstance(sets_data, dict):
+ continue
+ rule_sets = []
+ for rs_name, rs_data in sets_data.items():
+ if not isinstance(rs_data, dict):
+ continue
+ rs = {
+ "name": rs_name,
+ "default_action": rs_data.get("default-action", "drop"),
+ }
+ if "description" in rs_data:
+ rs["description"] = rs_data["description"]
+ rules = []
+ for rnum, rdata in (rs_data.get("rule") or {}).items():
+ if not isinstance(rdata, dict):
+ continue
+ rule = {"number": int(rnum)}
+ if "action" in rdata:
+ rule["action"] = rdata["action"]
+ if "description" in rdata:
+ rule["description"] = rdata["description"]
+ if "protocol" in rdata:
+ rule["protocol"] = rdata["protocol"]
+ rule["enabled"] = "disable" not in rdata
+ rules.append(rule)
+ if rules:
+ rs["rules"] = sorted(rules, key=lambda r: r["number"])
+ rule_sets.append(rs)
+ if rule_sets:
+ out.append({"afi": afi, "rule_sets": rule_sets})
+ return out
+ except VyOSRestError:
+ return []
+
+
+def _apply_rule_set(client, afi, rs, commands):
+ base = _FW_PATH[afi] + [rs["name"]]
+ client.configure_set(base)
+ commands.append("set {p}".format(p=" ".join(base)))
+
+ if rs.get("default_action"):
+ client.configure_set(base + ["default-action"], rs["default_action"])
+ commands.append(
+ "set {p} default-action {a}".format(
+ p=" ".join(base),
+ a=rs["default_action"],
+ ),
+ )
+ if rs.get("description"):
+ client.configure_set(base + ["description"], rs["description"])
+ commands.append(
+ "set {p} description '{d}'".format(
+ p=" ".join(base),
+ d=rs["description"],
+ ),
+ )
+
+ for rule in rs.get("rules") or []:
+ rbase = base + ["rule", str(rule["number"])]
+ client.configure_set(rbase)
+ commands.append("set {p}".format(p=" ".join(rbase)))
+
+ if rule.get("action"):
+ client.configure_set(rbase + ["action"], rule["action"])
+ commands.append(
+ "set {p} action {a}".format(p=" ".join(rbase), a=rule["action"]),
+ )
+ if rule.get("description"):
+ client.configure_set(rbase + ["description"], rule["description"])
+ if rule.get("protocol"):
+ client.configure_set(rbase + ["protocol"], rule["protocol"])
+ commands.append(
+ "set {p} protocol {pr}".format(
+ p=" ".join(rbase),
+ pr=rule["protocol"],
+ ),
+ )
+ for direction in ("source", "destination"):
+ match = rule.get(direction)
+ if not match:
+ continue
+ mbase = rbase + [direction]
+ if match.get("address"):
+ client.configure_set(mbase + ["address"], match["address"])
+ commands.append(
+ "set {p} address '{a}'".format(
+ p=" ".join(mbase),
+ a=match["address"],
+ ),
+ )
+ if match.get("port"):
+ client.configure_set(mbase + ["port"], match["port"])
+ commands.append(
+ "set {p} port {pt}".format(
+ p=" ".join(mbase),
+ pt=match["port"],
+ ),
+ )
+
+ state_match = rule.get("state")
+ if state_match:
+ sbase = rbase + ["state"]
+ for st_name in ("established", "new", "related", "invalid"):
+ if state_match.get(st_name):
+ client.configure_set(sbase + [st_name], "enable")
+ commands.append(
+ "set {p} {s} enable".format(
+ p=" ".join(sbase),
+ s=st_name,
+ ),
+ )
+
+ if "enabled" in rule and not rule["enabled"]:
+ client.configure_set(rbase + ["disable"])
+ commands.append("set {p} disable".format(p=" ".join(rbase)))
+
+
+def main():
+ rule_spec = dict(
+ number=dict(type="int", required=True),
+ action=dict(type="str", choices=["drop", "reject", "accept", "inspect"]),
+ description=dict(type="str"),
+ protocol=dict(type="str"),
+ source=dict(
+ type="dict",
+ options=dict(
+ address=dict(type="str"),
+ port=dict(type="str"),
+ group=dict(
+ type="dict",
+ options=dict(
+ address_group=dict(type="str"),
+ network_group=dict(type="str"),
+ ),
+ ),
+ ),
+ ),
+ destination=dict(
+ type="dict",
+ options=dict(
+ address=dict(type="str"),
+ port=dict(type="str"),
+ group=dict(
+ type="dict",
+ options=dict(
+ address_group=dict(type="str"),
+ network_group=dict(type="str"),
+ ),
+ ),
+ ),
+ ),
+ state=dict(
+ type="dict",
+ options=dict(
+ established=dict(type="bool"),
+ new=dict(type="bool"),
+ related=dict(type="bool"),
+ invalid=dict(type="bool"),
+ ),
+ ),
+ enabled=dict(type="bool", default=True),
+ )
+
+ argument_spec = dict(
+ config=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ afi=dict(type="str", required=True, choices=["ipv4", "ipv6"]),
+ rule_sets=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ name=dict(type="str", required=True),
+ default_action=dict(
+ type="str",
+ choices=["drop", "reject", "accept"],
+ default="drop",
+ ),
+ description=dict(type="str"),
+ rules=dict(type="list", elements="dict", options=rule_spec),
+ ),
+ ),
+ ),
+ ),
+ state=dict(
+ type="str",
+ default="merged",
+ choices=["merged", "replaced", "overridden", "deleted", "gathered"],
+ ),
+ )
+ argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC)
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+
+ client = VyOSRestClient(module)
+ state = module.params["state"]
+ config = module.params.get("config") or []
+ commands = []
+ changed = False
+
+ before = _get_fw_rules(client)
+
+ if state == "gathered":
+ module.exit_json(changed=False, gathered=before, before=before, commands=[])
+
+ if module.check_mode:
+ module.exit_json(changed=True, before=before, commands=["(check mode)"])
+
+ try:
+ if state in ("deleted", "overridden") and not config:
+ for afi_key in ("name", "ipv6-name"):
+ try:
+ client.configure_delete(["firewall", afi_key])
+ commands.append(
+ "delete firewall {k}".format(k=afi_key),
+ )
+ except VyOSRestError:
+ pass
+ changed = True
+
+ elif state == "deleted" and config:
+ for entry in config:
+ afi = entry["afi"]
+ base_path = _FW_PATH[afi]
+ for rs in entry.get("rule_sets") or []:
+ try:
+ client.configure_delete(base_path + [rs["name"]])
+ commands.append(
+ "delete {p} {n}".format(
+ p=" ".join(base_path),
+ n=rs["name"],
+ ),
+ )
+ changed = True
+ except VyOSRestError:
+ pass
+
+ elif state in ("merged", "replaced", "overridden"):
+ if state == "replaced":
+ for entry in config:
+ afi = entry["afi"]
+ base_path = _FW_PATH[afi]
+ for rs in entry.get("rule_sets") or []:
+ try:
+ client.configure_delete(base_path + [rs["name"]])
+ commands.append(
+ "delete {p} {n}".format(
+ p=" ".join(base_path),
+ n=rs["name"],
+ ),
+ )
+ except VyOSRestError:
+ pass
+ for entry in config:
+ afi = entry["afi"]
+ for rs in entry.get("rule_sets") or []:
+ _apply_rule_set(client, afi, rs, commands)
+ changed = True
+ except VyOSRestError as exc:
+ module.fail_json(msg=str(exc))
+
+ after = _get_fw_rules(client) if changed else before
+ module.exit_json(changed=changed, before=before, after=after, commands=commands)
+
+
+if __name__ == "__main__":
+ main()