summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoromnom62 <75066712+omnom62@users.noreply.github.com>2026-09-01 22:37:23 +1000
committerGitHub <noreply@github.com>2026-09-01 15:37:23 +0300
commit497022e13c372ed68afe49aeafca66285e2f8001 (patch)
treebb01d4dbe45e34a465244e2f89778012ee65a6cf
parent61d27bc11d3f64ad4ad66ec42ff5ae83ccd30732 (diff)
downloadvyos.vyos-497022e13c372ed68afe49aeafca66285e2f8001.tar.gz
vyos.vyos-497022e13c372ed68afe49aeafca66285e2f8001.zip
T8323: vyos_nat module (#451)
Add NAT module
-rw-r--r--README.md1
-rw-r--r--changelogs/fragments/t8323-vyos_nat.yml3
-rw-r--r--docs/vyos.vyos.vyos_nat_module.rst3723
-rw-r--r--meta/runtime.yml2
l---------plugins/action/nat.py1
-rw-r--r--plugins/module_utils/network/vyos/argspec/nat/__init__.py0
-rw-r--r--plugins/module_utils/network/vyos/argspec/nat/nat.py652
-rw-r--r--plugins/module_utils/network/vyos/config/nat/__init__.py0
-rw-r--r--plugins/module_utils/network/vyos/config/nat/nat.py577
-rw-r--r--plugins/module_utils/network/vyos/facts/facts.py4
-rw-r--r--plugins/module_utils/network/vyos/facts/nat/__init__.py0
-rw-r--r--plugins/module_utils/network/vyos/facts/nat/nat.py174
-rw-r--r--plugins/module_utils/network/vyos/rm_templates/nat.py1186
-rw-r--r--plugins/module_utils/network/vyos/utils/utils.py1
-rw-r--r--plugins/modules/vyos_nat.py934
-rw-r--r--plugins/modules/vyos_vrf.py4
-rw-r--r--tests/integration/targets/vyos_nat/defaults/main.yaml3
-rw-r--r--tests/integration/targets/vyos_nat/tasks/cli.yaml20
-rw-r--r--tests/integration/targets/vyos_nat/tasks/main.yaml5
-rw-r--r--tests/integration/targets/vyos_nat/tests/cli/_parsed.cfg17
-rw-r--r--tests/integration/targets/vyos_nat/tests/cli/_populate.yaml8
-rw-r--r--tests/integration/targets/vyos_nat/tests/cli/_remove_config.yaml10
-rw-r--r--tests/integration/targets/vyos_nat/tests/cli/deleted.yaml34
-rw-r--r--tests/integration/targets/vyos_nat/tests/cli/deleted_full_match.yaml70
-rw-r--r--tests/integration/targets/vyos_nat/tests/cli/empty_config.yaml60
-rw-r--r--tests/integration/targets/vyos_nat/tests/cli/gathered.yaml22
-rw-r--r--tests/integration/targets/vyos_nat/tests/cli/merged.yaml110
-rw-r--r--tests/integration/targets/vyos_nat/tests/cli/merged_load_balance.yaml107
-rw-r--r--tests/integration/targets/vyos_nat/tests/cli/overridden.yaml45
-rw-r--r--tests/integration/targets/vyos_nat/tests/cli/parsed.yaml16
-rw-r--r--tests/integration/targets/vyos_nat/tests/cli/rendered.yaml32
-rw-r--r--tests/integration/targets/vyos_nat/tests/cli/replaced.yaml45
-rw-r--r--tests/integration/targets/vyos_nat/tests/cli/replaced_cgnat_pool.yaml77
-rw-r--r--tests/integration/targets/vyos_nat/tests/cli/rtt.yaml70
-rw-r--r--tests/integration/targets/vyos_nat/vars/main.yaml172
-rw-r--r--tests/sanity/ignore-2.15.txt1
-rw-r--r--tests/sanity/ignore-2.16.txt1
-rw-r--r--tests/sanity/ignore-2.17.txt1
-rw-r--r--tests/sanity/ignore-2.18.txt1
-rw-r--r--tests/sanity/ignore-2.19.txt1
-rw-r--r--tests/sanity/ignore-2.20.txt1
-rw-r--r--tests/sanity/ignore-2.21.txt1
-rw-r--r--tests/sanity/ignore-2.22.txt1
-rw-r--r--tests/unit/modules/network/vyos/fixtures/vyos_nat_config.cfg61
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_nat.py839
45 files changed, 9090 insertions, 3 deletions
diff --git a/README.md b/README.md
index bf6a7701..5e7fbc82 100644
--- a/README.md
+++ b/README.md
@@ -60,6 +60,7 @@ Name | Description
[vyos.vyos.vyos_lldp_global](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_lldp_global_module.rst)|LLDP global resource module
[vyos.vyos.vyos_lldp_interfaces](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_lldp_interfaces_module.rst)|LLDP interfaces resource module
[vyos.vyos.vyos_logging_global](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_logging_global_module.rst)|Logging resource module
+[vyos.vyos.vyos_nat](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_nat_module.rst)|NAT resource module
[vyos.vyos.vyos_ntp_global](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_ntp_global_module.rst)|NTP global resource module
[vyos.vyos.vyos_ospf_interfaces](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_ospf_interfaces_module.rst)|OSPF Interfaces Resource Module.
[vyos.vyos.vyos_ospfv2](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_ospfv2_module.rst)|OSPFv2 resource module
diff --git a/changelogs/fragments/t8323-vyos_nat.yml b/changelogs/fragments/t8323-vyos_nat.yml
new file mode 100644
index 00000000..ae71d4a9
--- /dev/null
+++ b/changelogs/fragments/t8323-vyos_nat.yml
@@ -0,0 +1,3 @@
+---
+minor_changes:
+ - vyos_nat - Add new module to support NAT configuration.
diff --git a/docs/vyos.vyos.vyos_nat_module.rst b/docs/vyos.vyos.vyos_nat_module.rst
new file mode 100644
index 00000000..7a8045ee
--- /dev/null
+++ b/docs/vyos.vyos.vyos_nat_module.rst
@@ -0,0 +1,3723 @@
+.. _vyos.vyos.vyos_nat_module:
+
+
+******************
+vyos.vyos.vyos_nat
+******************
+
+**NAT resource module**
+
+
+Version added: 6.0.0
+
+.. contents::
+ :local:
+ :depth: 1
+
+
+Synopsis
+--------
+- This module manages NAT configuration on devices running VyOS.
+
+
+
+
+Parameters
+----------
+
+.. raw:: html
+
+ <table border=0 cellpadding=0 class="documentation-table">
+ <tr>
+ <th colspan="7">Parameter</th>
+ <th>Choices/<font color="blue">Defaults</font></th>
+ <th width="100%">Comments</th>
+ </tr>
+ <tr>
+ <td colspan="7">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>config</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>The desired configuration for the NAT resource represented as a dictionary.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td colspan="6">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>nat</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Configuration for NAT rules.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="5">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>cgnat</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Configuration for Carrier Grade NAT (CGNAT).</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="4">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>log_allocation</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">boolean</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>no</li>
+ <li>yes</li>
+ </ul>
+ </td>
+ <td>
+ <div>Log CGNAT address allocations.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="4">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>pool</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Configuration for CGNAT pools.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>external</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>List of external NAT pools for CGNAT.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>external_port_range</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Port range to use for NAT translations in this external pool.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>name</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ / <span style="color: red">required</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Name of the external NAT pool.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>per_user_limit</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Per-user limit configuration for the external pool.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>port</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Maximum number of ports allocated per user.</div>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>range</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>List of external IP address ranges in the pool.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>seq</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Optional sequence number for this range entry.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>value</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ / <span style="color: red">required</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>IP address, prefix, or range (e.g. 203.0.113.0/24 or 203.0.113.1-203.0.113.60).</div>
+ </td>
+ </tr>
+
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>internal</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>List of internal NAT pools for CGNAT.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>name</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ / <span style="color: red">required</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Name of the internal NAT pool.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>range</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>List of internal IP addresses or prefixes in the pool.</div>
+ </td>
+ </tr>
+
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="4">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>rule</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>List of CGNAT rules.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>id</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">integer</span>
+ / <span style="color: red">required</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Rule number for CGNAT.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>source</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Source pool configuration for CGNAT translation.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>pool</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Source pool name to use for CGNAT translation.</div>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>translation</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Translation pool configuration for CGNAT.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>pool</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Translation pool name to use for CGNAT translation.</div>
+ </td>
+ </tr>
+
+
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="5">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>destination</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Configuration for destination NAT rules.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="4">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>rule</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>List of destination NAT rules.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>description</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>User-friendly description of the destination NAT rule.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>destination</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Match criteria for destination NAT.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>address</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>IP address, subnet, or range to match.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>address_group</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Address group name to match.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>domain_group</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Domain group name to match.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>fqdn</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Fully qualified domain name to match.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>mac_group</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>MAC address group name to match.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>network_group</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Network group name to match.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>port</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Port number or range to match.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>port_group</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Port group name to match.</div>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>disable</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">boolean</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>no</li>
+ <li>yes</li>
+ </ul>
+ </td>
+ <td>
+ <div>Disable this destination NAT rule.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>exclude</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">boolean</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>no</li>
+ <li>yes</li>
+ </ul>
+ </td>
+ <td>
+ <div>Exclude packets matching this rule from NAT.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>id</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">integer</span>
+ / <span style="color: red">required</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Rule number for destination NAT.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>inbound_interface</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Match inbound interface.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>group</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Interface group to match.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>name</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Interface name to match.</div>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>load_balance</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Load balancing configuration for this NAT rule.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>backend</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>List of backends to load-balance across. Weights should sum to 100.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>ip</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>IP address of the backend translation target.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>weight</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">integer</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Relative weight (1-100) for this backend&#x27;s share of load-balanced traffic.</div>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>hash</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=string</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>source-address</li>
+ <li>destination-address</li>
+ <li>source-port</li>
+ <li>destination-port</li>
+ <li>random</li>
+ </ul>
+ </td>
+ <td>
+ <div>Fields to hash on for load balancing. Mutually exclusive with <em>translation.address</em>.</div>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>log</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">boolean</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>no</li>
+ <li>yes</li>
+ </ul>
+ </td>
+ <td>
+ <div>Log packets hitting this rule.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>packet_type</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Packet type to match.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>protocol</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Protocol to NAT (default all).</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>translation</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Translation configuration for destination NAT.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>address</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>IP address or prefix to translate destination to.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>address_mapping</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>random</li>
+ <li>persistent</li>
+ </ul>
+ </td>
+ <td>
+ <div>Address mapping mode for translation.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>port</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Port number or range to translate destination port to.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>port_mapping</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>random</li>
+ <li>none</li>
+ </ul>
+ </td>
+ <td>
+ <div>Port mapping mode for translation.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>redirect_port</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Redirect to local port number.</div>
+ </td>
+ </tr>
+
+
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="5">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>source</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Configuration for source NAT rules.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="4">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>rule</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>List of source NAT rules.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>description</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>User-friendly description of the source NAT rule.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>destination</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Destination match criteria for source NAT.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>address</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>IP address, subnet, or range to match.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>address_group</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Address group name to match.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>domain_group</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Domain group name to match.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>fqdn</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Fully qualified domain name to match.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>mac_group</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>MAC address group name to match.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>network_group</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Network group name to match.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>port</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Port number or range to match.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>port_group</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Port group name to match.</div>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>disable</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">boolean</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>no</li>
+ <li>yes</li>
+ </ul>
+ </td>
+ <td>
+ <div>Disable this source NAT rule.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>exclude</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">boolean</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>no</li>
+ <li>yes</li>
+ </ul>
+ </td>
+ <td>
+ <div>Exclude packets matching this rule from NAT.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>id</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">integer</span>
+ / <span style="color: red">required</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Rule number for source NAT.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>load_balance</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Load balancing configuration for this NAT rule.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>backend</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>List of backends to load-balance across. Weights should sum to 100.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>ip</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>IP address of the backend translation target.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>weight</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">integer</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Relative weight (1-100) for this backend&#x27;s share of load-balanced traffic.</div>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>hash</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=string</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>source-address</li>
+ <li>destination-address</li>
+ <li>source-port</li>
+ <li>destination-port</li>
+ <li>random</li>
+ </ul>
+ </td>
+ <td>
+ <div>Fields to hash on for load balancing. Mutually exclusive with <em>translation.address</em>.</div>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>log</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">boolean</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>no</li>
+ <li>yes</li>
+ </ul>
+ </td>
+ <td>
+ <div>Log packets hitting this rule.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>outbound_interface</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Match outbound interface.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>group</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Interface group to match.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>name</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Interface name to match.</div>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>packet_type</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Packet type to match.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>protocol</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Protocol to NAT (default all).</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>source</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Source match criteria for source NAT.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>address</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>IP address, subnet, or range to match.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>address_group</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Address group name to match.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>domain_group</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Domain group name to match.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>fqdn</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Fully qualified domain name to match.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>mac_group</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>MAC address group name to match.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>network_group</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Network group name to match.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>port</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Port number or range to match.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>port_group</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Port group name to match.</div>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>translation</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Translation configuration for source NAT.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>address</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>IP address or prefix to translate source to. Use masquerade to masquerade as the outbound interface address.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>address_mapping</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>random</li>
+ <li>persistent</li>
+ </ul>
+ </td>
+ <td>
+ <div>Address mapping mode for translation.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>port</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Port number or range to translate source port to.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>port_mapping</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>random</li>
+ <li>none</li>
+ </ul>
+ </td>
+ <td>
+ <div>Port mapping mode for translation.</div>
+ </td>
+ </tr>
+
+
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="5">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>static</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Configuration for static one-to-one NAT rules.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="4">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>rule</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>List of static NAT rules.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>description</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>User-friendly description of the static NAT rule.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>destination</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Match criteria for static NAT.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>address</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>IP address, subnet, or range to match.</div>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>id</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">integer</span>
+ / <span style="color: red">required</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Rule number for static NAT.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>inbound_interface</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Inbound interface that this static NAT rule applies to.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>log</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">boolean</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>no</li>
+ <li>yes</li>
+ </ul>
+ </td>
+ <td>
+ <div>Log packets hitting this static NAT rule.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>translation</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Translation configuration for static NAT.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>address</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>IP address or prefix to translate to.</div>
+ </td>
+ </tr>
+
+
+
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td colspan="6">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>nat64</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Configuration for NAT64 (IPv6-to-IPv4) rules.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="5">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>source</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Configuration for NAT64 source rules.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="4">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>rule</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>List of NAT64 source rules.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>description</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>User-friendly description of the NAT64 source rule.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>disable</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">boolean</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>no</li>
+ <li>yes</li>
+ </ul>
+ </td>
+ <td>
+ <div>Disable this NAT64 source rule.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>id</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">integer</span>
+ / <span style="color: red">required</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Rule number for NAT64 source rule (1-999999).</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>match</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Match criteria for NAT64 source rule.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>mark</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">integer</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Match on firewall mark value (1-2147483647).</div>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>source</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>IPv6 source prefix to match for NAT64 translation.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>prefix</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>IPv6 source prefix to match (h:h:h:h:h:h:h:h/x).</div>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>translation</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Translation configuration for NAT64 source rule.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>pool</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>List of translation pools for NAT64.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>address</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>IPv4 address or prefix for translation pool.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>description</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>User-friendly description of the translation pool.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>disable</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">boolean</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>no</li>
+ <li>yes</li>
+ </ul>
+ </td>
+ <td>
+ <div>Disable this translation pool.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>id</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">integer</span>
+ / <span style="color: red">required</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Pool number (1-999999).</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>port</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Port number or range for translation pool.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>protocol</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>icmp</li>
+ <li>tcp</li>
+ <li>udp</li>
+ </ul>
+ </td>
+ <td>
+ <div>Protocol for this translation pool entry.</div>
+ </td>
+ </tr>
+
+
+
+
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td colspan="6">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>nat66</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Configuration for NAT66 (IPv6-to-IPv6) rules.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="5">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>destination</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Configuration for NAT66 destination rules.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="4">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>rule</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>List of NAT66 destination rules.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>description</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>User-friendly description of the NAT66 destination rule.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>destination</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Match criteria for NAT66 destination rule.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>address</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>IPv6 address or prefix to match.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>port</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Port number or range to match.</div>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>disable</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">boolean</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>no</li>
+ <li>yes</li>
+ </ul>
+ </td>
+ <td>
+ <div>Disable this NAT66 destination rule.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>exclude</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">boolean</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>no</li>
+ <li>yes</li>
+ </ul>
+ </td>
+ <td>
+ <div>Exclude packets matching this rule from NAT66.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>id</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">integer</span>
+ / <span style="color: red">required</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Rule number for NAT66 destination rule.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>inbound_interface</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Inbound interface to match for NAT66 destination rule.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>name</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Interface name to match.</div>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>log</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">boolean</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>no</li>
+ <li>yes</li>
+ </ul>
+ </td>
+ <td>
+ <div>Log packets hitting this NAT66 destination rule.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>protocol</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Protocol to match.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>source</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Source match criteria for NAT66 destination rule.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>address</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>IPv6 source address or prefix to match.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>port</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Source port number or range to match.</div>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>translation</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Translation configuration for NAT66 destination rule.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>address</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>IPv6 address or prefix to translate destination to.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>port</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Port number or range to translate destination port to.</div>
+ </td>
+ </tr>
+
+
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="5">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>source</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Configuration for NAT66 source rules.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="4">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>rule</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>List of NAT66 source rules.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>description</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>User-friendly description of the NAT66 source rule.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>destination</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Destination match criteria for NAT66 source rule.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>port</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Destination port number or range to match.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>prefix</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>IPv6 destination prefix to match (h:h:h:h:h:h:h:h/x).</div>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>disable</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">boolean</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>no</li>
+ <li>yes</li>
+ </ul>
+ </td>
+ <td>
+ <div>Disable this NAT66 source rule.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>exclude</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">boolean</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>no</li>
+ <li>yes</li>
+ </ul>
+ </td>
+ <td>
+ <div>Exclude packets matching this rule from NAT66.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>id</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">integer</span>
+ / <span style="color: red">required</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Rule number for NAT66 source rule.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>log</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">boolean</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>no</li>
+ <li>yes</li>
+ </ul>
+ </td>
+ <td>
+ <div>Log packets hitting this NAT66 source rule.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>outbound_interface</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Outbound interface to match for NAT66 source rule.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>name</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Interface name to match.</div>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>protocol</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Protocol to match.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>source</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Source match criteria for NAT66 source rule.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>port</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Source port number or range to match.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>prefix</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>IPv6 source prefix to match (h:h:h:h:h:h:h:h/x).</div>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>translation</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Translation configuration for NAT66 source rule.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>address</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>IPv6 address or prefix to translate source to. Use masquerade to masquerade as the outbound interface address.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>port</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Port number or range to translate source port to.</div>
+ </td>
+ </tr>
+
+
+
+
+
+ <tr>
+ <td colspan="7">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>running_config</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>This option is used only with state <em>parsed</em>.</div>
+ <div>The value of this option should be the output received from the VyOS device by executing the command <b>show configuration commands | match &#x27;nat&#x27;</b>.</div>
+ <div>The state <em>parsed</em> reads the configuration from <code>show configuration commands | match &#x27;nat&#x27;</code> and transforms it into Ansible structured data as per the module argspec. The value is then returned in the <em>parsed</em> key within the result.</div>
+ <div>The state <em>replaced</em> replaces only the provided configuration, while <em>overridden</em> removes any existing NAT configuration not specified in <em>config</em>.</div>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="7">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>state</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>deleted</li>
+ <li><div style="color: blue"><b>merged</b>&nbsp;&larr;</div></li>
+ <li>overridden</li>
+ <li>replaced</li>
+ <li>gathered</li>
+ <li>rendered</li>
+ <li>parsed</li>
+ </ul>
+ </td>
+ <td>
+ <div>The state the configuration should be left in.</div>
+ </td>
+ </tr>
+ </table>
+ <br/>
+
+
+Notes
+-----
+
+.. note::
+ - Tested against VyOS 1.4.3 and 1.5.0.
+ - This module works with connection ``network_cli``.
+
+
+
+Examples
+--------
+
+.. code-block:: yaml
+
+ # Using merged - configure CGNAT
+ - name: Merge CGNAT configuration
+ vyos.vyos.vyos_nat:
+ config:
+ nat:
+ cgnat:
+ log_allocation: true
+ pool:
+ external:
+ - name: ext-pool-1
+ external_port_range: "10000-20000"
+ per_user_limit:
+ port: "200"
+ range:
+ - value: 203.0.113.0/24
+ internal:
+ - name: int-pool-1
+ range:
+ - 10.0.0.0/24
+ rule:
+ - id: 1
+ source:
+ pool: int-pool-1
+ translation:
+ pool: ext-pool-1
+ state: merged
+
+ # Using merged - configure destination NAT
+ - name: Merge destination NAT rule
+ vyos.vyos.vyos_nat:
+ config:
+ nat:
+ destination:
+ rule:
+ - id: 100
+ description: "Web server NAT"
+ protocol: tcp
+ log: true
+ destination:
+ address: 198.51.100.10
+ port: "80"
+ translation:
+ address: 192.168.1.10
+ port: "8080"
+ state: merged
+
+ # Using merged - configure source NAT
+ - name: Merge source NAT rule
+ vyos.vyos.vyos_nat:
+ config:
+ nat:
+ source:
+ rule:
+ - id: 200
+ description: "Outbound masquerade"
+ protocol: tcp
+ log: true
+ outbound_interface:
+ name: eth0
+ translation:
+ address: masquerade
+ state: merged
+
+ # Using merged - configure static NAT
+ - name: Merge static NAT rule
+ vyos.vyos.vyos_nat:
+ config:
+ nat:
+ static:
+ rule:
+ - id: 300
+ description: "Static mapping"
+ inbound_interface: eth2
+ destination:
+ address: 198.51.100.20
+ translation:
+ address: 192.168.1.20
+ log: true
+ state: merged
+
+ # Using merged - configure NAT64
+ - name: Merge NAT64 source rule
+ vyos.vyos.vyos_nat:
+ config:
+ nat64:
+ source:
+ rule:
+ - id: 10
+ description: "NAT64 example"
+ source:
+ prefix: 2001:db8::/96
+ match:
+ mark: 100
+ translation:
+ pool:
+ - id: 1
+ address: 192.168.100.10
+ port: "1-65535"
+ protocol: udp
+ state: merged
+
+ # Using merged - configure NAT66
+ - name: Merge NAT66 destination rule
+ vyos.vyos.vyos_nat:
+ config:
+ nat66:
+ destination:
+ rule:
+ - id: 20
+ description: "NAT66 DNAT"
+ protocol: tcp
+ inbound_interface:
+ name: eth1
+ destination:
+ address: 2001:db8::1
+ translation:
+ address: 2001:db8:1::10
+ port: "8443"
+ state: merged
+
+ # Using replaced - replace specific NAT rules
+ - name: Replace destination NAT rule
+ vyos.vyos.vyos_nat:
+ config:
+ nat:
+ destination:
+ rule:
+ - id: 100
+ description: "Replaced web server NAT"
+ protocol: tcp
+ destination:
+ address: 198.51.100.10
+ port: "443"
+ translation:
+ address: 192.168.1.10
+ port: "8443"
+ state: replaced
+
+ # Using overridden - override entire NAT configuration
+ - name: Override entire NAT configuration
+ vyos.vyos.vyos_nat:
+ config:
+ nat:
+ destination:
+ rule:
+ - id: 100
+ description: "Only rule after override"
+ protocol: tcp
+ destination:
+ address: 198.51.100.10
+ port: "80"
+ translation:
+ address: 192.168.1.10
+ port: "8080"
+ state: overridden
+
+ # Using deleted - delete all NAT configuration
+ - name: Delete all NAT configuration
+ vyos.vyos.vyos_nat:
+ state: deleted
+
+ # Using deleted - delete specific NAT rules
+ - name: Delete specific NAT rules
+ vyos.vyos.vyos_nat:
+ config:
+ nat:
+ destination:
+ rule:
+ - id: 100
+ source:
+ rule:
+ - id: 200
+ nat64:
+ source:
+ rule:
+ - id: 10
+ state: deleted
+
+ # Using gathered
+ - name: Gather NAT configuration from device
+ vyos.vyos.vyos_nat:
+ state: gathered
+
+ # Using rendered
+ - name: Render NAT configuration offline
+ vyos.vyos.vyos_nat:
+ config:
+ nat:
+ destination:
+ rule:
+ - id: 100
+ description: "Rendered rule"
+ protocol: tcp
+ destination:
+ address: 198.51.100.10
+ port: "80"
+ translation:
+ address: 192.168.1.10
+ port: "8080"
+ state: rendered
+
+ # Using parsed
+ - name: Parse NAT configuration from file
+ vyos.vyos.vyos_nat:
+ running_config: "{{ lookup('file', './nat_config.cfg') }}"
+ state: parsed
+
+
+
+Return Values
+-------------
+Common return values are documented `here <https://docs.ansible.com/ansible/latest/reference_appendices/common_return_values.html#common-return-values>`_, the following are the fields unique to this module:
+
+.. raw:: html
+
+ <table border=0 cellpadding=0 class="documentation-table">
+ <tr>
+ <th colspan="1">Key</th>
+ <th>Returned</th>
+ <th width="100%">Description</th>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="return-"></div>
+ <b>after</b>
+ <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>when changed</td>
+ <td>
+ <div>The resulting configuration after module execution.</div>
+ <br/>
+ <div style="font-size: smaller"><b>Sample:</b></div>
+ <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">This output will always be in the same format as the module argspec.</div>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="return-"></div>
+ <b>before</b>
+ <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>when <em>state</em> is <code>merged</code>, <code>replaced</code>, <code>overridden</code> or <code>deleted</code></td>
+ <td>
+ <div>The configuration prior to the module execution.</div>
+ <br/>
+ <div style="font-size: smaller"><b>Sample:</b></div>
+ <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">This output will always be in the same format as the module argspec.</div>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="return-"></div>
+ <b>commands</b>
+ <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ </div>
+ </td>
+ <td>when <em>state</em> is <code>merged</code>, <code>replaced</code>, <code>overridden</code> or <code>deleted</code></td>
+ <td>
+ <div>The set of commands pushed to the remote device.</div>
+ <br/>
+ <div style="font-size: smaller"><b>Sample:</b></div>
+ <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">[&quot;set nat destination rule 100 description &#x27;Web server NAT&#x27;&quot;, &#x27;set nat destination rule 100 protocol tcp&#x27;, &#x27;set nat destination rule 100 inbound-interface name eth2&#x27;, &#x27;set nat destination rule 100 destination address 198.51.100.10&#x27;, &#x27;set nat destination rule 100 translation address 192.168.1.10&#x27;, &#x27;delete nat source rule 200&#x27;]</div>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="return-"></div>
+ <b>gathered</b>
+ <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>when <em>state</em> is <code>gathered</code></td>
+ <td>
+ <div>Facts about the network resource gathered from the remote device as structured data.</div>
+ <br/>
+ <div style="font-size: smaller"><b>Sample:</b></div>
+ <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">This output will always be in the same format as the module argspec.</div>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="return-"></div>
+ <b>parsed</b>
+ <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>when <em>state</em> is <code>parsed</code></td>
+ <td>
+ <div>The device native config provided in <em>running_config</em> option parsed into structured data as per module argspec.</div>
+ <br/>
+ <div style="font-size: smaller"><b>Sample:</b></div>
+ <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">This output will always be in the same format as the module argspec.</div>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="return-"></div>
+ <b>rendered</b>
+ <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ </div>
+ </td>
+ <td>when <em>state</em> is <code>rendered</code></td>
+ <td>
+ <div>The provided configuration in the task rendered in device-native format (offline).</div>
+ <br/>
+ <div style="font-size: smaller"><b>Sample:</b></div>
+ <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">[&quot;set nat destination rule 100 description &#x27;Web server NAT&#x27;&quot;, &#x27;set nat destination rule 100 protocol tcp&#x27;, &#x27;set nat destination rule 100 inbound-interface name eth2&#x27;, &#x27;set nat destination rule 100 destination address 198.51.100.10&#x27;, &#x27;set nat destination rule 100 translation address 192.168.1.10&#x27;]</div>
+ </td>
+ </tr>
+ </table>
+ <br/><br/>
+
+
+Status
+------
+
+
+Authors
+~~~~~~~
+
+- Evgeny Molotkov (@omnom62)
diff --git a/meta/runtime.yml b/meta/runtime.yml
index c1065dbd..093e129d 100644
--- a/meta/runtime.yml
+++ b/meta/runtime.yml
@@ -46,6 +46,8 @@ plugin_routing:
redirect: vyos.vyos.vyos_logging_global
ntp_global:
redirect: vyos.vyos.vyos_ntp_global
+ nat:
+ redirect: vyos.vyos.vyos_nat
ospfv2:
redirect: vyos.vyos.vyos_ospfv2
ospfv3:
diff --git a/plugins/action/nat.py b/plugins/action/nat.py
new file mode 120000
index 00000000..331a791f
--- /dev/null
+++ b/plugins/action/nat.py
@@ -0,0 +1 @@
+vyos.py \ No newline at end of file
diff --git a/plugins/module_utils/network/vyos/argspec/nat/__init__.py b/plugins/module_utils/network/vyos/argspec/nat/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/plugins/module_utils/network/vyos/argspec/nat/__init__.py
diff --git a/plugins/module_utils/network/vyos/argspec/nat/nat.py b/plugins/module_utils/network/vyos/argspec/nat/nat.py
new file mode 100644
index 00000000..6f81c53c
--- /dev/null
+++ b/plugins/module_utils/network/vyos/argspec/nat/nat.py
@@ -0,0 +1,652 @@
+# -*- 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
+
+"""
+The arg spec for the vyos_nat module
+"""
+
+
+class NatArgs(object): # pylint: disable=R0903
+ """The arg spec for the vyos_nat module"""
+
+ argument_spec = {
+ "config": {
+ "type": "dict",
+ "options": {
+ "nat": {
+ "type": "dict",
+ "options": {
+ "cgnat": {
+ "type": "dict",
+ "options": {
+ "log_allocation": {
+ "type": "bool",
+ },
+ "pool": {
+ "type": "dict",
+ "options": {
+ "external": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "name": {
+ "type": "str",
+ "required": True,
+ },
+ "external_port_range": {
+ "type": "str",
+ },
+ "per_user_limit": {
+ "type": "dict",
+ "options": {
+ "port": {
+ "type": "str",
+ },
+ },
+ },
+ "range": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "value": {
+ "type": "str",
+ "required": True,
+ },
+ "seq": {
+ "type": "str",
+ },
+ },
+ },
+ },
+ },
+ "internal": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "name": {
+ "type": "str",
+ "required": True,
+ },
+ "range": {
+ "type": "list",
+ "elements": "str",
+ },
+ },
+ },
+ },
+ },
+ "rule": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "id": {
+ "type": "int",
+ "required": True,
+ },
+ "source": {
+ "type": "dict",
+ "options": {
+ "pool": {
+ "type": "str",
+ },
+ },
+ },
+ "translation": {
+ "type": "dict",
+ "options": {
+ "pool": {
+ "type": "str",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ "destination": {
+ "type": "dict",
+ "options": {
+ "rule": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "id": {
+ "type": "int",
+ "required": True,
+ },
+ "description": {
+ "type": "str",
+ },
+ "protocol": {
+ "type": "str",
+ },
+ "packet_type": {
+ "type": "str",
+ },
+ "exclude": {
+ "type": "bool",
+ },
+ "log": {
+ "type": "bool",
+ },
+ "disable": {
+ "type": "bool",
+ },
+ "inbound_interface": {
+ "type": "dict",
+ "options": {
+ "name": {
+ "type": "str",
+ },
+ "group": {
+ "type": "str",
+ },
+ },
+ },
+ "destination": {
+ "type": "dict",
+ "options": {
+ "address": {
+ "type": "str",
+ },
+ "fqdn": {
+ "type": "str",
+ },
+ "port": {
+ "type": "str",
+ },
+ "address_group": {
+ "type": "str",
+ },
+ "domain_group": {
+ "type": "str",
+ },
+ "mac_group": {
+ "type": "str",
+ },
+ "network_group": {
+ "type": "str",
+ },
+ "port_group": {
+ "type": "str",
+ },
+ },
+ },
+ "translation": {
+ "type": "dict",
+ "options": {
+ "address": {
+ "type": "str",
+ },
+ "port": {
+ "type": "str",
+ },
+ "redirect_port": {
+ "type": "str",
+ },
+ "address_mapping": {
+ "type": "str",
+ "choices": [
+ "random",
+ "persistent",
+ ],
+ },
+ "port_mapping": {
+ "type": "str",
+ "choices": [
+ "random",
+ "none",
+ ],
+ },
+ },
+ },
+ "load_balance": {
+ "type": "dict",
+ "options": {
+ "backend": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "ip": {"type": "str"},
+ "weight": {"type": "int"},
+ },
+ },
+ "hash": {
+ "type": "list",
+ "elements": "str",
+ "choices": [
+ "source-address",
+ "destination-address",
+ "source-port",
+ "destination-port",
+ "random",
+ ],
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ "source": {
+ "type": "dict",
+ "options": {
+ "rule": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "id": {
+ "type": "int",
+ "required": True,
+ },
+ "description": {
+ "type": "str",
+ },
+ "protocol": {
+ "type": "str",
+ },
+ "packet_type": {
+ "type": "str",
+ },
+ "exclude": {
+ "type": "bool",
+ },
+ "log": {
+ "type": "bool",
+ },
+ "disable": {
+ "type": "bool",
+ },
+ "outbound_interface": {
+ "type": "dict",
+ "options": {
+ "name": {
+ "type": "str",
+ },
+ "group": {
+ "type": "str",
+ },
+ },
+ },
+ "destination": {
+ "type": "dict",
+ "options": {
+ "address": {
+ "type": "str",
+ },
+ "fqdn": {
+ "type": "str",
+ },
+ "address_group": {
+ "type": "str",
+ },
+ "domain_group": {
+ "type": "str",
+ },
+ "mac_group": {
+ "type": "str",
+ },
+ "network_group": {
+ "type": "str",
+ },
+ "port_group": {
+ "type": "str",
+ },
+ "port": {
+ "type": "str",
+ },
+ },
+ },
+ "source": {
+ "type": "dict",
+ "options": {
+ "address": {"type": "str"},
+ "fqdn": {"type": "str"},
+ "port": {"type": "str"},
+ "address_group": {"type": "str"},
+ "domain_group": {"type": "str"},
+ "mac_group": {"type": "str"},
+ "network_group": {"type": "str"},
+ "port_group": {"type": "str"},
+ },
+ },
+ "translation": {
+ "type": "dict",
+ "options": {
+ "address": {
+ "type": "str",
+ },
+ "port": {
+ "type": "str",
+ },
+ "address_mapping": {
+ "type": "str",
+ "choices": [
+ "random",
+ "persistent",
+ ],
+ },
+ "port_mapping": {
+ "type": "str",
+ "choices": [
+ "random",
+ "none",
+ ],
+ },
+ },
+ },
+ "load_balance": {
+ "type": "dict",
+ "options": {
+ "backend": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "ip": {"type": "str"},
+ "weight": {"type": "int"},
+ },
+ },
+ "hash": {
+ "type": "list",
+ "elements": "str",
+ "choices": [
+ "source-address",
+ "destination-address",
+ "source-port",
+ "destination-port",
+ "random",
+ ],
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ "static": {
+ "type": "dict",
+ "options": {
+ "rule": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "id": {
+ "type": "int",
+ "required": True,
+ },
+ "description": {
+ "type": "str",
+ },
+ "destination": {
+ "type": "dict",
+ "options": {
+ "address": {
+ "type": "str",
+ },
+ },
+ },
+ "inbound_interface": {
+ "type": "str",
+ },
+ "log": {
+ "type": "bool",
+ },
+ "translation": {
+ "type": "dict",
+ "options": {
+ "address": {
+ "type": "str",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ "nat64": {
+ "type": "dict",
+ "options": {
+ "source": {
+ "type": "dict",
+ "options": {
+ "rule": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "id": {
+ "type": "int",
+ "required": True,
+ },
+ "description": {
+ "type": "str",
+ },
+ "disable": {
+ "type": "bool",
+ },
+ "match": {
+ "type": "dict",
+ "options": {
+ "mark": {
+ "type": "int",
+ },
+ },
+ },
+ "source": {
+ "type": "dict",
+ "options": {
+ "prefix": {
+ "type": "str",
+ },
+ },
+ },
+ "translation": {
+ "type": "dict",
+ "options": {
+ "pool": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "id": {
+ "type": "int",
+ "required": True,
+ },
+ "address": {
+ "type": "str",
+ },
+ "description": {
+ "type": "str",
+ },
+ "disable": {
+ "type": "bool",
+ },
+ "port": {
+ "type": "str",
+ },
+ "protocol": {
+ "type": "str",
+ "choices": [
+ "icmp",
+ "tcp",
+ "udp",
+ ],
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ "nat66": {
+ "type": "dict",
+ "options": {
+ "destination": {
+ "type": "dict",
+ "options": {
+ "rule": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "id": {
+ "type": "int",
+ "required": True,
+ },
+ "description": {
+ "type": "str",
+ },
+ "destination": {
+ "type": "dict",
+ "options": {
+ "address": {
+ "type": "str",
+ },
+ "port": {
+ "type": "str",
+ },
+ },
+ },
+ "disable": {
+ "type": "bool",
+ },
+ "exclude": {
+ "type": "bool",
+ },
+ "inbound_interface": {
+ "type": "dict",
+ "options": {
+ "name": {
+ "type": "str",
+ },
+ },
+ },
+ "log": {
+ "type": "bool",
+ },
+ "protocol": {
+ "type": "str",
+ },
+ "source": {
+ "type": "dict",
+ "options": {
+ "address": {
+ "type": "str",
+ },
+ "port": {
+ "type": "str",
+ },
+ },
+ },
+ "translation": {
+ "type": "dict",
+ "options": {
+ "address": {
+ "type": "str",
+ },
+ "port": {
+ "type": "str",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ "source": {
+ "type": "dict",
+ "options": {
+ "rule": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "id": {
+ "type": "int",
+ "required": True,
+ },
+ "description": {
+ "type": "str",
+ },
+ "destination": {
+ "type": "dict",
+ "options": {
+ "port": {
+ "type": "str",
+ },
+ "prefix": {
+ "type": "str",
+ },
+ },
+ },
+ "disable": {
+ "type": "bool",
+ },
+ "exclude": {
+ "type": "bool",
+ },
+ "log": {
+ "type": "bool",
+ },
+ "outbound_interface": {
+ "type": "dict",
+ "options": {
+ "name": {
+ "type": "str",
+ },
+ },
+ },
+ "protocol": {
+ "type": "str",
+ },
+ "source": {
+ "type": "dict",
+ "options": {
+ "port": {
+ "type": "str",
+ },
+ "prefix": {
+ "type": "str",
+ },
+ },
+ },
+ "translation": {
+ "type": "dict",
+ "options": {
+ "address": {
+ "type": "str",
+ },
+ "port": {
+ "type": "str",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ "running_config": {"type": "str"},
+ "state": {
+ "type": "str",
+ "choices": [
+ "deleted",
+ "merged",
+ "overridden",
+ "replaced",
+ "gathered",
+ "rendered",
+ "parsed",
+ ],
+ "default": "merged",
+ },
+ } # pylint: disable=C0301
diff --git a/plugins/module_utils/network/vyos/config/nat/__init__.py b/plugins/module_utils/network/vyos/config/nat/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/plugins/module_utils/network/vyos/config/nat/__init__.py
diff --git a/plugins/module_utils/network/vyos/config/nat/nat.py b/plugins/module_utils/network/vyos/config/nat/nat.py
new file mode 100644
index 00000000..a2800914
--- /dev/null
+++ b/plugins/module_utils/network/vyos/config/nat/nat.py
@@ -0,0 +1,577 @@
+# -*- 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
+
+from copy import deepcopy
+
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import (
+ ResourceModule,
+)
+
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts import Facts
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.nat import (
+ NatTemplate,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.utils import combine
+
+
+class Nat(ResourceModule):
+ """The vyos_nat config class"""
+
+ def __init__(self, module):
+ super(Nat, self).__init__(
+ empty_fact_val={},
+ facts_module=Facts(module),
+ module=module,
+ resource="nat",
+ tmplt=NatTemplate(),
+ )
+ self.parsers = []
+
+ def execute_module(self):
+ if self.state not in ["parsed", "gathered"]:
+ self.generate_commands()
+ self.run_commands()
+ return self.result
+
+ def generate_commands(self):
+ wantd = deepcopy(self.want)
+ haved = deepcopy(self.have)
+
+ if self.state == "merged":
+ wantd = combine(haved, wantd, recursive=True)
+
+ if self.state == "deleted":
+ if not wantd:
+ for nat_type in haved:
+ self.commands.append(f"delete {nat_type}")
+ return
+ self._list_to_dict(wantd)
+ self._list_to_dict(haved)
+ self._delete_nat_objects(wantd, haved, only_missing=False)
+ return
+
+ self._list_to_dict(wantd)
+ self._list_to_dict(haved)
+
+ if self.state == "replaced":
+ self._delete_nat_objects(wantd, haved, only_missing=False)
+ self._set_commands(wantd, haved)
+ elif self.state == "overridden":
+ self._delete_nat_objects(wantd, haved, only_missing=True)
+ self._delete_nat_objects(wantd, haved, only_missing=False)
+ self._set_commands(wantd, haved)
+ else:
+ self._set_commands(wantd, haved)
+
+ self.commands = list(dict.fromkeys(self.commands))
+
+ # -------------------------------------------------------------------------
+ # List → keyed dict conversion
+ # -------------------------------------------------------------------------
+
+ def _list_to_dict(self, config):
+ nat = config.get("nat", {})
+ cgnat = nat.get("cgnat", {})
+
+ pool = cgnat.get("pool", {})
+ for ptype in ("external", "internal"):
+ entries = pool.get(ptype)
+ if isinstance(entries, list):
+ pool[ptype] = {item["name"]: item for item in entries}
+
+ rules = cgnat.get("rule")
+ if isinstance(rules, list):
+ cgnat["rule"] = {r["id"]: r for r in rules}
+
+ for section in ("destination", "source", "static"):
+ rules = nat.get(section, {}).get("rule")
+ if isinstance(rules, list):
+ nat[section]["rule"] = {r["id"]: r for r in rules}
+
+ nat64 = config.get("nat64", {})
+ rules = nat64.get("source", {}).get("rule")
+ if isinstance(rules, list):
+ nat64["source"]["rule"] = {r["id"]: r for r in rules}
+ for rule in nat64["source"]["rule"].values():
+ pools = rule.get("translation", {}).get("pool")
+ if isinstance(pools, list):
+ rule["translation"]["pool"] = {p["id"]: p for p in pools}
+
+ nat66 = config.get("nat66", {})
+ for section in ("destination", "source"):
+ rules = nat66.get(section, {}).get("rule")
+ if isinstance(rules, list):
+ nat66[section]["rule"] = {r["id"]: r for r in rules}
+
+ # -------------------------------------------------------------------------
+ # Top-level dispatch
+ # -------------------------------------------------------------------------
+
+ def _set_commands(self, wantd, haved):
+ self._compare_cgnat_global(wantd, haved)
+ self._compare_cgnat_pools(wantd, haved)
+ self._compare_cgnat_rules(wantd, haved)
+
+ for section in ("destination", "source", "static"):
+ self._compare_nat_rules("nat", section, wantd, haved)
+
+ self._compare_nat_rules("nat64", "source", wantd, haved)
+
+ for section in ("destination", "source"):
+ self._compare_nat_rules("nat66", section, wantd, haved)
+
+ self.commands = list(dict.fromkeys(self.commands))
+
+ # -------------------------------------------------------------------------
+ # Delete helpers
+ # -------------------------------------------------------------------------
+
+ def _delete_nat_objects(self, wantd, haved, only_missing=False):
+ """
+ Generate delete commands for NAT objects.
+ only_missing=False: delete objects present in both want and have (when different)
+ only_missing=True: delete objects present in have but absent from want
+ """
+ for nat_type in haved:
+ want_nat = wantd.get(nat_type, {})
+ have_nat = haved[nat_type]
+
+ if only_missing and nat_type not in wantd:
+ self.commands.append(f"delete {nat_type}")
+ continue
+
+ for section in have_nat:
+ want_section = want_nat.get(section, {})
+ have_section = have_nat[section]
+
+ if only_missing and section not in want_nat:
+ self.commands.append(
+ f"delete {nat_type} {section.replace('_', '-')}",
+ )
+ continue
+
+ if section == "cgnat":
+ for pool_type in ("external", "internal"):
+ want_pools = want_section.get("pool", {}).get(pool_type, {})
+ have_pools = have_section.get("pool", {}).get(pool_type, {})
+ for name in have_pools:
+ if only_missing and name not in want_pools:
+ self.commands.append(
+ f"delete {nat_type} cgnat pool {pool_type} {name}",
+ )
+
+ elif not only_missing and name in want_pools:
+ if self.state == "deleted" or want_pools[name] != have_pools[name]:
+ self.commands.append(
+ f"delete {nat_type} cgnat pool {pool_type} {name}",
+ )
+ want_rules = want_section.get("rule", {})
+ have_rules = have_section.get("rule", {})
+ for rid in have_rules:
+ if only_missing and rid not in want_rules:
+ self.commands.append(f"delete {nat_type} cgnat rule {rid}")
+
+ elif not only_missing and rid in want_rules:
+ if self.state == "deleted" or want_rules[rid] != have_rules[rid]:
+ self.commands.append(f"delete {nat_type} cgnat rule {rid}")
+ else:
+ want_rules = want_section.get("rule", {})
+ have_rules = have_section.get("rule", {})
+ cli_section = section.replace("_", "-")
+ for rid in have_rules:
+ if only_missing and rid not in want_rules:
+ self.commands.append(
+ f"delete {nat_type} {cli_section} rule {rid}",
+ )
+
+ elif not only_missing and rid in want_rules:
+ if self.state == "deleted" or want_rules[rid] != have_rules[rid]:
+ self.commands.append(
+ f"delete {nat_type} {cli_section} rule {rid}",
+ )
+
+ # -------------------------------------------------------------------------
+ # CGNAT
+ # -------------------------------------------------------------------------
+
+ def _compare_cgnat_global(self, wantd, haved):
+ if self.state in ("replaced", "overridden") and not wantd.get("nat", {}).get("cgnat"):
+ return
+ w = wantd.get("nat", {}).get("cgnat", {}).get("log_allocation")
+ h = haved.get("nat", {}).get("cgnat", {}).get("log_allocation")
+ if bool(w) != bool(h):
+ self.addcmd(
+ {"nat": {"cgnat": {"log_allocation": True}}},
+ "cgnat_log_allocation",
+ not bool(w),
+ )
+
+ def _compare_cgnat_pools(self, wantd, haved):
+ want_ext = wantd.get("nat", {}).get("cgnat", {}).get("pool", {}).get("external", {})
+ have_ext = haved.get("nat", {}).get("cgnat", {}).get("pool", {}).get("external", {})
+ want_int = wantd.get("nat", {}).get("cgnat", {}).get("pool", {}).get("internal", {})
+ have_int = haved.get("nat", {}).get("cgnat", {}).get("pool", {}).get("internal", {})
+
+ scope = self.state in ("replaced", "overridden")
+ ext_names = set(want_ext) if scope else set(want_ext) | set(have_ext)
+ int_names = set(want_int) if scope else set(want_int) | set(have_int)
+
+ for name in ext_names:
+ w = want_ext.get(name, {})
+ h = have_ext.get(name, {})
+ if scope and w != h:
+ h = {}
+ self._compare_external_pool(name, w, h)
+
+ for name in int_names:
+ w = want_int.get(name, {})
+ h = have_int.get(name, {})
+ if scope and w != h:
+ h = {}
+ self._compare_internal_pool(name, w, h)
+
+ def _compare_external_pool(self, name, want, have):
+ w = want.get("external_port_range")
+ h = have.get("external_port_range")
+ if w != h:
+ if w:
+ self.addcmd({"name": name, "range": w}, "cgnat_pool_external_port_range", False)
+ elif self.state in ("replaced", "overridden"):
+ self.addcmd({"name": name, "range": h}, "cgnat_pool_external_port_range", True)
+
+ w = want.get("per_user_limit", {}).get("port")
+ h = have.get("per_user_limit", {}).get("port")
+ if w != h:
+ if w:
+ self.addcmd({"name": name, "limit": w}, "cgnat_pool_external_per_user", False)
+ elif self.state in ("replaced", "overridden"):
+ self.addcmd({"name": name, "limit": h}, "cgnat_pool_external_per_user", True)
+
+ want_ranges = {(r["value"] if isinstance(r, dict) else r): r for r in want.get("range", [])}
+ have_ranges = {(r["value"] if isinstance(r, dict) else r): r for r in have.get("range", [])}
+ for val, rng in want_ranges.items():
+ existing = have_ranges.get(val)
+ if existing is None or existing != rng:
+ seq = rng.get("seq") if isinstance(rng, dict) else None
+ self.addcmd(
+ {"name": name, "range": val, "seq": seq},
+ "cgnat_pool_external_range",
+ False,
+ )
+
+ if self.state in ("replaced", "overridden"):
+ for val in have_ranges:
+ if val not in want_ranges:
+ self.addcmd({"name": name, "range": val}, "cgnat_pool_external_range", True)
+
+ def _compare_internal_pool(self, name, want, have):
+ want_ranges = set(want.get("range", []))
+ have_ranges = set(have.get("range", []))
+
+ for rng in want_ranges - have_ranges:
+ self.addcmd({"name": name, "range": rng}, "cgnat_pool_internal_range", False)
+
+ if self.state in ("replaced", "overridden"):
+ for rng in have_ranges - want_ranges:
+ self.addcmd({"name": name, "range": rng}, "cgnat_pool_internal_range", True)
+
+ def _compare_cgnat_rules(self, wantd, haved):
+ want_rules = wantd.get("nat", {}).get("cgnat", {}).get("rule", {})
+ have_rules = haved.get("nat", {}).get("cgnat", {}).get("rule", {})
+
+ rids = (
+ set(want_rules)
+ if self.state in ("replaced", "overridden")
+ else set(want_rules) | set(have_rules)
+ )
+
+ for rid in rids:
+ w = want_rules.get(rid, {})
+ h = have_rules.get(rid, {})
+
+ if self.state in ("replaced", "overridden") and w != h:
+ h = {}
+
+ w_src = w.get("source", {}).get("pool")
+ h_src = h.get("source", {}).get("pool")
+ if w_src != h_src:
+ if w_src:
+ self.addcmd({"id": rid, "pool": w_src}, "cgnat_rule_source_pool", False)
+ elif self.state in ("replaced", "overridden"):
+ self.addcmd({"id": rid, "pool": h_src}, "cgnat_rule_source_pool", True)
+
+ w_tr = w.get("translation", {}).get("pool")
+ h_tr = h.get("translation", {}).get("pool")
+ if w_tr != h_tr:
+ if w_tr:
+ self.addcmd({"id": rid, "pool": w_tr}, "cgnat_rule_translation_pool", False)
+ elif self.state in ("replaced", "overridden"):
+ self.addcmd({"id": rid, "pool": h_tr}, "cgnat_rule_translation_pool", True)
+
+ # -------------------------------------------------------------------------
+ # NAT / NAT64 / NAT66 rules
+ # -------------------------------------------------------------------------
+
+ def _compare_nat_rules(self, nat_type, section, wantd, haved):
+ want_rules = wantd.get(nat_type, {}).get(section, {}).get("rule", {})
+ have_rules = haved.get(nat_type, {}).get(section, {}).get("rule", {})
+
+ rids = (
+ set(want_rules)
+ if self.state in ("replaced", "overridden")
+ else set(want_rules) | set(have_rules)
+ )
+
+ for rid in rids:
+ w = want_rules.get(rid, {})
+ h = have_rules.get(rid, {})
+ if self.state in ("replaced", "overridden") and w != h:
+ h = {}
+ if w == h and self.state != "rendered":
+ continue
+ self._compare_rule(nat_type, section, rid, w, h)
+
+ def _compare_rule(self, nat_type, section, rid, want, have):
+ ctx = {"nat": nat_type, "type": section, "id": rid}
+
+ want_lb = want.get("load_balance") or {}
+ have_lb = have.get("load_balance") or {}
+ want_trans_addr = (want.get("translation") or {}).get("address")
+ have_trans_addr = (have.get("translation") or {}).get("address")
+ if want_lb and want_trans_addr is not None:
+ self._module.fail_json(
+ msg="translation.address and load_balance are mutually exclusive",
+ )
+ if self.state == "merged":
+ if want_lb and have_trans_addr is not None:
+ self._module.fail_json(
+ msg=(
+ "Cannot add load_balance to a rule that already has translation.address with "
+ "state=merged; use state=replaced or state=overridden"
+ ),
+ )
+ if want_trans_addr is not None and have_lb:
+ self._module.fail_json(
+ msg=(
+ "Cannot add translation.address to a rule that already has load_balance with "
+ "state=merged; use state=replaced or state=overridden"
+ ),
+ )
+
+ for field in set(want) | set(have):
+ if field == "inbound_interface":
+ continue
+ val = want.get(field) if field in want else have.get(field)
+ if isinstance(val, bool):
+ self._cmp_bool(want, have, field, ctx, f"nat_type_{field}")
+ elif isinstance(val, str):
+ self._cmp_scalar(want, have, field, ctx, f"nat_type_{field}")
+
+ self._cmp_interface(want, have, ctx, nat_type, section)
+ self._cmp_outbound_interface(want, have, ctx)
+ for atype in ("destination", "source"):
+ self._cmp_addr_sub(want, have, atype, ctx)
+ self._cmp_translation(want, have, ctx)
+ self._cmp_match_mark(want, have, ctx)
+ self._cmp_nat64_pools(want, have, ctx)
+ self._cmp_load_balance(want, have, ctx)
+
+ # -------------------------------------------------------------------------
+ # Field-level helpers
+ # -------------------------------------------------------------------------
+
+ def _cmp_scalar(self, want, have, field, ctx, parser):
+ w = want.get(field)
+ h = have.get(field)
+ if w != h:
+ if w is not None:
+ self.addcmd(dict(ctx, **{field: w}), parser, False)
+ elif self.state in ("replaced", "overridden"):
+ self.addcmd(dict(ctx, **{field: h}), parser, True)
+
+ def _cmp_bool(self, want, have, field, ctx, parser):
+ w = bool(want.get(field))
+ h = bool(have.get(field))
+ if w != h:
+ if w:
+ self.addcmd(dict(ctx), parser, False)
+ elif self.state in ("replaced", "overridden"):
+ self.addcmd(dict(ctx), parser, True)
+
+ def _cmp_interface(self, want, have, ctx, nat_type, section):
+ iface_w = want.get("inbound_interface")
+ iface_h = have.get("inbound_interface")
+ if iface_w == iface_h:
+ return
+
+ if nat_type == "nat" and section == "static":
+ if iface_w:
+ self.addcmd(dict(ctx, value=iface_w), "nat_static_inbound_interface", False)
+ elif self.state in ("replaced", "overridden"):
+ self.addcmd(dict(ctx, value=iface_h), "nat_static_inbound_interface", True)
+ return
+
+ iface_w = iface_w or {}
+ iface_h = iface_h or {}
+
+ if nat_type == "nat":
+ parser_name = "nat_inbound_interface_name"
+ parser_group = "nat_inbound_interface_group"
+ else:
+ parser_name = "nat6x_inbound_interface"
+ parser_group = "nat6x_inbound_interface"
+
+ if iface_w.get("name") != iface_h.get("name"):
+ if iface_w.get("name"):
+ self.addcmd(dict(ctx, value=iface_w["name"]), parser_name, False)
+ elif self.state in ("replaced", "overridden"):
+ self.addcmd(dict(ctx, value=iface_h["name"]), parser_name, True)
+
+ if nat_type == "nat" and iface_w.get("group") != iface_h.get("group"):
+ if iface_w.get("group"):
+ self.addcmd(dict(ctx, value=iface_w["group"]), parser_group, False)
+ elif self.state in ("replaced", "overridden"):
+ self.addcmd(dict(ctx, value=iface_h["group"]), parser_group, True)
+
+ def _cmp_outbound_interface(self, want, have, ctx):
+ iface_w = want.get("outbound_interface") or {}
+ iface_h = have.get("outbound_interface") or {}
+
+ if iface_w.get("name") != iface_h.get("name"):
+ if iface_w.get("name"):
+ self.addcmd(dict(ctx, value=iface_w["name"]), "nat_type_outbound_interface", False)
+ elif self.state in ("replaced", "overridden"):
+ self.addcmd(dict(ctx, value=iface_h["name"]), "nat_type_outbound_interface", True)
+
+ if iface_w.get("group") != iface_h.get("group"):
+ if iface_w.get("group"):
+ self.addcmd(
+ dict(ctx, value=iface_w["group"]),
+ "nat_type_outbound_interface_group",
+ False,
+ )
+ elif self.state in ("replaced", "overridden"):
+ self.addcmd(
+ dict(ctx, value=iface_h["group"]),
+ "nat_type_outbound_interface_group",
+ True,
+ )
+
+ def _cmp_addr_sub(self, want, have, atype, ctx):
+ sub_w = want.get(atype) or {}
+ sub_h = have.get(atype) or {}
+ if sub_w == sub_h:
+ return
+
+ changed = {k: v for k, v in sub_w.items() if sub_h.get(k) != v}
+ removed = {
+ k: v
+ for k, v in sub_h.items()
+ if k not in sub_w and self.state in ("replaced", "overridden")
+ }
+
+ if changed:
+ self.addcmd(dict(ctx, atype=atype, sub=changed), "nat_type_address", False)
+ if removed:
+ self.addcmd(dict(ctx, atype=atype, sub=removed), "nat_type_address", True)
+
+ def _cmp_translation(self, want, have, ctx):
+ trans_w = want.get("translation") or {}
+ trans_h = have.get("translation") or {}
+ if trans_w == trans_h:
+ return
+
+ changed = {k: v for k, v in trans_w.items() if k != "pool" and trans_h.get(k) != v}
+ removed = {
+ k: v
+ for k, v in trans_h.items()
+ if k != "pool" and k not in trans_w and self.state in ("replaced", "overridden")
+ }
+
+ if changed:
+ self.addcmd(dict(ctx, translation=changed), "nat_type_translation_address", False)
+ if removed:
+ self.addcmd(dict(ctx, translation=removed), "nat_type_translation_address", True)
+
+ def _cmp_match_mark(self, want, have, ctx):
+ w = want.get("match", {}).get("mark")
+ h = have.get("match", {}).get("mark")
+ if w != h:
+ if w is not None:
+ self.addcmd(dict(ctx, mark=w), "nat64_match_mark", False)
+ elif self.state in ("replaced", "overridden"):
+ self.addcmd(dict(ctx, mark=h), "nat64_match_mark", True)
+
+ def _cmp_nat64_pools(self, want, have, ctx):
+ want_pools = want.get("translation", {}).get("pool", {})
+ have_pools = have.get("translation", {}).get("pool", {})
+
+ if isinstance(want_pools, list):
+ want_pools = {p["id"]: p for p in want_pools}
+ if isinstance(have_pools, list):
+ have_pools = {p["id"]: p for p in have_pools}
+
+ for pid in set(want_pools) | set(have_pools):
+ wp = want_pools.get(pid, {})
+ hp = have_pools.get(pid, {})
+
+ if wp == hp:
+ continue
+
+ changed = {k: v for k, v in wp.items() if k != "id" and hp.get(k) != v}
+ removed = {
+ k: v
+ for k, v in hp.items()
+ if k != "id" and k not in wp and self.state in ("replaced", "overridden")
+ }
+
+ if changed:
+ self.addcmd(
+ dict(ctx, pool_id=pid, pool=changed),
+ "nat64_translation_pool",
+ False,
+ )
+ if removed:
+ self.addcmd(
+ dict(ctx, pool_id=pid, pool=removed),
+ "nat64_translation_pool",
+ True,
+ )
+
+ def _cmp_load_balance(self, want, have, ctx):
+ lb_w = want.get("load_balance") or {}
+ lb_h = have.get("load_balance") or {}
+
+ want_hash = set(lb_w.get("hash") or [])
+ have_hash = set(lb_h.get("hash") or [])
+
+ for h in want_hash - have_hash:
+ self.addcmd(dict(ctx, value=h), "nat_type_lb_hash", False)
+ if self.state in ("replaced", "overridden"):
+ for h in have_hash - want_hash:
+ self.addcmd(dict(ctx, value=h), "nat_type_lb_hash", True)
+
+ want_backends = lb_w.get("backend", [])
+ have_backends = lb_h.get("backend", [])
+ if isinstance(want_backends, list):
+ want_backends = {b["ip"]: b for b in want_backends}
+ if isinstance(have_backends, list):
+ have_backends = {b["ip"]: b for b in have_backends}
+
+ for ip in set(want_backends) | set(have_backends):
+ wb = want_backends.get(ip, {})
+ hb = have_backends.get(ip, {})
+ if wb == hb:
+ continue
+ if wb:
+ weight = wb.get("weight")
+ if weight is None:
+ self._module.fail_json(msg="load_balance.backend entries require 'weight'")
+ self.addcmd(dict(ctx, ip=ip, weight=weight), "nat_type_lb_backend", False)
+ elif self.state in ("replaced", "overridden"):
+ self.addcmd(dict(ctx, ip=ip, weight=hb.get("weight")), "nat_type_lb_backend", True)
diff --git a/plugins/module_utils/network/vyos/facts/facts.py b/plugins/module_utils/network/vyos/facts/facts.py
index 69cb0d0b..92c0f97a 100644
--- a/plugins/module_utils/network/vyos/facts/facts.py
+++ b/plugins/module_utils/network/vyos/facts/facts.py
@@ -59,6 +59,9 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.lldp_
from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.logging_global.logging_global import (
Logging_globalFacts,
)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.nat.nat import (
+ NatFacts,
+)
from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.ntp_global.ntp_global import (
Ntp_globalFacts,
)
@@ -112,6 +115,7 @@ FACT_RESOURCE_SUBSETS = dict(
prefix_lists=Prefix_listsFacts,
logging_global=Logging_globalFacts,
ntp_global=Ntp_globalFacts,
+ nat=NatFacts,
snmp_server=Snmp_serverFacts,
hostname=HostnameFacts,
vrf=VrfFacts,
diff --git a/plugins/module_utils/network/vyos/facts/nat/__init__.py b/plugins/module_utils/network/vyos/facts/nat/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/plugins/module_utils/network/vyos/facts/nat/__init__.py
diff --git a/plugins/module_utils/network/vyos/facts/nat/nat.py b/plugins/module_utils/network/vyos/facts/nat/nat.py
new file mode 100644
index 00000000..73d6238f
--- /dev/null
+++ b/plugins/module_utils/network/vyos/facts/nat/nat.py
@@ -0,0 +1,174 @@
+# -*- 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
+
+import re
+
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import utils
+
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.nat.nat import (
+ NatArgs,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.nat import (
+ NatTemplate,
+)
+
+
+class NatFacts(object):
+ """The vyos nat facts class"""
+
+ def __init__(self, module, subspec="config", options="options"):
+ self._module = module
+ self.argument_spec = NatArgs.argument_spec
+
+ def get_config(self, connection):
+ return connection.get("show configuration commands | match 'set nat'")
+
+ def populate_facts(self, connection, ansible_facts, data=None):
+ facts = {}
+ config_lines = []
+
+ if not data:
+ data = self.get_config(connection)
+
+ for resource in data.splitlines():
+ config_lines.append(re.sub(r"'([^']*)'", r"\1", resource))
+
+ nat_parser = NatTemplate(lines=config_lines, module=self._module)
+ objs = nat_parser.parse()
+ objs = self._normalise(objs)
+
+ ansible_facts["ansible_network_resources"].pop("nat", None)
+
+ params = utils.remove_empties(
+ nat_parser.validate_config(self.argument_spec, {"config": objs}, redact=True),
+ )
+
+ if params.get("config"):
+ facts["nat"] = params["config"]
+ ansible_facts["ansible_network_resources"].update(facts)
+
+ return ansible_facts
+
+ def _deep_merge(self, base, override):
+ for k, v in override.items():
+ if k in base and isinstance(base[k], dict) and isinstance(v, dict):
+ self._deep_merge(base[k], v)
+ elif k in base and isinstance(base[k], list) and isinstance(v, list):
+ for entry in v:
+ if entry not in base[k]:
+ base[k].append(entry)
+ else:
+ base[k] = v
+ return base
+
+ def _merge_rule_list(self, rules):
+ merged = {}
+ for item in rules:
+ rid = item["id"]
+ if rid not in merged:
+ merged[rid] = {"id": rid}
+ for k, v in item.items():
+ if k == "id":
+ continue
+ if isinstance(v, list):
+ existing = merged[rid].setdefault(k, [])
+ for entry in v:
+ if entry not in existing:
+ existing.append(entry)
+ elif isinstance(v, dict):
+ merged[rid].setdefault(k, {})
+ self._deep_merge(merged[rid][k], v)
+ else:
+ merged[rid][k] = v
+ return list(merged.values())
+
+ def _merge_pool_list(self, pools):
+ merged = {}
+ for item in pools:
+ name = item["name"]
+ if name not in merged:
+ merged[name] = {"name": name}
+ for k, v in item.items():
+ if k == "name":
+ continue
+ if k == "range" and isinstance(v, list):
+ existing = merged[name].setdefault(k, [])
+ existing.extend(v)
+ if v and isinstance(v[0], dict):
+ merged[name][k] = self._merge_range_list(existing)
+ else:
+ merged[name][k] = list(dict.fromkeys(existing))
+ elif isinstance(v, list):
+ merged[name].setdefault(k, [])
+ for val in v:
+ if val not in merged[name][k]:
+ merged[name][k].append(val)
+ elif isinstance(v, dict):
+ merged[name].setdefault(k, {})
+ self._deep_merge(merged[name][k], v)
+ else:
+ merged[name][k] = v
+ return list(merged.values())
+
+ def _merge_range_list(self, ranges):
+ """Merge external pool range entries by value, preserving seq."""
+ merged = {}
+ for entry in ranges:
+ if isinstance(entry, dict):
+ key = entry.get("value") or entry.get("address", "")
+ if not key:
+ continue
+ if key not in merged:
+ merged[key] = {"value": key}
+ if entry.get("seq"):
+ merged[key]["seq"] = entry["seq"]
+ else:
+ if entry not in merged:
+ merged[entry] = {"value": entry}
+ return list(merged.values())
+
+ def _normalise(self, objs):
+ for nat_type in ["nat", "nat64", "nat66"]:
+ nat = objs.get(nat_type)
+ if not nat:
+ continue
+
+ for section in ["destination", "source", "static", "cgnat"]:
+ if section not in nat:
+ continue
+ rules = nat[section].get("rule")
+ if isinstance(rules, list):
+ nat[section]["rule"] = self._merge_rule_list(rules)
+ nat[section]["rule"].sort(key=lambda x: x.get("id", 0))
+
+ if "cgnat" in nat and "pool" in nat["cgnat"]:
+ pool = nat["cgnat"]["pool"]
+ for ptype in ["external", "internal"]:
+ if ptype in pool and isinstance(pool[ptype], list):
+ pool[ptype] = self._merge_pool_list(pool[ptype])
+
+ if nat_type == "nat64":
+ for rule in nat.get("source", {}).get("rule", []):
+ pools = rule.get("translation", {}).get("pool")
+ if pools and isinstance(pools, list):
+ rule["translation"]["pool"] = self._merge_rule_list(pools)
+ rule["translation"]["pool"].sort(key=lambda x: x.get("id", 0))
+
+ self._cast_ports(objs)
+ return objs
+
+ def _cast_ports(self, obj):
+ """Recursively cast known integer port/seq fields to str."""
+ if isinstance(obj, dict):
+ for k, v in obj.items():
+ if k in ("port", "seq") and isinstance(v, int):
+ obj[k] = str(v)
+ else:
+ self._cast_ports(v)
+ elif isinstance(obj, list):
+ for item in obj:
+ self._cast_ports(item)
diff --git a/plugins/module_utils/network/vyos/rm_templates/nat.py b/plugins/module_utils/network/vyos/rm_templates/nat.py
new file mode 100644
index 00000000..94af8696
--- /dev/null
+++ b/plugins/module_utils/network/vyos/rm_templates/nat.py
@@ -0,0 +1,1186 @@
+# -*- 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
+
+import re
+
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import (
+ NetworkTemplate,
+)
+
+
+def _tmplt_nat_rule_addr_sub(config_data):
+ """Generate address/fqdn/prefix/port/group commands for destination or source sub-dict."""
+ nat = config_data["nat"]
+ type_ = config_data["type"]
+ rid = config_data["id"]
+ atype = config_data["atype"]
+ sub = config_data["sub"]
+
+ base = f"{nat} {type_} rule {rid} {atype}"
+ commands = []
+
+ for field in ("address", "fqdn", "prefix", "port"):
+ if sub.get(field) is not None:
+ commands.append(f"{base} {field} {sub[field]}")
+
+ for gtype in ("address_group", "domain_group", "mac_group", "network_group", "port_group"):
+ if sub.get(gtype) is not None:
+ commands.append(f"{base} group {gtype.replace('_', '-')} {sub[gtype]}")
+
+ return commands
+
+
+def _tmplt_nat_rule_translation(config_data):
+ """Generate translation commands."""
+ nat = config_data["nat"]
+ type_ = config_data["type"]
+ rid = config_data["id"]
+ trans = config_data["translation"]
+
+ base = f"{nat} {type_} rule {rid} translation"
+ commands = []
+
+ if trans.get("address") is not None:
+ commands.append(f"{base} address {trans['address']}")
+
+ if trans.get("port") is not None:
+ commands.append(f"{base} port {trans['port']}")
+
+ if trans.get("redirect_port") is not None:
+ commands.append(f"{base} redirect port {trans['redirect_port']}")
+
+ if trans.get("address_mapping") is not None:
+ commands.append(f"{base} options address-mapping {trans['address_mapping']}")
+
+ if trans.get("port_mapping") is not None:
+ commands.append(f"{base} options port-mapping {trans['port_mapping']}")
+
+ return commands
+
+
+def _tmplt_nat64_translation_pool(config_data):
+ """Generate all nat64 translation pool commands from a single call."""
+ nat = config_data["nat"]
+ type_ = config_data["type"]
+ rid = config_data["id"]
+ pool_id = config_data["pool_id"]
+ pool = config_data["pool"]
+
+ base = f"{nat} {type_} rule {rid} translation pool {pool_id}"
+ commands = []
+
+ if pool.get("address") is not None:
+ commands.append(f"{base} address {pool['address']}")
+ if pool.get("description") is not None:
+ commands.append(f"{base} description '{pool['description']}'")
+ if pool.get("port") is not None:
+ commands.append(f"{base} port {pool['port']}")
+ if pool.get("protocol") is not None:
+ commands.append(f"{base} protocol {pool['protocol']}")
+ if pool.get("disable"):
+ commands.append(f"{base} disable")
+
+ return commands
+
+
+class NatTemplate(NetworkTemplate):
+ def __init__(self, lines=None, module=None):
+ prefix = {"set": "set", "remove": "delete"}
+ super(NatTemplate, self).__init__(lines=lines, tmplt=self, prefix=prefix, module=module)
+
+ # fmt: off
+ PARSERS = [
+
+ # -------------------------
+ # CGNAT
+ # -------------------------
+ {
+ "name": "cgnat_log_allocation",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+nat
+ \s+cgnat
+ \s+log-allocation
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "nat cgnat log-allocation",
+ "result": {
+ "nat": {
+ "cgnat": {
+ "log_allocation": True,
+ },
+ },
+ },
+ },
+ {
+ "name": "cgnat_pool_external_range",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+nat
+ \s+cgnat
+ \s+pool
+ \s+external
+ \s+(?P<name>\S+)
+ \s+range
+ \s+(?P<range>\S+)(?:\s+seq\s+(?P<seq>\d+))?
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "nat cgnat pool external {{ name }} range {{ range }}{% if seq is defined and seq %} seq {{ seq }}{% endif %}",
+ "result": {
+ "nat": {
+ "cgnat": {
+ "pool": {
+ "external": [
+ {
+ "name": "{{ name }}",
+ "range": [
+ {
+ "value": "{{ range }}",
+ "seq": "{{ seq }}",
+ },
+ ],
+ },
+ ],
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "cgnat_pool_external_port_range",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+nat
+ \s+cgnat
+ \s+pool
+ \s+external
+ \s+(?P<name>\S+)
+ \s+external-port-range
+ \s+(?P<range>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "nat cgnat pool external {{ name }} external-port-range {{ range }}",
+ "result": {
+ "nat": {
+ "cgnat": {
+ "pool": {
+ "external": [
+ {
+ "name": "{{ name }}",
+ "external_port_range": "{{ range }}",
+ },
+ ],
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "cgnat_pool_external_per_user",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+nat
+ \s+cgnat
+ \s+pool
+ \s+external
+ \s+(?P<name>\S+)
+ \s+per-user-limit
+ \s+port
+ \s+(?P<limit>\d+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "nat cgnat pool external {{ name }} per-user-limit port {{ limit }}",
+ "result": {
+ "nat": {
+ "cgnat": {
+ "pool": {
+ "external": [
+ {
+ "name": "{{ name }}",
+ "per_user_limit": {"port": "{{ limit }}"},
+ },
+ ],
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "cgnat_pool_internal_range",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+nat
+ \s+cgnat
+ \s+pool
+ \s+internal
+ \s+(?P<name>\S+)
+ \s+range
+ \s+(?P<range>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "nat cgnat pool internal {{ name }} range {{ range }}",
+ "result": {
+ "nat": {
+ "cgnat": {
+ "pool": {
+ "internal": [
+ {
+ "name": "{{ name }}",
+ "range": ["{{ range }}"],
+ },
+ ],
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "cgnat_rule_source_pool",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+nat
+ \s+cgnat
+ \s+rule
+ \s+(?P<id>\d+)
+ \s+source
+ \s+pool
+ \s+(?P<pool>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "nat cgnat rule {{ id }} source pool {{ pool }}",
+ "result": {
+ "nat": {
+ "cgnat": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "source": {"pool": "{{ pool }}"},
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "cgnat_rule_translation_pool",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+nat
+ \s+cgnat
+ \s+rule
+ \s+(?P<id>\d+)
+ \s+translation
+ \s+pool
+ \s+(?P<pool>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "nat cgnat rule {{ id }} translation pool {{ pool }}",
+ "result": {
+ "nat": {
+ "cgnat": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "translation": {"pool": "{{ pool }}"},
+ },
+ ],
+ },
+ },
+ },
+ },
+
+ # -------------------------
+ # GENERIC NAT
+ # -------------------------
+ {
+ "name": "nat_type_description",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source|static)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+description
+ \s+(?P<description>.+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "{{ nat }} {{ type }} rule {{ id }} description '{{ description }}'",
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "description": "{{ description }}",
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_protocol",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source|static)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+protocol
+ \s+(?P<protocol>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "{{ nat }} {{ type }} rule {{ id }} protocol {{ protocol }}",
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "protocol": "{{ protocol }}",
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_disable",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source|static)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+disable
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "{{ nat }} {{ type }} rule {{ id }} disable",
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "disable": True,
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_exclude",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source|static)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+exclude
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "{{ nat }} {{ type }} rule {{ id }} exclude",
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "exclude": True,
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_log",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source|static)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+log
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "{{ nat }} {{ type }} rule {{ id }} log",
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "log": True,
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_address",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source|static)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+(?P<atype>destination|source)
+ \s+address
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_nat_rule_addr_sub,
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "{{ atype }}": {"address": "{{ value }}"},
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_prefix",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source|static)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+(?P<atype>destination|source)
+ \s+prefix
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_nat_rule_addr_sub,
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "{{ atype }}": {"prefix": "{{ value }}"},
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_fqdn",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+(?P<atype>destination|source)
+ \s+fqdn
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_nat_rule_addr_sub,
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "{{ atype }}": {"fqdn": "{{ value }}"},
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_port",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+(?P<atype>destination|source)
+ \s+port
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_nat_rule_addr_sub,
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "{{ atype }}": {"port": "{{ value }}"},
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_address_group",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+(?P<atype>destination|source)
+ \s+group
+ \s+(?P<gtype>address-group|domain-group|mac-group|network-group|port-group)
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_nat_rule_addr_sub,
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "{{ atype }}": {
+ "{{ gtype | replace('-', '_') }}": "{{ value }}",
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_translation_address",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source|static)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+translation
+ \s+address
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_nat_rule_translation,
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "translation": {"address": "{{ value }}"},
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_translation_port",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+translation
+ \s+port
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_nat_rule_translation,
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "translation": {"port": "{{ value }}"},
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_translation_options",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+translation
+ \s+options
+ \s+(?P<opt>address-mapping|port-mapping)
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_nat_rule_translation,
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "translation": {
+ "{{ opt | replace('-', '_') }}": "{{ value }}",
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_translation_redirect",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+translation
+ \s+redirect
+ \s+port
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_nat_rule_translation,
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "translation": {
+ "redirect_port": "{{ value }}",
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_inbound_interface_name",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+nat
+ \s+(?P<type>destination|source)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+inbound-interface
+ \s+name
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "nat {{ type }} rule {{ id }} inbound-interface name {{ value }}",
+ "result": {
+ "nat": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "inbound_interface": {"name": "{{ value }}"},
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_inbound_interface_group",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+nat
+ \s+(?P<type>destination|source)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+inbound-interface
+ \s+group
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "nat {{ type }} rule {{ id }} inbound-interface group {{ value }}",
+ "result": {
+ "nat": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "inbound_interface": {"group": "{{ value }}"},
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_static_inbound_interface",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+nat
+ \s+static
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+inbound-interface
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "nat static rule {{ id }} inbound-interface {{ value }}",
+ "result": {
+ "nat": {
+ "static": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "inbound_interface": "{{ value }}",
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat6x_inbound_interface",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat64|nat66)
+ \s+(?P<type>destination|source|static)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+inbound-interface
+ \s+name
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "{{ nat }} {{ type }} rule {{ id }} inbound-interface name {{ value }}",
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "inbound_interface": {"name": "{{ value }}"},
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_outbound_interface",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source|static)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+outbound-interface
+ \s+name
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "{{ nat }} {{ type }} rule {{ id }} outbound-interface name {{ value }}",
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "outbound_interface": {"name": "{{ value }}"},
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_outbound_interface_group",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source|static)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+outbound-interface
+ \s+group
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "{{ nat }} {{ type }} rule {{ id }} outbound-interface group {{ value }}",
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "outbound_interface": {"group": "{{ value }}"},
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_packet_type",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+packet-type
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "{{ nat }} {{ type }} rule {{ id }} packet-type {{ value }}",
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "packet_type": "{{ value }}",
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_lb_backend",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+load-balance
+ \s+backend
+ \s+(?P<ip>\S+)
+ \s+weight
+ \s+(?P<weight>\d+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "{{ nat }} {{ type }} rule {{ id }} load-balance backend {{ ip }} weight {{ weight }}",
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "load_balance": {
+ "backend": [
+ {"ip": "{{ ip }}", "weight": "{{ weight }}"},
+ ],
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_lb_hash",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+load-balance
+ \s+hash
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "{{ nat }} {{ type }} rule {{ id }} load-balance hash {{ value }}",
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "load_balance": {"hash": ["{{ value }}"]},
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat64_match_mark",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+nat64
+ \s+source
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+match
+ \s+mark
+ \s+(?P<mark>\d+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "nat64 source rule {{ id }} match mark {{ mark }}",
+ "result": {
+ "nat64": {
+ "source": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "match": {"mark": "{{ mark }}"},
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat64_translation_pool",
+ "getval": re.compile(r"^$"), # never matches — setval only
+ "setval": _tmplt_nat64_translation_pool,
+ "result": {},
+ },
+ {
+ "name": "nat64_translation_pool_address",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+nat64
+ \s+source
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+translation
+ \s+pool
+ \s+(?P<pool_id>\d+)
+ \s+address
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "nat64 source rule {{ id }} translation pool {{ pool_id }} address {{ value }}",
+ "result": {
+ "nat64": {
+ "source": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "translation": {
+ "pool": [{"id": "{{ pool_id }}", "address": "{{ value }}"}],
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+
+ {
+ "name": "nat64_translation_pool_description",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+nat64
+ \s+source
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+translation
+ \s+pool
+ \s+(?P<pool_id>\d+)
+ \s+description
+ \s+(?P<value>.+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "nat64 source rule {{ id }} translation pool {{ pool_id }} description '{{ value }}'",
+ "result": {
+ "nat64": {
+ "source": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "translation": {
+ "pool": [{"id": "{{ pool_id }}", "description": "{{ value }}"}],
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat64_translation_pool_disable",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+nat64
+ \s+source
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+translation
+ \s+pool
+ \s+(?P<pool_id>\d+)
+ \s+disable
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "nat64 source rule {{ id }} translation pool {{ pool_id }} disable",
+ "result": {
+ "nat64": {
+ "source": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "translation": {
+ "pool": [{"id": "{{ pool_id }}", "disable": True}],
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat64_translation_pool_port",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+nat64
+ \s+source
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+translation
+ \s+pool
+ \s+(?P<pool_id>\d+)
+ \s+port
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "nat64 source rule {{ id }} translation pool {{ pool_id }} port {{ value }}",
+ "result": {
+ "nat64": {
+ "source": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "translation": {
+ "pool": [{"id": "{{ pool_id }}", "port": "{{ value }}"}],
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat64_translation_pool_protocol",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+nat64
+ \s+source
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+translation
+ \s+pool
+ \s+(?P<pool_id>\d+)
+ \s+protocol
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "nat64 source rule {{ id }} translation pool {{ pool_id }} protocol {{ value }}",
+ "result": {
+ "nat64": {
+ "source": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "translation": {
+ "pool": [{"id": "{{ pool_id }}", "protocol": "{{ value }}"}],
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ ]
+ # fmt: on
diff --git a/plugins/module_utils/network/vyos/utils/utils.py b/plugins/module_utils/network/vyos/utils/utils.py
index e95e8dea..89f12773 100644
--- a/plugins/module_utils/network/vyos/utils/utils.py
+++ b/plugins/module_utils/network/vyos/utils/utils.py
@@ -310,6 +310,7 @@ def combine(a, b, recursive=False, list_merge="replace"):
:raises ValueError: If either argument is not a dict, or if an
unsupported ``list_merge`` mode is given.
"""
+
if not isinstance(a, dict) or not isinstance(b, dict):
raise ValueError("combine expects two dictionaries")
diff --git a/plugins/modules/vyos_nat.py b/plugins/modules/vyos_nat.py
new file mode 100644
index 00000000..7715cb76
--- /dev/null
+++ b/plugins/modules/vyos_nat.py
@@ -0,0 +1,934 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+"""
+The module file for vyos_nat
+"""
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+DOCUMENTATION = """
+module: vyos_nat
+version_added: 6.0.0
+short_description: NAT resource module
+description:
+- This module manages NAT configuration on devices running VyOS.
+author:
+- Evgeny Molotkov (@omnom62)
+notes:
+- Tested against VyOS 1.4.3 and 1.5.0.
+- This module works with connection C(network_cli).
+options:
+ config:
+ description:
+ - The desired configuration for the NAT resource represented as a dictionary.
+ type: dict
+ suboptions:
+ nat:
+ type: dict
+ description: Configuration for NAT rules.
+ suboptions:
+ cgnat:
+ type: dict
+ description: Configuration for Carrier Grade NAT (CGNAT).
+ suboptions:
+ log_allocation:
+ type: bool
+ description: Log CGNAT address allocations.
+ pool:
+ type: dict
+ description: Configuration for CGNAT pools.
+ suboptions:
+ external:
+ type: list
+ elements: dict
+ description: List of external NAT pools for CGNAT.
+ suboptions:
+ name:
+ type: str
+ required: true
+ description: Name of the external NAT pool.
+ external_port_range:
+ type: str
+ description: Port range to use for NAT translations in this external pool.
+ per_user_limit:
+ type: dict
+ description: Per-user limit configuration for the external pool.
+ suboptions:
+ port:
+ type: str
+ description: Maximum number of ports allocated per user.
+ range:
+ type: list
+ elements: dict
+ description: List of external IP address ranges in the pool.
+ suboptions:
+ value:
+ type: str
+ required: true
+ description: IP address, prefix, or range (e.g. 203.0.113.0/24 or 203.0.113.1-203.0.113.60).
+ seq:
+ type: str
+ description: Optional sequence number for this range entry.
+ internal:
+ type: list
+ elements: dict
+ description: List of internal NAT pools for CGNAT.
+ suboptions:
+ name:
+ type: str
+ required: true
+ description: Name of the internal NAT pool.
+ range:
+ type: list
+ elements: str
+ description: List of internal IP addresses or prefixes in the pool.
+ rule:
+ type: list
+ elements: dict
+ description: List of CGNAT rules.
+ suboptions:
+ id:
+ type: int
+ required: true
+ description: Rule number for CGNAT.
+ source:
+ type: dict
+ description: Source pool configuration for CGNAT translation.
+ suboptions:
+ pool:
+ type: str
+ description: Source pool name to use for CGNAT translation.
+ translation:
+ type: dict
+ description: Translation pool configuration for CGNAT.
+ suboptions:
+ pool:
+ type: str
+ description: Translation pool name to use for CGNAT translation.
+ destination:
+ type: dict
+ description: Configuration for destination NAT rules.
+ suboptions:
+ rule:
+ type: list
+ elements: dict
+ description: List of destination NAT rules.
+ suboptions:
+ id:
+ type: int
+ required: true
+ description: Rule number for destination NAT.
+ description:
+ type: str
+ description: User-friendly description of the destination NAT rule.
+ protocol:
+ type: str
+ description: Protocol to NAT (default all).
+ packet_type:
+ type: str
+ description: Packet type to match.
+ exclude:
+ type: bool
+ description: Exclude packets matching this rule from NAT.
+ log:
+ type: bool
+ description: Log packets hitting this rule.
+ disable:
+ type: bool
+ description: Disable this destination NAT rule.
+ inbound_interface:
+ type: dict
+ description: Match inbound interface.
+ suboptions:
+ name:
+ type: str
+ description: Interface name to match.
+ group:
+ type: str
+ description: Interface group to match.
+ destination:
+ type: dict
+ description: Match criteria for destination NAT.
+ suboptions:
+ address:
+ type: str
+ description: IP address, subnet, or range to match.
+ fqdn:
+ type: str
+ description: Fully qualified domain name to match.
+ port:
+ type: str
+ description: Port number or range to match.
+ address_group:
+ type: str
+ description: Address group name to match.
+ domain_group:
+ type: str
+ description: Domain group name to match.
+ mac_group:
+ type: str
+ description: MAC address group name to match.
+ network_group:
+ type: str
+ description: Network group name to match.
+ port_group:
+ type: str
+ description: Port group name to match.
+ translation:
+ type: dict
+ description: Translation configuration for destination NAT.
+ suboptions:
+ address:
+ type: str
+ description: IP address or prefix to translate destination to.
+ port:
+ type: str
+ description: Port number or range to translate destination port to.
+ redirect_port:
+ type: str
+ description: Redirect to local port number.
+ address_mapping:
+ type: str
+ choices:
+ - random
+ - persistent
+ description: Address mapping mode for translation.
+ port_mapping:
+ type: str
+ choices:
+ - random
+ - none
+ description: Port mapping mode for translation.
+ load_balance:
+ type: dict
+ description: Load balancing configuration for this NAT rule.
+ suboptions:
+ hash:
+ type: list
+ elements: str
+ description: Fields to hash on for load balancing. Mutually exclusive with I(translation.address).
+ choices:
+ - source-address
+ - destination-address
+ - source-port
+ - destination-port
+ - random
+ backend:
+ type: list
+ elements: dict
+ description: List of backends to load-balance across. Weights should sum to 100.
+ suboptions:
+ ip:
+ type: str
+ description: IP address of the backend translation target.
+ weight:
+ type: int
+ description: Relative weight (1-100) for this backend's share of load-balanced traffic.
+ source:
+ type: dict
+ description: Configuration for source NAT rules.
+ suboptions:
+ rule:
+ type: list
+ elements: dict
+ description: List of source NAT rules.
+ suboptions:
+ id:
+ type: int
+ required: true
+ description: Rule number for source NAT.
+ description:
+ type: str
+ description: User-friendly description of the source NAT rule.
+ protocol:
+ type: str
+ description: Protocol to NAT (default all).
+ packet_type:
+ type: str
+ description: Packet type to match.
+ exclude:
+ type: bool
+ description: Exclude packets matching this rule from NAT.
+ log:
+ type: bool
+ description: Log packets hitting this rule.
+ disable:
+ type: bool
+ description: Disable this source NAT rule.
+ outbound_interface:
+ type: dict
+ description: Match outbound interface.
+ suboptions:
+ name:
+ type: str
+ description: Interface name to match.
+ group:
+ type: str
+ description: Interface group to match.
+ destination:
+ type: dict
+ description: Destination match criteria for source NAT.
+ suboptions:
+ address:
+ type: str
+ description: IP address, subnet, or range to match.
+ fqdn:
+ type: str
+ description: Fully qualified domain name to match.
+ port:
+ type: str
+ description: Port number or range to match.
+ address_group:
+ type: str
+ description: Address group name to match.
+ domain_group:
+ type: str
+ description: Domain group name to match.
+ mac_group:
+ type: str
+ description: MAC address group name to match.
+ network_group:
+ type: str
+ description: Network group name to match.
+ port_group:
+ type: str
+ description: Port group name to match.
+ source:
+ type: dict
+ description: Source match criteria for source NAT.
+ suboptions:
+ address:
+ type: str
+ description: IP address, subnet, or range to match.
+ fqdn:
+ type: str
+ description: Fully qualified domain name to match.
+ port:
+ type: str
+ description: Port number or range to match.
+ address_group:
+ type: str
+ description: Address group name to match.
+ domain_group:
+ type: str
+ description: Domain group name to match.
+ mac_group:
+ type: str
+ description: MAC address group name to match.
+ network_group:
+ type: str
+ description: Network group name to match.
+ port_group:
+ type: str
+ description: Port group name to match.
+ translation:
+ type: dict
+ description: Translation configuration for source NAT.
+ suboptions:
+ address:
+ type: str
+ description: IP address or prefix to translate source to. Use masquerade to masquerade as the outbound interface address.
+ port:
+ type: str
+ description: Port number or range to translate source port to.
+ address_mapping:
+ type: str
+ choices:
+ - random
+ - persistent
+ description: Address mapping mode for translation.
+ port_mapping:
+ type: str
+ choices:
+ - random
+ - none
+ description: Port mapping mode for translation.
+ load_balance:
+ type: dict
+ description: Load balancing configuration for this NAT rule.
+ suboptions:
+ hash:
+ type: list
+ elements: str
+ description: Fields to hash on for load balancing. Mutually exclusive with I(translation.address).
+ choices:
+ - source-address
+ - destination-address
+ - source-port
+ - destination-port
+ - random
+ backend:
+ type: list
+ elements: dict
+ description: List of backends to load-balance across. Weights should sum to 100.
+ suboptions:
+ ip:
+ type: str
+ description: IP address of the backend translation target.
+ weight:
+ type: int
+ description: Relative weight (1-100) for this backend's share of load-balanced traffic.
+ static:
+ type: dict
+ description: Configuration for static one-to-one NAT rules.
+ suboptions:
+ rule:
+ type: list
+ elements: dict
+ description: List of static NAT rules.
+ suboptions:
+ id:
+ type: int
+ required: true
+ description: Rule number for static NAT.
+ description:
+ type: str
+ description: User-friendly description of the static NAT rule.
+ destination:
+ type: dict
+ description: Match criteria for static NAT.
+ suboptions:
+ address:
+ type: str
+ description: IP address, subnet, or range to match.
+ inbound_interface:
+ type: str
+ description: Inbound interface that this static NAT rule applies to.
+ log:
+ type: bool
+ description: Log packets hitting this static NAT rule.
+ translation:
+ type: dict
+ description: Translation configuration for static NAT.
+ suboptions:
+ address:
+ type: str
+ description: IP address or prefix to translate to.
+ nat64:
+ type: dict
+ description: Configuration for NAT64 (IPv6-to-IPv4) rules.
+ suboptions:
+ source:
+ type: dict
+ description: Configuration for NAT64 source rules.
+ suboptions:
+ rule:
+ type: list
+ elements: dict
+ description: List of NAT64 source rules.
+ suboptions:
+ id:
+ type: int
+ required: true
+ description: Rule number for NAT64 source rule (1-999999).
+ description:
+ type: str
+ description: User-friendly description of the NAT64 source rule.
+ disable:
+ type: bool
+ description: Disable this NAT64 source rule.
+ match:
+ type: dict
+ description: Match criteria for NAT64 source rule.
+ suboptions:
+ mark:
+ type: int
+ description: Match on firewall mark value (1-2147483647).
+ source:
+ type: dict
+ description: IPv6 source prefix to match for NAT64 translation.
+ suboptions:
+ prefix:
+ type: str
+ description: IPv6 source prefix to match (h:h:h:h:h:h:h:h/x).
+ translation:
+ type: dict
+ description: Translation configuration for NAT64 source rule.
+ suboptions:
+ pool:
+ type: list
+ elements: dict
+ description: List of translation pools for NAT64.
+ suboptions:
+ id:
+ type: int
+ required: true
+ description: Pool number (1-999999).
+ address:
+ type: str
+ description: IPv4 address or prefix for translation pool.
+ description:
+ type: str
+ description: User-friendly description of the translation pool.
+ disable:
+ type: bool
+ description: Disable this translation pool.
+ port:
+ type: str
+ description: Port number or range for translation pool.
+ protocol:
+ type: str
+ choices:
+ - icmp
+ - tcp
+ - udp
+ description: Protocol for this translation pool entry.
+ nat66:
+ type: dict
+ description: Configuration for NAT66 (IPv6-to-IPv6) rules.
+ suboptions:
+ destination:
+ type: dict
+ description: Configuration for NAT66 destination rules.
+ suboptions:
+ rule:
+ type: list
+ elements: dict
+ description: List of NAT66 destination rules.
+ suboptions:
+ id:
+ type: int
+ required: true
+ description: Rule number for NAT66 destination rule.
+ description:
+ type: str
+ description: User-friendly description of the NAT66 destination rule.
+ destination:
+ type: dict
+ description: Match criteria for NAT66 destination rule.
+ suboptions:
+ address:
+ type: str
+ description: IPv6 address or prefix to match.
+ port:
+ type: str
+ description: Port number or range to match.
+ disable:
+ type: bool
+ description: Disable this NAT66 destination rule.
+ exclude:
+ type: bool
+ description: Exclude packets matching this rule from NAT66.
+ inbound_interface:
+ type: dict
+ description: Inbound interface to match for NAT66 destination rule.
+ suboptions:
+ name:
+ type: str
+ description: Interface name to match.
+ log:
+ type: bool
+ description: Log packets hitting this NAT66 destination rule.
+ protocol:
+ type: str
+ description: Protocol to match.
+ source:
+ type: dict
+ description: Source match criteria for NAT66 destination rule.
+ suboptions:
+ address:
+ type: str
+ description: IPv6 source address or prefix to match.
+ port:
+ type: str
+ description: Source port number or range to match.
+ translation:
+ type: dict
+ description: Translation configuration for NAT66 destination rule.
+ suboptions:
+ address:
+ type: str
+ description: IPv6 address or prefix to translate destination to.
+ port:
+ type: str
+ description: Port number or range to translate destination port to.
+ source:
+ type: dict
+ description: Configuration for NAT66 source rules.
+ suboptions:
+ rule:
+ type: list
+ elements: dict
+ description: List of NAT66 source rules.
+ suboptions:
+ id:
+ type: int
+ required: true
+ description: Rule number for NAT66 source rule.
+ description:
+ type: str
+ description: User-friendly description of the NAT66 source rule.
+ destination:
+ type: dict
+ description: Destination match criteria for NAT66 source rule.
+ suboptions:
+ port:
+ type: str
+ description: Destination port number or range to match.
+ prefix:
+ type: str
+ description: IPv6 destination prefix to match (h:h:h:h:h:h:h:h/x).
+ disable:
+ type: bool
+ description: Disable this NAT66 source rule.
+ exclude:
+ type: bool
+ description: Exclude packets matching this rule from NAT66.
+ log:
+ type: bool
+ description: Log packets hitting this NAT66 source rule.
+ outbound_interface:
+ type: dict
+ description: Outbound interface to match for NAT66 source rule.
+ suboptions:
+ name:
+ type: str
+ description: Interface name to match.
+ protocol:
+ type: str
+ description: Protocol to match.
+ source:
+ type: dict
+ description: Source match criteria for NAT66 source rule.
+ suboptions:
+ port:
+ type: str
+ description: Source port number or range to match.
+ prefix:
+ type: str
+ description: IPv6 source prefix to match (h:h:h:h:h:h:h:h/x).
+ translation:
+ type: dict
+ description: Translation configuration for NAT66 source rule.
+ suboptions:
+ address:
+ type: str
+ description: IPv6 address or prefix to translate source to. Use masquerade to masquerade as the outbound interface address.
+ port:
+ type: str
+ description: Port number or range to translate source port to.
+ running_config:
+ description:
+ - This option is used only with state I(parsed).
+ - The value of this option should be the output received from the VyOS device by
+ executing the command B(show configuration commands | match 'nat').
+ - The state I(parsed) reads the configuration from C(show configuration commands | match 'nat')
+ and transforms it into Ansible structured data as per the module argspec.
+ The value is then returned in the I(parsed) key within the result.
+ - The state I(replaced) replaces only the provided configuration, while I(overridden) removes any
+ existing NAT configuration not specified in I(config).
+ type: str
+ state:
+ description:
+ - The state the configuration should be left in.
+ type: str
+ choices:
+ - deleted
+ - merged
+ - overridden
+ - replaced
+ - gathered
+ - rendered
+ - parsed
+ default: merged
+"""
+EXAMPLES = """
+# Using merged - configure CGNAT
+- name: Merge CGNAT configuration
+ vyos.vyos.vyos_nat:
+ config:
+ nat:
+ cgnat:
+ log_allocation: true
+ pool:
+ external:
+ - name: ext-pool-1
+ external_port_range: "10000-20000"
+ per_user_limit:
+ port: "200"
+ range:
+ - value: 203.0.113.0/24
+ internal:
+ - name: int-pool-1
+ range:
+ - 10.0.0.0/24
+ rule:
+ - id: 1
+ source:
+ pool: int-pool-1
+ translation:
+ pool: ext-pool-1
+ state: merged
+
+# Using merged - configure destination NAT
+- name: Merge destination NAT rule
+ vyos.vyos.vyos_nat:
+ config:
+ nat:
+ destination:
+ rule:
+ - id: 100
+ description: "Web server NAT"
+ protocol: tcp
+ log: true
+ destination:
+ address: 198.51.100.10
+ port: "80"
+ translation:
+ address: 192.168.1.10
+ port: "8080"
+ state: merged
+
+# Using merged - configure source NAT
+- name: Merge source NAT rule
+ vyos.vyos.vyos_nat:
+ config:
+ nat:
+ source:
+ rule:
+ - id: 200
+ description: "Outbound masquerade"
+ protocol: tcp
+ log: true
+ outbound_interface:
+ name: eth0
+ translation:
+ address: masquerade
+ state: merged
+
+# Using merged - configure static NAT
+- name: Merge static NAT rule
+ vyos.vyos.vyos_nat:
+ config:
+ nat:
+ static:
+ rule:
+ - id: 300
+ description: "Static mapping"
+ inbound_interface: eth2
+ destination:
+ address: 198.51.100.20
+ translation:
+ address: 192.168.1.20
+ log: true
+ state: merged
+
+# Using merged - configure NAT64
+- name: Merge NAT64 source rule
+ vyos.vyos.vyos_nat:
+ config:
+ nat64:
+ source:
+ rule:
+ - id: 10
+ description: "NAT64 example"
+ source:
+ prefix: 2001:db8::/96
+ match:
+ mark: 100
+ translation:
+ pool:
+ - id: 1
+ address: 192.168.100.10
+ port: "1-65535"
+ protocol: udp
+ state: merged
+
+# Using merged - configure NAT66
+- name: Merge NAT66 destination rule
+ vyos.vyos.vyos_nat:
+ config:
+ nat66:
+ destination:
+ rule:
+ - id: 20
+ description: "NAT66 DNAT"
+ protocol: tcp
+ inbound_interface:
+ name: eth1
+ destination:
+ address: 2001:db8::1
+ translation:
+ address: 2001:db8:1::10
+ port: "8443"
+ state: merged
+
+# Using replaced - replace specific NAT rules
+- name: Replace destination NAT rule
+ vyos.vyos.vyos_nat:
+ config:
+ nat:
+ destination:
+ rule:
+ - id: 100
+ description: "Replaced web server NAT"
+ protocol: tcp
+ destination:
+ address: 198.51.100.10
+ port: "443"
+ translation:
+ address: 192.168.1.10
+ port: "8443"
+ state: replaced
+
+# Using overridden - override entire NAT configuration
+- name: Override entire NAT configuration
+ vyos.vyos.vyos_nat:
+ config:
+ nat:
+ destination:
+ rule:
+ - id: 100
+ description: "Only rule after override"
+ protocol: tcp
+ destination:
+ address: 198.51.100.10
+ port: "80"
+ translation:
+ address: 192.168.1.10
+ port: "8080"
+ state: overridden
+
+# Using deleted - delete all NAT configuration
+- name: Delete all NAT configuration
+ vyos.vyos.vyos_nat:
+ state: deleted
+
+# Using deleted - delete specific NAT rules
+- name: Delete specific NAT rules
+ vyos.vyos.vyos_nat:
+ config:
+ nat:
+ destination:
+ rule:
+ - id: 100
+ source:
+ rule:
+ - id: 200
+ nat64:
+ source:
+ rule:
+ - id: 10
+ state: deleted
+
+# Using gathered
+- name: Gather NAT configuration from device
+ vyos.vyos.vyos_nat:
+ state: gathered
+
+# Using rendered
+- name: Render NAT configuration offline
+ vyos.vyos.vyos_nat:
+ config:
+ nat:
+ destination:
+ rule:
+ - id: 100
+ description: "Rendered rule"
+ protocol: tcp
+ destination:
+ address: 198.51.100.10
+ port: "80"
+ translation:
+ address: 192.168.1.10
+ port: "8080"
+ state: rendered
+
+# Using parsed
+- name: Parse NAT configuration from file
+ vyos.vyos.vyos_nat:
+ running_config: "{{ lookup('file', './nat_config.cfg') }}"
+ state: parsed
+"""
+RETURN = """
+before:
+ description: The configuration prior to the module execution.
+ returned: when I(state) is C(merged), C(replaced), C(overridden) or C(deleted)
+ type: dict
+ sample: >
+ This output will always be in the same format as the
+ module argspec.
+after:
+ description: The resulting configuration after module execution.
+ returned: when changed
+ type: dict
+ sample: >
+ This output will always be in the same format as the
+ module argspec.
+commands:
+ description: The set of commands pushed to the remote device.
+ returned: when I(state) is C(merged), C(replaced), C(overridden) or C(deleted)
+ type: list
+ sample:
+ - set nat destination rule 100 description 'Web server NAT'
+ - set nat destination rule 100 protocol tcp
+ - set nat destination rule 100 inbound-interface name eth2
+ - set nat destination rule 100 destination address 198.51.100.10
+ - set nat destination rule 100 translation address 192.168.1.10
+ - delete nat source rule 200
+rendered:
+ description: The provided configuration in the task rendered in device-native format (offline).
+ returned: when I(state) is C(rendered)
+ type: list
+ sample:
+ - set nat destination rule 100 description 'Web server NAT'
+ - set nat destination rule 100 protocol tcp
+ - set nat destination rule 100 inbound-interface name eth2
+ - set nat destination rule 100 destination address 198.51.100.10
+ - set nat destination rule 100 translation address 192.168.1.10
+gathered:
+ description: Facts about the network resource gathered from the remote device as structured data.
+ returned: when I(state) is C(gathered)
+ type: dict
+ sample: >
+ This output will always be in the same format as the
+ module argspec.
+parsed:
+ description: The device native config provided in I(running_config) option parsed into structured data as per module argspec.
+ returned: when I(state) is C(parsed)
+ type: dict
+ sample: >
+ This output will always be in the same format as the
+ module argspec.
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.nat.nat import (
+ NatArgs,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.nat.nat import (
+ Nat,
+)
+
+
+def main():
+ """
+ Main entry point for module execution
+
+ :returns: the result form module invocation
+ """
+ module = AnsibleModule(
+ argument_spec=NatArgs.argument_spec,
+ mutually_exclusive=[["config", "running_config"]],
+ required_if=[
+ ["state", "merged", ["config"]],
+ ["state", "replaced", ["config"]],
+ ["state", "overridden", ["config"]],
+ ["state", "rendered", ["config"]],
+ ["state", "parsed", ["running_config"]],
+ ],
+ supports_check_mode=True,
+ )
+
+ result = Nat(module).execute_module()
+ module.exit_json(**result)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/plugins/modules/vyos_vrf.py b/plugins/modules/vyos_vrf.py
index defe24bb..3f7ae10e 100644
--- a/plugins/modules/vyos_vrf.py
+++ b/plugins/modules/vyos_vrf.py
@@ -1,8 +1,6 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
-# Copyright 2024 Red Hat
-# GNU General Public License v3.0+
-# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""
The module file for vyos_vrf
diff --git a/tests/integration/targets/vyos_nat/defaults/main.yaml b/tests/integration/targets/vyos_nat/defaults/main.yaml
new file mode 100644
index 00000000..164afead
--- /dev/null
+++ b/tests/integration/targets/vyos_nat/defaults/main.yaml
@@ -0,0 +1,3 @@
+---
+testcase: "[^_].*"
+test_items: []
diff --git a/tests/integration/targets/vyos_nat/tasks/cli.yaml b/tests/integration/targets/vyos_nat/tasks/cli.yaml
new file mode 100644
index 00000000..a71206c0
--- /dev/null
+++ b/tests/integration/targets/vyos_nat/tasks/cli.yaml
@@ -0,0 +1,20 @@
+---
+- name: Collect all cli test cases
+ ansible.builtin.find:
+ paths: "{{ role_path }}/tests/cli"
+ patterns: "{{ testcase }}\\.yaml$"
+ use_regex: true
+ delegate_to: localhost
+ register: test_cases
+
+- name: Set test_items
+ ansible.builtin.set_fact:
+ test_items: "{{ test_cases.files | map(attribute='path') | list }}"
+
+- name: Run test case (connection=ansible.netcommon.network_cli)
+ ansible.builtin.include_tasks: "{{ test_case_to_run }}"
+ vars:
+ ansible_connection: ansible.netcommon.network_cli
+ with_items: "{{ test_items }}"
+ loop_control:
+ loop_var: test_case_to_run
diff --git a/tests/integration/targets/vyos_nat/tasks/main.yaml b/tests/integration/targets/vyos_nat/tasks/main.yaml
new file mode 100644
index 00000000..e6378581
--- /dev/null
+++ b/tests/integration/targets/vyos_nat/tasks/main.yaml
@@ -0,0 +1,5 @@
+---
+- name: Run CLI tests
+ ansible.builtin.include_tasks: cli.yaml
+ tags:
+ - network_cli
diff --git a/tests/integration/targets/vyos_nat/tests/cli/_parsed.cfg b/tests/integration/targets/vyos_nat/tests/cli/_parsed.cfg
new file mode 100644
index 00000000..68375366
--- /dev/null
+++ b/tests/integration/targets/vyos_nat/tests/cli/_parsed.cfg
@@ -0,0 +1,17 @@
+set nat destination rule 100 description 'Web server NAT'
+set nat destination rule 100 destination address '198.51.100.10'
+set nat destination rule 100 destination port '80'
+set nat destination rule 100 inbound-interface name 'eth2'
+set nat destination rule 100 log
+set nat destination rule 100 protocol 'tcp'
+set nat destination rule 100 translation address '192.168.1.10'
+set nat destination rule 100 translation port '8080'
+set nat source rule 200 description 'Outbound NAT'
+set nat source rule 200 destination address '192.168.10.100'
+set nat source rule 200 destination port '8083'
+set nat source rule 200 disable
+set nat source rule 200 exclude
+set nat source rule 200 log
+set nat source rule 200 protocol 'tcp'
+set nat source rule 200 translation address 'masquerade'
+set nat source rule 200 translation port '443'
diff --git a/tests/integration/targets/vyos_nat/tests/cli/_populate.yaml b/tests/integration/targets/vyos_nat/tests/cli/_populate.yaml
new file mode 100644
index 00000000..a3715be9
--- /dev/null
+++ b/tests/integration/targets/vyos_nat/tests/cli/_populate.yaml
@@ -0,0 +1,8 @@
+---
+- ansible.builtin.include_tasks: _remove_config.yaml
+
+- name: Setup NAT configuration
+ vyos.vyos.vyos_config:
+ lines: "{{ populate_config }}"
+ vars:
+ ansible_connection: ansible.netcommon.network_cli
diff --git a/tests/integration/targets/vyos_nat/tests/cli/_remove_config.yaml b/tests/integration/targets/vyos_nat/tests/cli/_remove_config.yaml
new file mode 100644
index 00000000..3618bf40
--- /dev/null
+++ b/tests/integration/targets/vyos_nat/tests/cli/_remove_config.yaml
@@ -0,0 +1,10 @@
+---
+- name: Remove NAT configuration
+ vyos.vyos.vyos_config:
+ lines:
+ - delete nat
+ - delete nat64
+ - delete nat66
+ ignore_errors: true
+ vars:
+ ansible_connection: ansible.netcommon.network_cli
diff --git a/tests/integration/targets/vyos_nat/tests/cli/deleted.yaml b/tests/integration/targets/vyos_nat/tests/cli/deleted.yaml
new file mode 100644
index 00000000..c08d6b1c
--- /dev/null
+++ b/tests/integration/targets/vyos_nat/tests/cli/deleted.yaml
@@ -0,0 +1,34 @@
+---
+- debug:
+ msg: START vyos_nat deleted integration tests on connection={{ ansible_connection }}
+
+- include_tasks: _populate.yaml
+
+- block:
+ - name: Delete all NAT configuration
+ register: result
+ vyos.vyos.vyos_nat: &id001
+ config:
+ state: deleted
+
+ - assert:
+ that:
+ - result.changed == true
+ - result.commands|symmetric_difference(deleted.commands) == []
+
+ - name: Assert that the after dicts were correctly generated
+ assert:
+ that:
+ - result.after|symmetric_difference(deleted.after) == []
+
+ - name: Delete the existing configuration (IDEMPOTENT)
+ register: result
+ vyos.vyos.vyos_nat: *id001
+
+ - name: Assert that the previous task was idempotent
+ assert:
+ that:
+ - result['changed'] == false
+
+ always:
+ - include_tasks: _remove_config.yaml
diff --git a/tests/integration/targets/vyos_nat/tests/cli/deleted_full_match.yaml b/tests/integration/targets/vyos_nat/tests/cli/deleted_full_match.yaml
new file mode 100644
index 00000000..43a8124f
--- /dev/null
+++ b/tests/integration/targets/vyos_nat/tests/cli/deleted_full_match.yaml
@@ -0,0 +1,70 @@
+---
+- debug:
+ msg: START vyos_nat deleted full-match integration tests on connection={{ ansible_connection }}
+
+- include_tasks: _populate.yaml
+
+- block:
+ - name: Delete destination rule 100 with full matching config
+ register: result
+ vyos.vyos.vyos_nat:
+ config:
+ nat:
+ destination:
+ rule:
+ - id: 100
+ description: Web server NAT
+ protocol: tcp
+ log: true
+ inbound_interface:
+ name: eth2
+ destination:
+ address: 198.51.100.10
+ port: "80"
+ translation:
+ address: 192.168.1.10
+ port: "8080"
+ state: deleted
+
+ - assert:
+ that:
+ - result.changed == true
+ - result.commands == ["delete nat destination rule 100"]
+
+ - vyos.vyos.vyos_facts:
+ gather_network_resources: nat
+
+ - name: Assert rule 100 is actually gone from the device
+ assert:
+ that:
+ - ansible_facts['network_resources']['nat'].nat.destination is not defined or
+ ansible_facts['network_resources']['nat'].nat.destination.rule | default([]) | selectattr('id', 'equalto', 100) | list | length == 0
+
+ - name: Delete the same configuration again (IDEMPOTENT)
+ register: result
+ vyos.vyos.vyos_nat:
+ config:
+ nat:
+ destination:
+ rule:
+ - id: 100
+ description: Web server NAT
+ protocol: tcp
+ log: true
+ inbound_interface:
+ name: eth2
+ destination:
+ address: 198.51.100.10
+ port: "80"
+ translation:
+ address: 192.168.1.10
+ port: "8080"
+ state: deleted
+
+ - name: Assert that the previous task was idempotent
+ assert:
+ that:
+ - result['changed'] == false
+
+ always:
+ - include_tasks: _remove_config.yaml
diff --git a/tests/integration/targets/vyos_nat/tests/cli/empty_config.yaml b/tests/integration/targets/vyos_nat/tests/cli/empty_config.yaml
new file mode 100644
index 00000000..39b57fee
--- /dev/null
+++ b/tests/integration/targets/vyos_nat/tests/cli/empty_config.yaml
@@ -0,0 +1,60 @@
+---
+- debug:
+ msg: START vyos_nat empty_config integration tests on connection={{ ansible_connection }}
+
+- name: Merged with empty config should give appropriate error message
+ register: result
+ ignore_errors: true
+ vyos.vyos.vyos_nat:
+ config:
+ state: merged
+
+- assert:
+ that:
+ - result.msg == 'value of config parameter must not be empty for state merged'
+
+- name: Replaced with empty config should give appropriate error message
+ register: result
+ ignore_errors: true
+ vyos.vyos.vyos_nat:
+ config:
+ state: replaced
+
+- assert:
+ that:
+ - result.msg == 'value of config parameter must not be empty for state replaced'
+
+- name: Overridden with empty config should give appropriate error message
+ register: result
+ ignore_errors: true
+ vyos.vyos.vyos_nat:
+ config:
+ state: overridden
+
+- assert:
+ that:
+ - result.msg == 'value of config parameter must not be empty for state overridden'
+
+- name: Parsed with empty running_config should give appropriate error message
+ register: result
+ ignore_errors: true
+ vyos.vyos.vyos_nat:
+ running_config:
+ state: parsed
+
+- assert:
+ that:
+ - result.msg == 'value of running_config parameter must not be empty for state parsed'
+
+- name: Rendered with empty config should give appropriate error message
+ register: result
+ ignore_errors: true
+ vyos.vyos.vyos_nat:
+ config:
+ state: rendered
+
+- assert:
+ that:
+ - result.msg == 'value of config parameter must not be empty for state rendered'
+
+- include_tasks: _remove_config.yaml
diff --git a/tests/integration/targets/vyos_nat/tests/cli/gathered.yaml b/tests/integration/targets/vyos_nat/tests/cli/gathered.yaml
new file mode 100644
index 00000000..a91410b8
--- /dev/null
+++ b/tests/integration/targets/vyos_nat/tests/cli/gathered.yaml
@@ -0,0 +1,22 @@
+---
+- debug:
+ msg: START vyos_nat gathered integration tests on connection={{ ansible_connection }}
+
+- include_tasks: _populate.yaml
+
+- block:
+ - name: Gather NAT config from the device
+ register: result
+ vyos.vyos.vyos_nat:
+ state: gathered
+
+ - vyos.vyos.vyos_facts:
+ gather_network_resources: nat
+
+ - assert:
+ that:
+ - result.changed == false
+ - result.gathered|symmetric_difference(ansible_facts['network_resources']['nat']) == []
+
+ always:
+ - include_tasks: _remove_config.yaml
diff --git a/tests/integration/targets/vyos_nat/tests/cli/merged.yaml b/tests/integration/targets/vyos_nat/tests/cli/merged.yaml
new file mode 100644
index 00000000..73184fb6
--- /dev/null
+++ b/tests/integration/targets/vyos_nat/tests/cli/merged.yaml
@@ -0,0 +1,110 @@
+---
+- debug:
+ msg: START vyos_nat merged integration tests on connection={{ ansible_connection }}
+
+- include_tasks: _populate.yaml
+
+- block:
+ - name: Merge new NAT rule with existing configuration
+ register: result
+ vyos.vyos.vyos_nat: &id001
+ config:
+ nat:
+ destination:
+ rule:
+ - id: 101
+ description: New DNAT rule
+ protocol: tcp
+ destination:
+ address: 198.51.100.11
+ port: "443"
+ translation:
+ address: 192.168.1.11
+ port: "8443"
+ state: merged
+
+ - vyos.vyos.vyos_facts:
+ gather_network_resources: nat
+
+ - assert:
+ that:
+ - result.changed == true
+ - result.commands|symmetric_difference(merged.commands) == []
+ - result.after|symmetric_difference(ansible_facts['network_resources']['nat']) == []
+
+ - name: Assert that before dicts were correctly generated
+ assert:
+ that:
+ - merged['before'] | symmetric_difference(result['before']) | length == 0
+
+ - name: Merge the provided configuration with the existing running configuration (IDEMPOTENT)
+ register: result
+ vyos.vyos.vyos_nat: *id001
+
+ - name: Assert that the previous task was idempotent
+ assert:
+ that:
+ - result['changed'] == false
+
+ - name: Add static rule with plain inbound_interface string
+ register: result
+ vyos.vyos.vyos_nat: &id002
+ config:
+ nat:
+ static:
+ rule:
+ - id: 300
+ description: Static mapping
+ inbound_interface: eth2
+ destination:
+ address: 192.168.100.20
+ translation:
+ address: 192.168.1.20
+ state: merged
+
+ - assert:
+ that:
+ - result.changed == true
+ - "'set nat static rule 300 inbound-interface eth2' in result.commands"
+
+ - name: Add static rule again (IDEMPOTENT)
+ register: result
+ vyos.vyos.vyos_nat: *id002
+
+ - name: Assert that the previous task was idempotent
+ assert:
+ that:
+ - result['changed'] == false
+
+ - name: Change static rule inbound_interface
+ register: result
+ vyos.vyos.vyos_nat: &id003
+ config:
+ nat:
+ static:
+ rule:
+ - id: 300
+ description: Static mapping
+ inbound_interface: eth3
+ destination:
+ address: 192.168.100.20
+ translation:
+ address: 192.168.1.20
+ state: merged
+
+ - assert:
+ that:
+ - result.changed == true
+ - result.commands == ["set nat static rule 300 inbound-interface eth3"]
+
+ - name: Change static rule inbound_interface again (IDEMPOTENT)
+ register: result
+ vyos.vyos.vyos_nat: *id003
+
+ - name: Assert that the previous task was idempotent
+ assert:
+ that:
+ - result['changed'] == false
+
+ always:
+ - include_tasks: _remove_config.yaml
diff --git a/tests/integration/targets/vyos_nat/tests/cli/merged_load_balance.yaml b/tests/integration/targets/vyos_nat/tests/cli/merged_load_balance.yaml
new file mode 100644
index 00000000..04ee5816
--- /dev/null
+++ b/tests/integration/targets/vyos_nat/tests/cli/merged_load_balance.yaml
@@ -0,0 +1,107 @@
+---
+- debug:
+ msg: START vyos_nat load balance integration tests on connection={{ ansible_connection }}
+
+- include_tasks: _populate.yaml
+
+- block:
+ - name: Add destination rule with load-balance hash and backends
+ register: result
+ vyos.vyos.vyos_nat: &id001
+ config:
+ nat:
+ destination:
+ rule:
+ - id: 105
+ protocol: tcp
+ destination:
+ port: "80"
+ inbound_interface:
+ name: eth0
+ load_balance:
+ hash:
+ - source-address
+ - destination-address
+ backend:
+ - ip: 10.10.10.1
+ weight: 60
+ - ip: 10.10.10.2
+ weight: 40
+ state: merged
+
+ - assert:
+ that:
+ - result.changed == true
+ - "'set nat destination rule 105 load-balance hash source-address' in result.commands"
+ - "'set nat destination rule 105 load-balance hash destination-address' in result.commands"
+ - "'set nat destination rule 105 load-balance backend 10.10.10.1 weight 60' in result.commands"
+ - "'set nat destination rule 105 load-balance backend 10.10.10.2 weight 40' in result.commands"
+
+ - name: Re-apply same load-balance config (IDEMPOTENT)
+ register: result
+ vyos.vyos.vyos_nat: *id001
+
+ - name: Assert that the previous task was idempotent
+ assert:
+ that:
+ - result['changed'] == false
+
+ - name: Change backend weights only (in-place overwrite check)
+ register: result
+ vyos.vyos.vyos_nat: &id002
+ config:
+ nat:
+ destination:
+ rule:
+ - id: 105
+ protocol: tcp
+ destination:
+ port: "80"
+ inbound_interface:
+ name: eth0
+ load_balance:
+ hash:
+ - source-address
+ - destination-address
+ backend:
+ - ip: 10.10.10.1
+ weight: 70
+ - ip: 10.10.10.2
+ weight: 30
+ state: merged
+
+ - assert:
+ that:
+ - result.changed == true
+
+ - vyos.vyos.vyos_facts:
+ gather_network_resources: nat
+
+ - name: Extract rule 105 from gathered facts
+ set_fact:
+ rule_105: "{{ ansible_facts['network_resources']['nat'].nat.destination.rule | selectattr('id', 'equalto', 105) | first }}"
+
+ - name: Extract backend weights from rule 105
+ set_fact:
+ backend_105: "{{ rule_105.load_balance.backend }}"
+ weight_101: "{{ (rule_105.load_balance.backend | selectattr('ip', 'equalto', '10.10.10.1') | first).weight }}"
+ weight_102: "{{ (rule_105.load_balance.backend | selectattr('ip', 'equalto', '10.10.10.2') | first).weight }}"
+
+ - name: Assert weights updated and no duplicate/stale backend entries
+ assert:
+ that:
+ - backend_105 | length == 2
+ - weight_101 == 70
+ - weight_102 == 30
+
+ - name: Re-apply changed weights again (IDEMPOTENT)
+ register: result
+ vyos.vyos.vyos_nat: *id002
+
+ - name: Assert that the previous task was idempotent
+ assert:
+ that:
+ - result['changed'] == false
+
+ always:
+ - include_tasks: _remove_config.yaml
diff --git a/tests/integration/targets/vyos_nat/tests/cli/overridden.yaml b/tests/integration/targets/vyos_nat/tests/cli/overridden.yaml
new file mode 100644
index 00000000..fbb87d72
--- /dev/null
+++ b/tests/integration/targets/vyos_nat/tests/cli/overridden.yaml
@@ -0,0 +1,45 @@
+---
+- debug:
+ msg: START vyos_nat overridden integration tests on connection={{ ansible_connection }}
+
+- include_tasks: _populate.yaml
+
+- block:
+ - name: Override NAT configuration
+ register: result
+ vyos.vyos.vyos_nat: &id001
+ config:
+ nat:
+ destination:
+ rule:
+ - id: 100
+ description: Overridden DNAT rule
+ protocol: tcp
+ destination:
+ address: 198.51.100.20
+ port: "443"
+ translation:
+ address: 192.168.1.20
+ port: "8443"
+ state: overridden
+
+ - vyos.vyos.vyos_facts:
+ gather_network_resources: nat
+
+ - assert:
+ that:
+ - result.changed == true
+ - result.commands|symmetric_difference(overridden.commands) == []
+ - result.after|symmetric_difference(ansible_facts['network_resources']['nat']) == []
+
+ - name: Override the existing configuration (IDEMPOTENT)
+ register: result
+ vyos.vyos.vyos_nat: *id001
+
+ - name: Assert that the previous task was idempotent
+ assert:
+ that:
+ - result['changed'] == false
+
+ always:
+ - include_tasks: _remove_config.yaml
diff --git a/tests/integration/targets/vyos_nat/tests/cli/parsed.yaml b/tests/integration/targets/vyos_nat/tests/cli/parsed.yaml
new file mode 100644
index 00000000..13a54f42
--- /dev/null
+++ b/tests/integration/targets/vyos_nat/tests/cli/parsed.yaml
@@ -0,0 +1,16 @@
+---
+- debug:
+ msg: START vyos_nat parsed integration tests on connection={{ ansible_connection }}
+
+- name: Parse NAT configuration from file
+ register: result
+ vyos.vyos.vyos_nat:
+ running_config: "{{ lookup('file', '_parsed.cfg') }}"
+ state: parsed
+
+- assert:
+ that:
+ - result.changed == false
+ - result.parsed|symmetric_difference(merged.before) == []
+
+- include_tasks: _remove_config.yaml
diff --git a/tests/integration/targets/vyos_nat/tests/cli/rendered.yaml b/tests/integration/targets/vyos_nat/tests/cli/rendered.yaml
new file mode 100644
index 00000000..9c7a8ccb
--- /dev/null
+++ b/tests/integration/targets/vyos_nat/tests/cli/rendered.yaml
@@ -0,0 +1,32 @@
+---
+- debug:
+ msg: START vyos_nat rendered integration tests on connection={{ ansible_connection }}
+
+- include_tasks: _remove_config.yaml
+
+- block:
+ - name: Render NAT configuration without applying to device
+ register: result
+ vyos.vyos.vyos_nat:
+ config:
+ nat:
+ destination:
+ rule:
+ - id: 100
+ description: Rendered rule
+ protocol: tcp
+ destination:
+ address: 198.51.100.10
+ port: "80"
+ translation:
+ address: 192.168.1.10
+ port: "8080"
+ state: rendered
+
+ - assert:
+ that:
+ - result.changed == false
+ - result.rendered|symmetric_difference(rendered.commands) == []
+
+ always:
+ - include_tasks: _remove_config.yaml
diff --git a/tests/integration/targets/vyos_nat/tests/cli/replaced.yaml b/tests/integration/targets/vyos_nat/tests/cli/replaced.yaml
new file mode 100644
index 00000000..a327c623
--- /dev/null
+++ b/tests/integration/targets/vyos_nat/tests/cli/replaced.yaml
@@ -0,0 +1,45 @@
+---
+- debug:
+ msg: START vyos_nat replaced integration tests on connection={{ ansible_connection }}
+
+- include_tasks: _populate.yaml
+
+- block:
+ - name: Replace NAT destination rule 100
+ register: result
+ vyos.vyos.vyos_nat: &id001
+ config:
+ nat:
+ destination:
+ rule:
+ - id: 100
+ description: Replaced DNAT rule
+ protocol: udp
+ destination:
+ address: 198.51.100.10
+ port: "53"
+ translation:
+ address: 192.168.1.53
+ port: "53"
+ state: replaced
+
+ - vyos.vyos.vyos_facts:
+ gather_network_resources: nat
+
+ - assert:
+ that:
+ - result.changed == true
+ - result.commands|symmetric_difference(replaced.commands) == []
+ - result.after|symmetric_difference(ansible_facts['network_resources']['nat']) == []
+
+ - name: Replace the provided configuration (IDEMPOTENT)
+ register: result
+ vyos.vyos.vyos_nat: *id001
+
+ - name: Assert that the previous task was idempotent
+ assert:
+ that:
+ - result['changed'] == false
+
+ always:
+ - include_tasks: _remove_config.yaml
diff --git a/tests/integration/targets/vyos_nat/tests/cli/replaced_cgnat_pool.yaml b/tests/integration/targets/vyos_nat/tests/cli/replaced_cgnat_pool.yaml
new file mode 100644
index 00000000..77fc295b
--- /dev/null
+++ b/tests/integration/targets/vyos_nat/tests/cli/replaced_cgnat_pool.yaml
@@ -0,0 +1,77 @@
+---
+- debug:
+ msg: START vyos_nat replaced cgnat pool integration tests on connection={{ ansible_connection }}
+
+- include_tasks: _populate.yaml
+
+- block:
+ - name: Establish baseline CGNAT external pool
+ vyos.vyos.vyos_nat:
+ config:
+ nat:
+ cgnat:
+ log_allocation: true
+ pool:
+ external:
+ - name: ext-pool-1
+ external_port_range: "10000-20000"
+ per_user_limit:
+ port: "200"
+ range:
+ - value: 203.0.113.0/24
+ - value: 203.1.113.1-203.1.113.60
+ seq: "10"
+ internal:
+ - name: int-pool-1
+ range:
+ - 10.0.0.0/24
+ rule:
+ - id: 1
+ source:
+ pool: int-pool-1
+ translation:
+ pool: ext-pool-1
+ state: merged
+
+ - name: Replace CGNAT pool changing only external_port_range
+ register: result
+ vyos.vyos.vyos_nat:
+ config:
+ nat:
+ cgnat:
+ log_allocation: true
+ pool:
+ external:
+ - name: ext-pool-1
+ external_port_range: "30000-40000"
+ per_user_limit:
+ port: "200"
+ range:
+ - value: 203.0.113.0/24
+ - value: 203.1.113.1-203.1.113.60
+ seq: "10"
+ internal:
+ - name: int-pool-1
+ range:
+ - 10.0.0.0/24
+ rule:
+ - id: 1
+ source:
+ pool: int-pool-1
+ translation:
+ pool: ext-pool-1
+ state: replaced
+
+ - vyos.vyos.vyos_facts:
+ gather_network_resources: nat
+
+ - name: Assert all pool fields survived the replace, not just the changed one
+ assert:
+ that:
+ - result.changed == true
+ - ansible_facts['network_resources']['nat'].nat.cgnat.pool.external[0].external_port_range == "30000-40000"
+ - ansible_facts['network_resources']['nat'].nat.cgnat.pool.external[0].per_user_limit.port == "200"
+ - ansible_facts['network_resources']['nat'].nat.cgnat.pool.external[0].range | length == 2
+
+ always:
+ - include_tasks: _remove_config.yaml
diff --git a/tests/integration/targets/vyos_nat/tests/cli/rtt.yaml b/tests/integration/targets/vyos_nat/tests/cli/rtt.yaml
new file mode 100644
index 00000000..8ec9397e
--- /dev/null
+++ b/tests/integration/targets/vyos_nat/tests/cli/rtt.yaml
@@ -0,0 +1,70 @@
+---
+- debug:
+ msg: START vyos_nat round trip integration tests on connection={{ ansible_connection }}
+
+- include_tasks: _populate.yaml
+
+- block:
+ - name: Apply additional NAT rule to establish base config
+ register: baseconfig
+ vyos.vyos.vyos_nat:
+ config:
+ nat:
+ destination:
+ rule:
+ - id: 100
+ description: Web server NAT
+ protocol: tcp
+ log: true
+ inbound_interface:
+ name: eth2
+ destination:
+ address: 198.51.100.10
+ port: "80"
+ translation:
+ address: 192.168.1.10
+ port: "8080"
+ - id: 101
+ description: RTT base rule
+ protocol: tcp
+ destination:
+ address: 198.51.100.11
+ port: "443"
+ translation:
+ address: 192.168.1.11
+ port: "8443"
+ state: merged
+
+ - vyos.vyos.vyos_facts:
+ gather_network_resources: nat
+
+ - name: Modify rule 101 description
+ register: result
+ vyos.vyos.vyos_nat:
+ config:
+ nat:
+ destination:
+ rule:
+ - id: 101
+ description: RTT modified rule
+ protocol: tcp
+ destination:
+ address: 198.51.100.11
+ port: "443"
+ translation:
+ address: 192.168.1.11
+ port: "8443"
+ state: merged
+
+ - name: Revert back to base config using facts round trip
+ register: revert
+ vyos.vyos.vyos_nat:
+ config: "{{ ansible_facts['network_resources']['nat'] }}"
+ state: overridden
+
+ - name: Assert that config was reverted
+ assert:
+ that: baseconfig.after == revert.after
+
+ always:
+ - include_tasks: _remove_config.yaml
diff --git a/tests/integration/targets/vyos_nat/vars/main.yaml b/tests/integration/targets/vyos_nat/vars/main.yaml
new file mode 100644
index 00000000..0bae2c03
--- /dev/null
+++ b/tests/integration/targets/vyos_nat/vars/main.yaml
@@ -0,0 +1,172 @@
+---
+populate_config:
+ - set nat destination rule 100 description 'Web server NAT'
+ - set nat destination rule 100 destination address '198.51.100.10'
+ - set nat destination rule 100 destination port '80'
+ - set nat destination rule 100 inbound-interface name 'eth2'
+ - set nat destination rule 100 log
+ - set nat destination rule 100 protocol 'tcp'
+ - set nat destination rule 100 translation address '192.168.1.10'
+ - set nat destination rule 100 translation port '8080'
+ - set nat source rule 200 description 'Outbound NAT'
+ - set nat source rule 200 destination address '192.168.10.100'
+ - set nat source rule 200 destination port '8083'
+ - set nat source rule 200 disable
+ - set nat source rule 200 exclude
+ - set nat source rule 200 log
+ - set nat source rule 200 protocol 'tcp'
+ - set nat source rule 200 translation address 'masquerade'
+ - set nat source rule 200 translation port '443'
+
+merged:
+ before:
+ nat:
+ destination:
+ rule:
+ - id: 100
+ description: Web server NAT
+ protocol: tcp
+ log: true
+ inbound_interface:
+ name: eth2
+ destination:
+ address: 198.51.100.10
+ port: "80"
+ translation:
+ address: 192.168.1.10
+ port: "8080"
+ source:
+ rule:
+ - id: 200
+ description: Outbound NAT
+ protocol: tcp
+ log: true
+ disable: true
+ exclude: true
+ destination:
+ address: 192.168.10.100
+ port: "8083"
+ translation:
+ address: masquerade
+ port: "443"
+ commands:
+ - set nat destination rule 101 description 'New DNAT rule'
+ - set nat destination rule 101 protocol tcp
+ - set nat destination rule 101 destination address 198.51.100.11
+ - set nat destination rule 101 destination port 443
+ - set nat destination rule 101 translation address 192.168.1.11
+ - set nat destination rule 101 translation port 8443
+ after:
+ nat:
+ destination:
+ rule:
+ - id: 100
+ description: Web server NAT
+ protocol: tcp
+ log: true
+ inbound_interface:
+ name: eth2
+ destination:
+ address: 198.51.100.10
+ port: "80"
+ translation:
+ address: 192.168.1.10
+ port: "8080"
+ - id: 101
+ description: New DNAT rule
+ protocol: tcp
+ destination:
+ address: 198.51.100.11
+ port: "443"
+ translation:
+ address: 192.168.1.11
+ port: "8443"
+ source:
+ rule:
+ - id: 200
+ description: Outbound NAT
+ protocol: tcp
+ log: true
+ disable: true
+ exclude: true
+ destination:
+ address: 192.168.10.100
+ port: "8083"
+ translation:
+ address: masquerade
+ port: "443"
+
+replaced:
+ commands:
+ - delete nat destination rule 100
+ - set nat destination rule 100 description 'Replaced DNAT rule'
+ - set nat destination rule 100 protocol udp
+ - set nat destination rule 100 destination address 198.51.100.10
+ - set nat destination rule 100 destination port 53
+ - set nat destination rule 100 translation address 192.168.1.53
+ - set nat destination rule 100 translation port 53
+ after:
+ nat:
+ destination:
+ rule:
+ - id: 100
+ description: Replaced DNAT rule
+ protocol: udp
+ destination:
+ address: 198.51.100.10
+ port: "53"
+ translation:
+ address: 192.168.1.53
+ port: "53"
+ source:
+ rule:
+ - id: 200
+ description: Outbound NAT
+ protocol: tcp
+ log: true
+ disable: true
+ exclude: true
+ destination:
+ address: 192.168.10.100
+ port: "8083"
+ translation:
+ address: masquerade
+ port: "443"
+
+overridden:
+ commands:
+ - delete nat source
+ - delete nat destination rule 100
+ - set nat destination rule 100 description 'Overridden DNAT rule'
+ - set nat destination rule 100 protocol tcp
+ - set nat destination rule 100 destination address 198.51.100.20
+ - set nat destination rule 100 destination port 443
+ - set nat destination rule 100 translation address 192.168.1.20
+ - set nat destination rule 100 translation port 8443
+ after:
+ nat:
+ destination:
+ rule:
+ - id: 100
+ description: Overridden DNAT rule
+ protocol: tcp
+ destination:
+ address: 198.51.100.20
+ port: "443"
+ translation:
+ address: 192.168.1.20
+ port: "8443"
+
+deleted:
+ commands:
+ - delete nat
+ after: {}
+
+rendered:
+ commands:
+ - set nat destination rule 100 description 'Rendered rule'
+ - set nat destination rule 100 protocol tcp
+ - set nat destination rule 100 destination address 198.51.100.10
+ - set nat destination rule 100 destination port 80
+ - set nat destination rule 100 translation address 192.168.1.10
+ - set nat destination rule 100 translation port 8080
diff --git a/tests/sanity/ignore-2.15.txt b/tests/sanity/ignore-2.15.txt
index 255b0a87..ee3d2aac 100644
--- a/tests/sanity/ignore-2.15.txt
+++ b/tests/sanity/ignore-2.15.txt
@@ -21,6 +21,7 @@ plugins/action/lldp_global.py action-plugin-docs
plugins/action/lldp_interface.py action-plugin-docs
plugins/action/lldp_interfaces.py action-plugin-docs
plugins/action/logging_global.py action-plugin-docs
+plugins/action/nat.py action-plugin-docs
plugins/action/ntp_global.py action-plugin-docs
plugins/action/ospf_interfaces.py action-plugin-docs
plugins/action/ospfv2.py action-plugin-docs
diff --git a/tests/sanity/ignore-2.16.txt b/tests/sanity/ignore-2.16.txt
index 255b0a87..ee3d2aac 100644
--- a/tests/sanity/ignore-2.16.txt
+++ b/tests/sanity/ignore-2.16.txt
@@ -21,6 +21,7 @@ plugins/action/lldp_global.py action-plugin-docs
plugins/action/lldp_interface.py action-plugin-docs
plugins/action/lldp_interfaces.py action-plugin-docs
plugins/action/logging_global.py action-plugin-docs
+plugins/action/nat.py action-plugin-docs
plugins/action/ntp_global.py action-plugin-docs
plugins/action/ospf_interfaces.py action-plugin-docs
plugins/action/ospfv2.py action-plugin-docs
diff --git a/tests/sanity/ignore-2.17.txt b/tests/sanity/ignore-2.17.txt
index 255b0a87..ee3d2aac 100644
--- a/tests/sanity/ignore-2.17.txt
+++ b/tests/sanity/ignore-2.17.txt
@@ -21,6 +21,7 @@ plugins/action/lldp_global.py action-plugin-docs
plugins/action/lldp_interface.py action-plugin-docs
plugins/action/lldp_interfaces.py action-plugin-docs
plugins/action/logging_global.py action-plugin-docs
+plugins/action/nat.py action-plugin-docs
plugins/action/ntp_global.py action-plugin-docs
plugins/action/ospf_interfaces.py action-plugin-docs
plugins/action/ospfv2.py action-plugin-docs
diff --git a/tests/sanity/ignore-2.18.txt b/tests/sanity/ignore-2.18.txt
index 255b0a87..ee3d2aac 100644
--- a/tests/sanity/ignore-2.18.txt
+++ b/tests/sanity/ignore-2.18.txt
@@ -21,6 +21,7 @@ plugins/action/lldp_global.py action-plugin-docs
plugins/action/lldp_interface.py action-plugin-docs
plugins/action/lldp_interfaces.py action-plugin-docs
plugins/action/logging_global.py action-plugin-docs
+plugins/action/nat.py action-plugin-docs
plugins/action/ntp_global.py action-plugin-docs
plugins/action/ospf_interfaces.py action-plugin-docs
plugins/action/ospfv2.py action-plugin-docs
diff --git a/tests/sanity/ignore-2.19.txt b/tests/sanity/ignore-2.19.txt
index 255b0a87..ee3d2aac 100644
--- a/tests/sanity/ignore-2.19.txt
+++ b/tests/sanity/ignore-2.19.txt
@@ -21,6 +21,7 @@ plugins/action/lldp_global.py action-plugin-docs
plugins/action/lldp_interface.py action-plugin-docs
plugins/action/lldp_interfaces.py action-plugin-docs
plugins/action/logging_global.py action-plugin-docs
+plugins/action/nat.py action-plugin-docs
plugins/action/ntp_global.py action-plugin-docs
plugins/action/ospf_interfaces.py action-plugin-docs
plugins/action/ospfv2.py action-plugin-docs
diff --git a/tests/sanity/ignore-2.20.txt b/tests/sanity/ignore-2.20.txt
index 255b0a87..a96f7e91 100644
--- a/tests/sanity/ignore-2.20.txt
+++ b/tests/sanity/ignore-2.20.txt
@@ -20,6 +20,7 @@ plugins/action/lldp.py action-plugin-docs
plugins/action/lldp_global.py action-plugin-docs
plugins/action/lldp_interface.py action-plugin-docs
plugins/action/lldp_interfaces.py action-plugin-docs
+plugins/action/nat.py action-plugin-docs
plugins/action/logging_global.py action-plugin-docs
plugins/action/ntp_global.py action-plugin-docs
plugins/action/ospf_interfaces.py action-plugin-docs
diff --git a/tests/sanity/ignore-2.21.txt b/tests/sanity/ignore-2.21.txt
index 255b0a87..ee3d2aac 100644
--- a/tests/sanity/ignore-2.21.txt
+++ b/tests/sanity/ignore-2.21.txt
@@ -21,6 +21,7 @@ plugins/action/lldp_global.py action-plugin-docs
plugins/action/lldp_interface.py action-plugin-docs
plugins/action/lldp_interfaces.py action-plugin-docs
plugins/action/logging_global.py action-plugin-docs
+plugins/action/nat.py action-plugin-docs
plugins/action/ntp_global.py action-plugin-docs
plugins/action/ospf_interfaces.py action-plugin-docs
plugins/action/ospfv2.py action-plugin-docs
diff --git a/tests/sanity/ignore-2.22.txt b/tests/sanity/ignore-2.22.txt
index 255b0a87..ee3d2aac 100644
--- a/tests/sanity/ignore-2.22.txt
+++ b/tests/sanity/ignore-2.22.txt
@@ -21,6 +21,7 @@ plugins/action/lldp_global.py action-plugin-docs
plugins/action/lldp_interface.py action-plugin-docs
plugins/action/lldp_interfaces.py action-plugin-docs
plugins/action/logging_global.py action-plugin-docs
+plugins/action/nat.py action-plugin-docs
plugins/action/ntp_global.py action-plugin-docs
plugins/action/ospf_interfaces.py action-plugin-docs
plugins/action/ospfv2.py action-plugin-docs
diff --git a/tests/unit/modules/network/vyos/fixtures/vyos_nat_config.cfg b/tests/unit/modules/network/vyos/fixtures/vyos_nat_config.cfg
new file mode 100644
index 00000000..505f5e57
--- /dev/null
+++ b/tests/unit/modules/network/vyos/fixtures/vyos_nat_config.cfg
@@ -0,0 +1,61 @@
+set nat cgnat log-allocation
+set nat cgnat pool external ext-pool-1 external-port-range '10000-20000'
+set nat cgnat pool external ext-pool-1 per-user-limit port '200'
+set nat cgnat pool external ext-pool-1 range 203.0.113.0/24
+set nat cgnat pool external ext-pool-1 range 203.1.113.1-203.1.113.60 seq '10'
+set nat cgnat pool internal int-pool-1 range '10.0.0.0/24'
+set nat cgnat pool internal int-pool-1 range '10.1.0.0/24'
+set nat cgnat rule 1 source pool 'int-pool-1'
+set nat cgnat rule 1 translation pool 'ext-pool-1'
+set nat destination rule 100 description 'Web server NAT'
+set nat destination rule 100 destination address '198.51.100.10'
+set nat destination rule 100 destination port '80'
+set nat destination rule 100 inbound-interface name 'eth2'
+set nat destination rule 100 log
+set nat destination rule 100 protocol 'tcp'
+set nat destination rule 100 translation address '192.168.1.10'
+set nat destination rule 100 translation options address-mapping 'persistent'
+set nat destination rule 100 translation options port-mapping 'random'
+set nat destination rule 100 translation port '8080'
+set nat destination rule 105 protocol tcp
+set nat destination rule 105 destination port '80'
+set nat destination rule 105 inbound-interface name 'eth0'
+set nat destination rule 105 load-balance hash 'source-address'
+set nat destination rule 105 load-balance hash 'destination-address'
+set nat destination rule 105 load-balance backend 10.10.10.1 weight '60'
+set nat destination rule 105 load-balance backend 10.10.10.2 weight '40'
+set nat source rule 200 description 'Outbound NAT'
+set nat source rule 200 destination address '192.168.10.100'
+set nat source rule 200 destination port '8083'
+set nat source rule 200 disable
+set nat source rule 200 exclude
+set nat source rule 200 log
+set nat source rule 200 protocol 'tcp'
+set nat source rule 200 translation address 'masquerade'
+set nat source rule 200 translation port '443'
+set nat static rule 300 description 'Static mapping'
+set nat static rule 300 destination address '192.168.100.20'
+set nat static rule 300 inbound-interface 'eth2'
+set nat static rule 300 log
+set nat static rule 300 translation address '192.168.1.20'
+set nat64 source rule 10 description 'NAT64 example'
+set nat64 source rule 10 disable
+set nat64 source rule 10 match mark '100'
+set nat64 source rule 10 source prefix '2001:db8::/96'
+set nat64 source rule 10 translation pool 1 address '192.168.100.10'
+set nat64 source rule 10 translation pool 1 description 'NAT64 translation pool'
+set nat64 source rule 10 translation pool 1 disable
+set nat64 source rule 10 translation pool 1 port '1-65535'
+set nat64 source rule 10 translation pool 1 protocol udp
+set nat66 destination rule 20 description 'NAT66 DNAT'
+set nat66 destination rule 20 destination address '2001:db8::1'
+set nat66 destination rule 20 inbound-interface name 'eth1'
+set nat66 destination rule 20 protocol 'tcp'
+set nat66 destination rule 20 translation address '2001:db8:1::10'
+set nat66 destination rule 20 translation port '8443'
+set nat66 source rule 30 description 'NAT66 SNAT'
+set nat66 source rule 30 destination prefix '2001:db8::/96'
+set nat66 source rule 30 outbound-interface name 'eth2'
+set nat66 source rule 30 protocol 'tcp'
+set nat66 source rule 30 source prefix '2001:db8:2::/64'
+set nat66 source rule 30 translation address 'masquerade'
diff --git a/tests/unit/modules/network/vyos/test_vyos_nat.py b/tests/unit/modules/network/vyos/test_vyos_nat.py
new file mode 100644
index 00000000..4ef76305
--- /dev/null
+++ b/tests/unit/modules/network/vyos/test_vyos_nat.py
@@ -0,0 +1,839 @@
+# (c) 2024 Red Hat Inc.
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+from unittest.mock import patch
+
+from ansible_collections.vyos.vyos.plugins.modules import vyos_nat
+from ansible_collections.vyos.vyos.tests.unit.modules.utils import set_module_args
+
+from .vyos_module import TestVyosModule, load_fixture
+
+
+class TestVyosNatModule(TestVyosModule):
+ module = vyos_nat
+
+ def setUp(self):
+ super(TestVyosNatModule, self).setUp()
+
+ self.mock_get_resource_connection_config = patch(
+ "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module_base.get_resource_connection",
+ )
+ self.get_resource_connection_config = self.mock_get_resource_connection_config.start()
+
+ self.mock_get_resource_connection_facts = patch(
+ "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.facts.facts.get_resource_connection",
+ )
+ self.get_resource_connection_facts = self.mock_get_resource_connection_facts.start()
+
+ self.mock_execute_show_command = patch(
+ "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.nat.nat.NatFacts.get_config",
+ )
+ self.execute_show_command = self.mock_execute_show_command.start()
+ self.maxDiff = None
+
+ def tearDown(self):
+ super(TestVyosNatModule, self).tearDown()
+ self.mock_get_resource_connection_config.stop()
+ self.mock_get_resource_connection_facts.stop()
+ self.mock_execute_show_command.stop()
+
+ def load_fixtures(self, commands=None, filename=None):
+ if filename is None:
+ filename = "vyos_nat_config.cfg"
+
+ def load_from_file(*args, **kwargs):
+ return load_fixture(filename)
+
+ self.execute_show_command.side_effect = load_from_file
+
+ # -------------------------------------------------------------------------
+ # merged
+ # -------------------------------------------------------------------------
+
+ def test_vyos_nat_merged_idempotent(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ cgnat=dict(
+ log_allocation=True,
+ pool=dict(
+ external=[
+ dict(
+ name="ext-pool-1",
+ external_port_range="10000-20000",
+ per_user_limit=dict(port="200"),
+ range=[
+ dict(value="203.0.113.0/24"),
+ dict(value="203.1.113.1-203.1.113.60", seq="10"),
+ ],
+ ),
+ ],
+ internal=[
+ dict(
+ name="int-pool-1",
+ range=["10.0.0.0/24", "10.1.0.0/24"],
+ ),
+ ],
+ ),
+ rule=[
+ dict(
+ id=1,
+ source=dict(pool="int-pool-1"),
+ translation=dict(pool="ext-pool-1"),
+ ),
+ ],
+ ),
+ destination=dict(
+ rule=[
+ dict(
+ id=100,
+ description="Web server NAT",
+ protocol="tcp",
+ log=True,
+ inbound_interface=dict(name="eth2"),
+ destination=dict(address="198.51.100.10", port="80"),
+ translation=dict(
+ address="192.168.1.10",
+ port="8080",
+ address_mapping="persistent",
+ port_mapping="random",
+ ),
+ ),
+ dict(
+ id=105,
+ protocol="tcp",
+ destination=dict(port="80"),
+ inbound_interface=dict(name="eth0"),
+ load_balance=dict(
+ hash=["source-address", "destination-address"],
+ backend=[
+ dict(ip="10.10.10.1", weight=60),
+ dict(ip="10.10.10.2", weight=40),
+ ],
+ ),
+ ),
+ ],
+ ),
+ source=dict(
+ rule=[
+ dict(
+ id=200,
+ description="Outbound NAT",
+ protocol="tcp",
+ log=True,
+ exclude=True,
+ disable=True,
+ destination=dict(address="192.168.10.100", port="8083"),
+ translation=dict(address="masquerade", port="443"),
+ ),
+ ],
+ ),
+ static=dict(
+ rule=[
+ dict(
+ id=300,
+ description="Static mapping",
+ inbound_interface="eth2",
+ destination=dict(address="192.168.100.20"),
+ translation=dict(address="192.168.1.20"),
+ log=True,
+ ),
+ ],
+ ),
+ ),
+ nat64=dict(
+ source=dict(
+ rule=[
+ dict(
+ id=10,
+ description="NAT64 example",
+ disable=True,
+ match=dict(mark=100),
+ source=dict(prefix="2001:db8::/96"),
+ translation=dict(
+ pool=[
+ dict(
+ id=1,
+ address="192.168.100.10",
+ description="NAT64 translation pool",
+ disable=True,
+ port="1-65535",
+ protocol="udp",
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ nat66=dict(
+ destination=dict(
+ rule=[
+ dict(
+ id=20,
+ description="NAT66 DNAT",
+ protocol="tcp",
+ inbound_interface=dict(name="eth1"),
+ destination=dict(address="2001:db8::1"),
+ translation=dict(address="2001:db8:1::10", port="8443"),
+ ),
+ ],
+ ),
+ source=dict(
+ rule=[
+ dict(
+ id=30,
+ description="NAT66 SNAT",
+ protocol="tcp",
+ destination=dict(prefix="2001:db8::/96"),
+ outbound_interface=dict(name="eth2"),
+ source=dict(prefix="2001:db8:2::/64"),
+ translation=dict(address="masquerade"),
+ ),
+ ],
+ ),
+ ),
+ ),
+ state="merged",
+ ),
+ )
+ self.execute_module(changed=False, commands=[])
+
+ def test_vyos_nat_merged_new_rule(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ destination=dict(
+ rule=[
+ dict(
+ id=101,
+ description="New DNAT rule",
+ protocol="tcp",
+ destination=dict(address="198.51.100.11", port="443"),
+ translation=dict(address="192.168.1.11", port="8443"),
+ ),
+ ],
+ ),
+ ),
+ ),
+ state="merged",
+ ),
+ )
+ commands = [
+ "set nat destination rule 101 description 'New DNAT rule'",
+ "set nat destination rule 101 protocol tcp",
+ "set nat destination rule 101 destination address 198.51.100.11",
+ "set nat destination rule 101 destination port 443",
+ "set nat destination rule 101 translation address 192.168.1.11",
+ "set nat destination rule 101 translation port 8443",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_nat_merged_update_existing_rule(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ destination=dict(
+ rule=[
+ dict(
+ id=100,
+ description="Updated DNAT rule",
+ protocol="tcp",
+ inbound_interface=dict(name="eth2"),
+ destination=dict(address="198.51.100.10", port="80"),
+ translation=dict(
+ address="192.168.1.10",
+ port="8080",
+ address_mapping="persistent",
+ port_mapping="random",
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ state="merged",
+ ),
+ )
+ commands = [
+ "set nat destination rule 100 description 'Updated DNAT rule'",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_nat_merged_cgnat_new_pool(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ cgnat=dict(
+ pool=dict(
+ external=[
+ dict(
+ name="ext-pool-2",
+ external_port_range="30000-40000",
+ range=[dict(value="203.0.114.0/24")],
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ state="merged",
+ ),
+ )
+ commands = [
+ "set nat cgnat pool external ext-pool-2 external-port-range 30000-40000",
+ "set nat cgnat pool external ext-pool-2 range 203.0.114.0/24",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_nat_merged_nat66_new_rule(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat66=dict(
+ source=dict(
+ rule=[
+ dict(
+ id=31,
+ description="New NAT66 SNAT",
+ protocol="udp",
+ outbound_interface=dict(name="eth3"),
+ source=dict(prefix="2001:db8:3::/64"),
+ translation=dict(address="masquerade"),
+ ),
+ ],
+ ),
+ ),
+ ),
+ state="merged",
+ ),
+ )
+ commands = [
+ "set nat66 source rule 31 description 'New NAT66 SNAT'",
+ "set nat66 source rule 31 protocol udp",
+ "set nat66 source rule 31 outbound-interface name eth3",
+ "set nat66 source rule 31 source prefix 2001:db8:3::/64",
+ "set nat66 source rule 31 translation address masquerade",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_nat_merged_static_inbound_interface_change(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ static=dict(
+ rule=[
+ dict(
+ id=300,
+ description="Static mapping",
+ inbound_interface="eth3",
+ destination=dict(address="192.168.100.20"),
+ translation=dict(address="192.168.1.20"),
+ log=True,
+ ),
+ ],
+ ),
+ ),
+ ),
+ state="merged",
+ ),
+ )
+ commands = ["set nat static rule 300 inbound-interface eth3"]
+ self.execute_module(changed=True, commands=commands)
+
+ # -------------------------------------------------------------------------
+ # deleted
+ # -------------------------------------------------------------------------
+
+ def test_vyos_nat_deleted_all(self):
+ set_module_args(dict(state="deleted"))
+ commands = [
+ "delete nat",
+ "delete nat64",
+ "delete nat66",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_nat_deleted_specific_rules(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ destination=dict(rule=[dict(id=100)]),
+ source=dict(rule=[dict(id=200)]),
+ ),
+ ),
+ state="deleted",
+ ),
+ )
+ commands = [
+ "delete nat destination rule 100",
+ "delete nat source rule 200",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_nat_deleted_cgnat_pool(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ cgnat=dict(
+ pool=dict(
+ external=[dict(name="ext-pool-1")],
+ internal=[dict(name="int-pool-1")],
+ ),
+ ),
+ ),
+ ),
+ state="deleted",
+ ),
+ )
+ commands = [
+ "delete nat cgnat pool external ext-pool-1",
+ "delete nat cgnat pool internal int-pool-1",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_nat_deleted_nat64_rule(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat64=dict(
+ source=dict(rule=[dict(id=10)]),
+ ),
+ ),
+ state="deleted",
+ ),
+ )
+ commands = ["delete nat64 source rule 10"]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_nat_deleted_nonexistent_rule(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ destination=dict(rule=[dict(id=999)]),
+ ),
+ ),
+ state="deleted",
+ ),
+ )
+ self.execute_module(changed=False, commands=[])
+
+ def test_vyos_nat_deleted_cgnat_rule_full_match(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ cgnat=dict(
+ rule=[
+ dict(
+ id=1,
+ source=dict(pool="int-pool-1"),
+ translation=dict(pool="ext-pool-1"),
+ ),
+ ],
+ ),
+ ),
+ ),
+ state="deleted",
+ ),
+ )
+ commands = ["delete nat cgnat rule 1"]
+ self.execute_module(changed=True, commands=commands)
+
+ # -------------------------------------------------------------------------
+ # replaced
+ # -------------------------------------------------------------------------
+
+ def test_vyos_nat_replaced_idempotent(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ destination=dict(
+ rule=[
+ dict(
+ id=100,
+ description="Web server NAT",
+ protocol="tcp",
+ log=True,
+ inbound_interface=dict(name="eth2"),
+ destination=dict(address="198.51.100.10", port="80"),
+ translation=dict(
+ address="192.168.1.10",
+ port="8080",
+ address_mapping="persistent",
+ port_mapping="random",
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ state="replaced",
+ ),
+ )
+ self.execute_module(changed=False, commands=[])
+
+ def test_vyos_nat_replaced_rule(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ destination=dict(
+ rule=[
+ dict(
+ id=100,
+ description="Replaced DNAT rule",
+ protocol="udp",
+ destination=dict(address="198.51.100.10", port="53"),
+ translation=dict(address="192.168.1.53", port="53"),
+ ),
+ ],
+ ),
+ ),
+ ),
+ state="replaced",
+ ),
+ )
+ commands = [
+ "delete nat destination rule 100",
+ "set nat destination rule 100 description 'Replaced DNAT rule'",
+ "set nat destination rule 100 protocol udp",
+ "set nat destination rule 100 destination address 198.51.100.10",
+ "set nat destination rule 100 destination port 53",
+ "set nat destination rule 100 translation address 192.168.1.53",
+ "set nat destination rule 100 translation port 53",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_nat_replaced_cgnat_pool_partial_field_change(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ cgnat=dict(
+ pool=dict(
+ external=[
+ dict(
+ name="ext-pool-1",
+ external_port_range="30000-40000",
+ per_user_limit=dict(port="200"),
+ range=[
+ dict(value="203.0.113.0/24"),
+ dict(value="203.1.113.1-203.1.113.60", seq="10"),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ state="replaced",
+ ),
+ )
+ commands = [
+ "delete nat cgnat pool external ext-pool-1",
+ "delete nat cgnat log-allocation",
+ "set nat cgnat pool external ext-pool-1 external-port-range 30000-40000",
+ "set nat cgnat pool external ext-pool-1 per-user-limit port 200",
+ "set nat cgnat pool external ext-pool-1 range 203.0.113.0/24",
+ "set nat cgnat pool external ext-pool-1 range 203.1.113.1-203.1.113.60 seq 10",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ # -------------------------------------------------------------------------
+ # overridden
+ # -------------------------------------------------------------------------
+
+ def test_vyos_nat_overridden_remove_sections(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ destination=dict(
+ rule=[
+ dict(
+ id=100,
+ description="Overridden web server NAT", # changed
+ protocol="tcp",
+ inbound_interface=dict(name="eth3"), # changed
+ destination=dict(address="198.51.100.10", port="80"),
+ translation=dict(
+ address="192.168.1.10",
+ port="8080",
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ state="overridden",
+ ),
+ )
+ commands = [
+ "delete nat cgnat",
+ "delete nat source",
+ "delete nat static",
+ "delete nat64",
+ "delete nat66",
+ "delete nat destination rule 100",
+ "delete nat destination rule 105",
+ "set nat destination rule 100 description 'Overridden web server NAT'",
+ "set nat destination rule 100 protocol tcp",
+ "set nat destination rule 100 inbound-interface name eth3",
+ "set nat destination rule 100 destination address 198.51.100.10",
+ "set nat destination rule 100 destination port 80",
+ "set nat destination rule 100 translation address 192.168.1.10",
+ "set nat destination rule 100 translation port 8080",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ # -------------------------------------------------------------------------
+ # rendered
+ # -------------------------------------------------------------------------
+
+ def test_vyos_nat_rendered(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ destination=dict(
+ rule=[
+ dict(
+ id=100,
+ description="Rendered rule",
+ protocol="tcp",
+ destination=dict(address="198.51.100.10", port="80"),
+ translation=dict(address="192.168.1.10", port="8080"),
+ ),
+ ],
+ ),
+ ),
+ ),
+ state="rendered",
+ ),
+ )
+ rendered_cmds = [
+ "set nat destination rule 100 description 'Rendered rule'",
+ "set nat destination rule 100 protocol tcp",
+ "set nat destination rule 100 destination address 198.51.100.10",
+ "set nat destination rule 100 destination port 80",
+ "set nat destination rule 100 translation address 192.168.1.10",
+ "set nat destination rule 100 translation port 8080",
+ ]
+ result = self.execute_module(changed=False)
+ self.assertEqual(sorted(result["rendered"]), sorted(rendered_cmds), result["rendered"])
+
+ # -------------------------------------------------------------------------
+ # parsed
+ # -------------------------------------------------------------------------
+
+ def test_vyos_nat_parsed(self):
+ parsed_str = (
+ "set nat destination rule 100 description 'Web server NAT'\n"
+ "set nat destination rule 100 destination address '198.51.100.10'\n"
+ "set nat destination rule 100 destination port '80'\n"
+ "set nat destination rule 100 inbound-interface name 'eth2'\n"
+ "set nat destination rule 100 log\n"
+ "set nat destination rule 100 protocol 'tcp'\n"
+ "set nat destination rule 100 translation address '192.168.1.10'\n"
+ "set nat destination rule 100 translation port '8080'"
+ )
+ set_module_args(dict(running_config=parsed_str, state="parsed"))
+ result = self.execute_module(changed=False)
+ parsed_list = {
+ "nat": {
+ "destination": {
+ "rule": [
+ {
+ "id": 100,
+ "description": "Web server NAT",
+ "protocol": "tcp",
+ "log": True,
+ "inbound_interface": {"name": "eth2"},
+ "destination": {"address": "198.51.100.10", "port": "80"},
+ "translation": {"address": "192.168.1.10", "port": "8080"},
+ },
+ ],
+ },
+ },
+ }
+ self.assertEqual(parsed_list, result["parsed"])
+
+ # -------------------------------------------------------------------------
+ # gathered
+ # -------------------------------------------------------------------------
+
+ def test_vyos_nat_gathered(self):
+ set_module_args(dict(state="gathered"))
+ result = self.execute_module(changed=False)
+ gathered = result["gathered"]
+ self.assertIn("nat", gathered)
+ self.assertIn("nat64", gathered)
+ self.assertIn("nat66", gathered)
+ self.assertEqual(gathered["nat"]["destination"]["rule"][0]["id"], 100)
+ self.assertEqual(gathered["nat64"]["source"]["rule"][0]["id"], 10)
+ self.assertEqual(gathered["nat66"]["destination"]["rule"][0]["id"], 20)
+
+ def test_vyos_nat_merged_load_balance_new_rule(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ destination=dict(
+ rule=[
+ dict(
+ id=110,
+ protocol="tcp",
+ destination=dict(port="443"),
+ load_balance=dict(
+ hash=["random"],
+ backend=[
+ dict(ip="10.20.20.1", weight=100),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ state="merged",
+ ),
+ )
+ commands = [
+ "set nat destination rule 110 protocol tcp",
+ "set nat destination rule 110 destination port 443",
+ "set nat destination rule 110 load-balance hash random",
+ "set nat destination rule 110 load-balance backend 10.20.20.1 weight 100",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_nat_merged_load_balance_hash_add(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ destination=dict(
+ rule=[
+ dict(
+ id=105,
+ protocol="tcp",
+ destination=dict(port="80"),
+ inbound_interface=dict(name="eth0"),
+ load_balance=dict(
+ hash=[
+ "source-address",
+ "destination-address",
+ "source-port",
+ ],
+ backend=[
+ dict(ip="10.10.10.1", weight=60),
+ dict(ip="10.10.10.2", weight=40),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ state="merged",
+ ),
+ )
+ commands = ["set nat destination rule 105 load-balance hash source-port"]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_nat_merged_load_balance_backend_weight_change(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ destination=dict(
+ rule=[
+ dict(
+ id=105,
+ protocol="tcp",
+ destination=dict(port="80"),
+ inbound_interface=dict(name="eth0"),
+ load_balance=dict(
+ hash=["source-address", "destination-address"],
+ backend=[
+ dict(ip="10.10.10.1", weight=70),
+ dict(ip="10.10.10.2", weight=30),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ state="merged",
+ ),
+ )
+ commands = [
+ "set nat destination rule 105 load-balance backend 10.10.10.1 weight 70",
+ "set nat destination rule 105 load-balance backend 10.10.10.2 weight 30",
+ ]
+ self.execute_module(changed=True, commands=commands)
+
+ def test_vyos_nat_replaced_load_balance_removes_omitted(self):
+ set_module_args(
+ dict(
+ config=dict(
+ nat=dict(
+ destination=dict(
+ rule=[
+ dict(
+ id=105,
+ protocol="tcp",
+ destination=dict(port="80"),
+ inbound_interface=dict(name="eth0"),
+ load_balance=dict(
+ hash=["source-address"],
+ backend=[
+ dict(ip="10.10.10.1", weight=100),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ state="replaced",
+ ),
+ )
+ commands = [
+ "delete nat destination rule 105",
+ "set nat destination rule 105 protocol tcp",
+ "set nat destination rule 105 destination port 80",
+ "set nat destination rule 105 inbound-interface name eth0",
+ "set nat destination rule 105 load-balance hash source-address",
+ "set nat destination rule 105 load-balance backend 10.10.10.1 weight 100",
+ ]
+ self.execute_module(changed=True, commands=commands)