diff options
| -rw-r--r-- | data/templates/firewall/nftables-nat66.j2 | 41 | ||||
| -rw-r--r-- | interface-definitions/include/nat/protocol.xml.i | 34 | ||||
| -rw-r--r-- | interface-definitions/nat66.xml.in | 8 | ||||
| -rw-r--r-- | op-mode-definitions/nat.xml.in | 4 | ||||
| -rw-r--r-- | op-mode-definitions/nat66.xml.in | 4 | ||||
| -rw-r--r-- | python/vyos/ifconfig/pppoe.py | 37 | ||||
| -rw-r--r-- | smoketest/configs/service-https | 55 | ||||
| -rw-r--r-- | smoketest/configs/vpn-openconnect-sstp (renamed from smoketest/configs/pki-misc) | 13 | ||||
| -rwxr-xr-x | smoketest/scripts/cli/test_nat66.py | 48 | ||||
| -rwxr-xr-x | src/conf_mode/service_monitoring_telegraf.py | 20 | ||||
| -rwxr-xr-x | src/etc/opennhrp/opennhrp-script.py | 15 | ||||
| -rwxr-xr-x | src/etc/telegraf/custom_scripts/show_interfaces_input_filter.py | 16 | ||||
| -rwxr-xr-x | src/op_mode/nat.py | 101 | 
13 files changed, 342 insertions, 54 deletions
| diff --git a/data/templates/firewall/nftables-nat66.j2 b/data/templates/firewall/nftables-nat66.j2 index 2fe04b4ff..28714c7a7 100644 --- a/data/templates/firewall/nftables-nat66.j2 +++ b/data/templates/firewall/nftables-nat66.j2 @@ -7,6 +7,17 @@  {% set src_prefix  = 'ip6 saddr ' ~ config.source.prefix.replace('!','!= ') if config.source.prefix is vyos_defined %}  {% set source_address  = 'ip6 saddr ' ~ config.source.address.replace('!','!= ') if config.source.address is vyos_defined %}  {% set dest_address  = 'ip6 daddr ' ~ config.destination.address.replace('!','!= ') if config.destination.address is vyos_defined %} +{# Port #} +{% if config.source.port is vyos_defined and config.source.port.startswith('!') %} +{%     set src_port  = 'sport != { ' ~ config.source.port.replace('!','') ~ ' }' %} +{% else %} +{%     set src_port  = 'sport { ' ~ config.source.port ~ ' }' if config.source.port is vyos_defined %} +{% endif %} +{% if config.destination.port is vyos_defined and config.destination.port.startswith('!') %} +{%     set dst_port  = 'dport != { ' ~ config.destination.port.replace('!','') ~ ' }' %} +{% else %} +{%     set dst_port  = 'dport { ' ~ config.destination.port ~ ' }' if config.destination.port is vyos_defined %} +{% endif %}  {% if chain is vyos_defined('PREROUTING') %}  {%     set comment   = 'DST-NAT66-' ~ rule %}  {%     set base_log  = '[NAT66-DST-' ~ rule %} @@ -36,6 +47,14 @@  {%     endif   %}  {%     set interface = ' oifname "' ~ config.outbound_interface ~ '"' if config.outbound_interface is vyos_defined else '' %}  {% endif %} +{% set trns_port = ':' ~ config.translation.port if config.translation.port is vyos_defined %} +{# protocol has a default value thus it is always present #} +{% if config.protocol is vyos_defined('tcp_udp') %} +{%     set protocol  = 'tcp' %} +{%     set comment   = comment ~ ' tcp_udp' %} +{% else %} +{%     set protocol  = config.protocol %} +{% endif %}  {% if config.log is vyos_defined %}  {%     if config.translation.address is vyos_defined('masquerade') %}  {%         set log = base_log ~ '-MASQ]' %} @@ -43,6 +62,11 @@  {%         set log = base_log ~ ']' %}  {%     endif %}  {% endif %} +{% if config.exclude is vyos_defined %} +{#     rule has been marked as 'exclude' thus we simply return here #} +{%     set trns_addr = 'return' %} +{%     set trns_port = '' %} +{% endif %}  {% set output = 'add rule ip6 nat ' ~ chain ~ interface %}  {# Count packets #}  {% set output = output ~ ' counter' %} @@ -54,12 +78,18 @@  {% if src_prefix is vyos_defined %}  {%     set output = output ~ ' ' ~ src_prefix %}  {% endif %} +{% if dst_port is vyos_defined %} +{%     set output = output ~ ' ' ~ protocol ~ ' ' ~ dst_port %} +{% endif %}  {% if dst_prefix is vyos_defined %}  {%     set output = output ~ ' ' ~ dst_prefix %}  {% endif %}  {% if source_address is vyos_defined %}  {%     set output = output ~ ' ' ~ source_address %}  {% endif %} +{% if src_port is vyos_defined %} +{%     set output = output ~ ' ' ~ protocol ~ ' ' ~ src_port %} +{% endif %}  {% if dest_address is vyos_defined %}  {%     set output = output ~ ' ' ~ dest_address %}  {% endif %} @@ -70,11 +100,22 @@  {% if trns_address is vyos_defined %}  {%     set output = output ~ ' ' ~ trns_address %}  {% endif %} +{% if trns_port is vyos_defined %} +{#     Do not add a whitespace here, translation port must be directly added after IP address #} +{#     e.g. 2001:db8::1:3389                                                                   #} +{%     set output = output ~ trns_port %} +{% endif %}  {% if comment is vyos_defined %}  {%     set output = output ~ ' comment "' ~ comment ~ '"' %}  {% endif %}  {{ log_output if log_output is vyos_defined }}  {{ output }} +{# Special handling if protocol is tcp_udp, we must repeat the entire rule with udp as protocol #} +{% if config.protocol is vyos_defined('tcp_udp') %} +{#     Beware of trailing whitespace, without it the comment tcp_udp will be changed to udp_udp #} +{{ log_output | replace('tcp ', 'udp ') if log_output is vyos_defined }} +{{ output | replace('tcp ', 'udp ') }} +{% endif %}  {% endmacro %}  # Start with clean NAT table diff --git a/interface-definitions/include/nat/protocol.xml.i b/interface-definitions/include/nat/protocol.xml.i new file mode 100644 index 000000000..54e7ff00d --- /dev/null +++ b/interface-definitions/include/nat/protocol.xml.i @@ -0,0 +1,34 @@ +<!-- include start from nat/protocol.xml.i --> +<leafNode name="protocol"> +  <properties> +    <help>Protocol to match (protocol name, number, or "all")</help> +    <completionHelp> +      <script>${vyos_completion_dir}/list_protocols.sh</script> +      <list>all tcp_udp</list> +    </completionHelp> +    <valueHelp> +      <format>all</format> +      <description>All IP protocols</description> +    </valueHelp> +    <valueHelp> +      <format>tcp_udp</format> +      <description>Both TCP and UDP</description> +    </valueHelp> +    <valueHelp> +      <format>u32:0-255</format> +      <description>IP protocol number</description> +    </valueHelp> +    <valueHelp> +      <format><protocol></format> +      <description>IP protocol name</description> +    </valueHelp> +    <valueHelp> +      <format>!<protocol></format> +      <description>IP protocol name</description> +    </valueHelp> +    <constraint> +      <validator name="ip-protocol"/> +    </constraint> +  </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/nat66.xml.in b/interface-definitions/nat66.xml.in index bde1a6f8d..dab4543e0 100644 --- a/interface-definitions/nat66.xml.in +++ b/interface-definitions/nat66.xml.in @@ -50,6 +50,7 @@                    </completionHelp>                  </properties>                </leafNode> +              #include <include/nat/protocol.xml.i>                <node name="destination">                  <properties>                    <help>IPv6 destination prefix options</help> @@ -72,6 +73,7 @@                        </constraint>                      </properties>                    </leafNode> +                  #include <include/nat-port.xml.i>                  </children>                </node>                <node name="source"> @@ -96,6 +98,7 @@                        </constraint>                      </properties>                    </leafNode> +                  #include <include/nat-port.xml.i>                  </children>                </node>                <node name="translation"> @@ -128,6 +131,7 @@                        </constraint>                      </properties>                    </leafNode> +                  #include <include/nat-translation-port.xml.i>                  </children>                </node>              </children> @@ -179,6 +183,7 @@                    </completionHelp>                  </properties>                </leafNode> +              #include <include/nat/protocol.xml.i>                <node name="destination">                  <properties>                    <help>IPv6 destination prefix options</help> @@ -211,6 +216,7 @@                        </constraint>                      </properties>                    </leafNode> +                  #include <include/nat-port.xml.i>                  </children>                </node>                <node name="source"> @@ -245,6 +251,7 @@                        </constraint>                      </properties>                    </leafNode> +                #include <include/nat-port.xml.i>                  </children>                </node>                <node name="translation"> @@ -269,6 +276,7 @@                        </constraint>                      </properties>                    </leafNode> +                  #include <include/nat-translation-port.xml.i>                  </children>                </node>              </children> diff --git a/op-mode-definitions/nat.xml.in b/op-mode-definitions/nat.xml.in index dbc06b930..ce0544390 100644 --- a/op-mode-definitions/nat.xml.in +++ b/op-mode-definitions/nat.xml.in @@ -45,7 +45,7 @@                      <command>${vyos_op_scripts_dir}/show_nat_translations.py --type=source --verbose</command>                    </node>                  </children> -                <command>${vyos_op_scripts_dir}/show_nat_translations.py --type=source</command> +                <command>${vyos_op_scripts_dir}/nat.py show_translations --direction source --family inet</command>                </node>              </children>            </node> @@ -87,7 +87,7 @@                      <command>${vyos_op_scripts_dir}/show_nat_translations.py --type=destination --verbose</command>                    </node>                  </children> -                <command>${vyos_op_scripts_dir}/show_nat_translations.py --type=destination</command> +                <command>${vyos_op_scripts_dir}/nat.py show_translations --direction destination --family inet</command>                </node>              </children>            </node> diff --git a/op-mode-definitions/nat66.xml.in b/op-mode-definitions/nat66.xml.in index aba2d6add..25aa04d59 100644 --- a/op-mode-definitions/nat66.xml.in +++ b/op-mode-definitions/nat66.xml.in @@ -45,7 +45,7 @@                      <command>${vyos_op_scripts_dir}/show_nat66_translations.py --type=source --verbose</command>                    </node>                  </children> -                <command>${vyos_op_scripts_dir}/show_nat66_translations.py --type=source</command> +                <command>${vyos_op_scripts_dir}/nat.py show_translations --direction source --family inet6</command>                </node>              </children>            </node> @@ -87,7 +87,7 @@                      <command>${vyos_op_scripts_dir}/show_nat66_translations.py --type=destination --verbose</command>                    </node>                  </children> -                <command>${vyos_op_scripts_dir}/show_nat66_translations.py --type=destination</command> +                <command>${vyos_op_scripts_dir}/nat.py show_translations --direction destination --family inet6</command>                </node>              </children>            </node> diff --git a/python/vyos/ifconfig/pppoe.py b/python/vyos/ifconfig/pppoe.py index 63ffc8069..437fe0cae 100644 --- a/python/vyos/ifconfig/pppoe.py +++ b/python/vyos/ifconfig/pppoe.py @@ -1,4 +1,4 @@ -# Copyright 2020-2021 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2020-2022 VyOS maintainers and contributors <maintainers@vyos.io>  #  # This library is free software; you can redistribute it and/or  # modify it under the terms of the GNU Lesser General Public @@ -14,6 +14,7 @@  # License along with this library.  If not, see <http://www.gnu.org/licenses/>.  from vyos.ifconfig.interface import Interface +from vyos.validate import assert_range  from vyos.util import get_interface_config  @Interface.register @@ -27,6 +28,21 @@ class PPPoEIf(Interface):          },      } +    _sysfs_get = { +        **Interface._sysfs_get,**{ +            'accept_ra_defrtr': { +                'location': '/proc/sys/net/ipv6/conf/{ifname}/accept_ra_defrtr', +            } +        } +    } + +    _sysfs_set = {**Interface._sysfs_set, **{ +        'accept_ra_defrtr': { +            'validate': lambda value: assert_range(value, 0, 2), +            'location': '/proc/sys/net/ipv6/conf/{ifname}/accept_ra_defrtr', +        }, +    }} +      def _remove_routes(self, vrf=None):          # Always delete default routes when interface is removed          vrf_cmd = '' @@ -70,6 +86,21 @@ class PPPoEIf(Interface):          """ Get a synthetic MAC address. """          return self.get_mac_synthetic() +    def set_accept_ra_defrtr(self, enable): +        """ +        Learn default router in Router Advertisement. +        1: enabled +        0: disable + +        Example: +        >>> from vyos.ifconfig import PPPoEIf +        >>> PPPoEIf('pppoe1').set_accept_ra_defrtr(0) +        """ +        tmp = self.get_interface('accept_ra_defrtr') +        if tmp == enable: +            return None +        self.set_interface('accept_ra_defrtr', enable) +      def update(self, config):          """ General helper function which works on a dictionary retrived by          get_config_dict(). It's main intention is to consolidate the scattered @@ -107,6 +138,10 @@ class PPPoEIf(Interface):              tmp = config['vrf']              vrf = f'-c "vrf {tmp}"' +        # learn default router in Router Advertisement. +        tmp = '0' if 'no_default_route' in config else '1' +        self.set_accept_ra_defrtr(tmp) +          if 'no_default_route' not in config:              # Set default route(s) pointing to PPPoE interface              distance = config['default_route_distance'] diff --git a/smoketest/configs/service-https b/smoketest/configs/service-https new file mode 100644 index 000000000..d478d5731 --- /dev/null +++ b/smoketest/configs/service-https @@ -0,0 +1,55 @@ +interfaces { +    ethernet eth0 { +        address 192.168.150.1/24 +    } +} +service { +    https { +        certificates { +            system-generated-certificate { +                lifetime 365 +            } +        } +    } +} +system { +    config-management { +        commit-revisions 100 +    } +    console { +        device ttyS0 { +            speed 115200 +        } +    } +    host-name vyos +    login { +        user vyos { +            authentication { +                encrypted-password $6$2Ta6TWHd/U$NmrX0x9kexCimeOcYK1MfhMpITF9ELxHcaBU/znBq.X2ukQOj61fVI2UYP/xBzP4QtiTcdkgs7WOQMHWsRymO/ +                plaintext-password "" +            } +        } +    } +    ntp { +        server time1.vyos.net { +        } +        server time2.vyos.net { +        } +        server time3.vyos.net { +        } +    } +    syslog { +        global { +            facility all { +                level info +            } +            facility protocols { +                level debug +            } +        } +    } +} + +// Warning: Do not remove the following line. +// vyos-config-version: "bgp@1:broadcast-relay@1:cluster@1:config-management@1:conntrack@2:conntrack-sync@2:dhcp-relay@2:dhcp-server@5:dhcpv6-server@1:dns-forwarding@3:firewall@5:https@2:interfaces@22:ipoe-server@1:ipsec@6:isis@1:l2tp@3:lldp@1:mdns@1:nat@5:nat66@1:ntp@1:policy@1:pppoe-server@5:pptp@2:qos@1:quagga@9:rpki@1:salt@1:snmp@2:ssh@2:sstp@3:system@21:vrf@2:vrrp@2:vyos-accel-ppp@2:wanloadbalance@3:webproxy@2:zone-policy@1" +// Release version: 1.4-rolling-202106290839 diff --git a/smoketest/configs/pki-misc b/smoketest/configs/vpn-openconnect-sstp index 4db795565..45e6dd9b2 100644 --- a/smoketest/configs/pki-misc +++ b/smoketest/configs/vpn-openconnect-sstp @@ -3,15 +3,6 @@ interfaces {          address 192.168.150.1/24      }  } -service { -    https { -        certificates { -            system-generated-certificate { -                lifetime 365 -            } -        } -    } -}  system {      config-management {          commit-revisions 100 @@ -59,10 +50,6 @@ vpn {              }              mode local          } -        listen-ports { -            tcp 4443 -            udp 4443 -        }          network-settings {              client-ip-settings {                  subnet 192.168.160.0/24 diff --git a/smoketest/scripts/cli/test_nat66.py b/smoketest/scripts/cli/test_nat66.py index 4b5625569..c5db066db 100755 --- a/smoketest/scripts/cli/test_nat66.py +++ b/smoketest/scripts/cli/test_nat66.py @@ -131,6 +131,30 @@ class TestNAT66(VyOSUnitTestSHIM.TestCase):          self.verify_nftables(nftables_search, 'ip6 nat') +    def test_destination_nat66_protocol(self): +        translation_address = '2001:db8:1111::1' +        source_prefix = '2001:db8:2222::/64' +        dport = '4545' +        sport = '8080' +        tport = '5555' +        proto = 'tcp' +        self.cli_set(dst_path + ['rule', '1', 'inbound-interface', 'eth1']) +        self.cli_set(dst_path + ['rule', '1', 'destination', 'port', dport]) +        self.cli_set(dst_path + ['rule', '1', 'source', 'address', source_prefix]) +        self.cli_set(dst_path + ['rule', '1', 'source', 'port', sport]) +        self.cli_set(dst_path + ['rule', '1', 'protocol', proto]) +        self.cli_set(dst_path + ['rule', '1', 'translation', 'address', translation_address]) +        self.cli_set(dst_path + ['rule', '1', 'translation', 'port', tport]) + +        # check validate() - outbound-interface must be defined +        self.cli_commit() + +        nftables_search = [ +            ['iifname "eth1"', 'tcp dport { 4545 } ip6 saddr 2001:db8:2222::/64 tcp sport { 8080 } dnat to 2001:db8:1111::1:5555'] +        ] + +        self.verify_nftables(nftables_search, 'ip6 nat') +      def test_destination_nat66_prefix(self):          destination_prefix = 'fc00::/64'          translation_prefix = 'fc01::/64' @@ -176,6 +200,30 @@ class TestNAT66(VyOSUnitTestSHIM.TestCase):          self.cli_set(src_path + ['rule', rule, 'translation', 'address', 'masquerade'])          self.cli_commit() +    def test_source_nat66_protocol(self): +        translation_address = '2001:db8:1111::1' +        source_prefix = '2001:db8:2222::/64' +        dport = '9999' +        sport = '8080' +        tport = '80' +        proto = 'tcp' +        self.cli_set(src_path + ['rule', '1', 'outbound-interface', 'eth1']) +        self.cli_set(src_path + ['rule', '1', 'destination', 'port', dport]) +        self.cli_set(src_path + ['rule', '1', 'source', 'prefix', source_prefix]) +        self.cli_set(src_path + ['rule', '1', 'source', 'port', sport]) +        self.cli_set(src_path + ['rule', '1', 'protocol', proto]) +        self.cli_set(src_path + ['rule', '1', 'translation', 'address', translation_address]) +        self.cli_set(src_path + ['rule', '1', 'translation', 'port', tport]) + +        # check validate() - outbound-interface must be defined +        self.cli_commit() + +        nftables_search = [ +            ['oifname "eth1"', 'ip6 saddr 2001:db8:2222::/64 tcp dport { 9999 } tcp sport { 8080 } snat to 2001:db8:1111::1:80'] +        ] + +        self.verify_nftables(nftables_search, 'ip6 nat') +      def test_nat66_no_rules(self):          # T3206: deleting all rules but keep the direction 'destination' or          # 'source' resulteds in KeyError: 'rule'. diff --git a/src/conf_mode/service_monitoring_telegraf.py b/src/conf_mode/service_monitoring_telegraf.py index 18b32edab..53df006a4 100755 --- a/src/conf_mode/service_monitoring_telegraf.py +++ b/src/conf_mode/service_monitoring_telegraf.py @@ -40,23 +40,6 @@ custom_scripts_dir = '/etc/telegraf/custom_scripts'  syslog_telegraf = '/etc/rsyslog.d/50-telegraf.conf'  systemd_override = '/etc/systemd/system/telegraf.service.d/10-override.conf' -def get_interfaces(type='', vlan=True): -    """ -    get_interfaces() -    ['dum0', 'eth0', 'eth1', 'eth1.5', 'lo', 'tun0'] - -    get_interfaces("dummy") -    ['dum0'] -    """ -    interfaces = [] -    ifaces = Section.interfaces(type) -    for iface in ifaces: -        if vlan == False and '.' in iface: -            continue -        interfaces.append(iface) - -    return interfaces -  def get_nft_filter_chains():      """ Get nft chains for table filter """      nft = cmd('nft --json list table ip filter') @@ -70,7 +53,6 @@ def get_nft_filter_chains():      return chain_list -  def get_config(config=None):      if config:          conf = config @@ -93,7 +75,7 @@ def get_config(config=None):      monitoring = dict_merge(default_values, monitoring)      monitoring['custom_scripts_dir'] = custom_scripts_dir -    monitoring['interfaces_ethernet'] = get_interfaces('ethernet', vlan=False) +    monitoring['interfaces_ethernet'] = Section.interfaces('ethernet', vlan=False)      monitoring['nft_chains'] = get_nft_filter_chains()      # Redefine azure group-metrics 'single-table' and 'table-per-metric' diff --git a/src/etc/opennhrp/opennhrp-script.py b/src/etc/opennhrp/opennhrp-script.py index a5293c97e..bf25a7331 100755 --- a/src/etc/opennhrp/opennhrp-script.py +++ b/src/etc/opennhrp/opennhrp-script.py @@ -81,7 +81,13 @@ def vici_ike_terminate(list_ikeid: list[str]) -> bool:          session = vici.Session()          for ikeid in list_ikeid:              logger.info(f'Terminating IKE SA with id {ikeid}') -            session.terminate({'ike-id': ikeid, 'timeout': '-1'}) +            session_generator = session.terminate( +                {'ike-id': ikeid, 'timeout': '-1'}) +            # a dummy `for` loop is required because of requirements +            # from vici. Without a full iteration on the output, the +            # command to vici may not be executed completely +            for _ in session_generator: +                pass          return True      except Exception as err:          logger.error(f'Failed to terminate SA for IKE ids {list_ikeid}: {err}') @@ -175,13 +181,18 @@ def vici_initiate(conn: str, child_sa: str, src_addr: str,          f'src_addr: {src_addr}, dst_addr: {dest_addr}')      try:          session = vici.Session() -        session.initiate({ +        session_generator = session.initiate({              'ike': conn,              'child': child_sa,              'timeout': '-1',              'my-host': src_addr,              'other-host': dest_addr          }) +        # a dummy `for` loop is required because of requirements +        # from vici. Without a full iteration on the output, the +        # command to vici may not be executed completely +        for _ in session_generator: +            pass          return True      except Exception as err:          logger.error(f'Unable to initiate connection {err}') diff --git a/src/etc/telegraf/custom_scripts/show_interfaces_input_filter.py b/src/etc/telegraf/custom_scripts/show_interfaces_input_filter.py index 0c7474156..6f14d6a8e 100755 --- a/src/etc/telegraf/custom_scripts/show_interfaces_input_filter.py +++ b/src/etc/telegraf/custom_scripts/show_interfaces_input_filter.py @@ -5,20 +5,6 @@ from vyos.ifconfig import Interface  import time -def get_interfaces(type='', vlan=True): -    """ -    Get interfaces: -    ['dum0', 'eth0', 'eth1', 'eth1.5', 'lo', 'tun0'] -    """ -    interfaces = [] -    ifaces = Section.interfaces(type) -    for iface in ifaces: -        if vlan == False and '.' in iface: -            continue -        interfaces.append(iface) - -    return interfaces -  def get_interface_addresses(iface, link_local_v6=False):      """      Get IP and IPv6 addresses from interface in one string @@ -77,7 +63,7 @@ def get_interface_oper_state(iface):      return oper_state -interfaces = get_interfaces() +interfaces = Section.interfaces('')  for iface in interfaces:      print(f'show_interfaces,interface={iface} ' diff --git a/src/op_mode/nat.py b/src/op_mode/nat.py index dec04aa48..1339d5b92 100755 --- a/src/op_mode/nat.py +++ b/src/op_mode/nat.py @@ -17,6 +17,7 @@  import jmespath  import json  import sys +import xmltodict  from sys import exit  from tabulate import tabulate @@ -27,6 +28,29 @@ from vyos.util import dict_search  import vyos.opmode +def _get_xml_translation(direction, family): +    """ +    Get conntrack XML output --src-nat|--dst-nat +    """ +    if direction == 'source': +        opt = '--src-nat' +    if direction == 'destination': +        opt = '--dst-nat' +    return cmd(f'sudo conntrack --dump --family {family} {opt} --output xml') + + +def _xml_to_dict(xml): +    """ +    Convert XML to dictionary +    Return: dictionary +    """ +    parse = xmltodict.parse(xml, attr_prefix='') +    # If only one conntrack entry we must change dict +    if 'meta' in parse['conntrack']['flow']: +        return dict(conntrack={'flow': [parse['conntrack']['flow']]}) +    return parse + +  def _get_json_data(direction, family):      """      Get NAT format JSON @@ -52,6 +76,22 @@ def _get_raw_data_rules(direction, family):      return rules +def _get_raw_translation(direction, family): +    """ +    Return: dictionary +    """ +    xml = _get_xml_translation(direction, family) +    if len(xml) == 0: +        output = {'conntrack': +            { +                'error': True, +                'reason': 'entries not found' +            } +        } +        return output +    return _xml_to_dict(xml) + +  def _get_formatted_output_rules(data, direction, family):      # Add default values before loop      sport, dport, proto = 'any', 'any', 'any' @@ -180,6 +220,58 @@ def _get_formatted_output_statistics(data, direction):      return output +def _get_formatted_translation(dict_data, nat_direction, family): +    data_entries = [] +    if 'error' in dict_data['conntrack']: +        return 'Entries not found' +    for entry in dict_data['conntrack']['flow']: +        orig_src, orig_dst, orig_sport, orig_dport = {}, {}, {}, {} +        reply_src, reply_dst, reply_sport, reply_dport = {}, {}, {}, {} +        proto = {} +        for meta in entry['meta']: +            direction = meta['direction'] +            if direction in ['original']: +                if 'layer3' in meta: +                    orig_src = meta['layer3']['src'] +                    orig_dst = meta['layer3']['dst'] +                if 'layer4' in meta: +                    if meta.get('layer4').get('sport'): +                        orig_sport = meta['layer4']['sport'] +                    if meta.get('layer4').get('dport'): +                        orig_dport = meta['layer4']['dport'] +                    proto = meta['layer4']['protoname'] +            if direction in ['reply']: +                if 'layer3' in meta: +                    reply_src = meta['layer3']['src'] +                    reply_dst = meta['layer3']['dst'] +                if 'layer4' in meta: +                    if meta.get('layer4').get('sport'): +                        reply_sport = meta['layer4']['sport'] +                    if meta.get('layer4').get('dport'): +                        reply_dport = meta['layer4']['dport'] +                    proto = meta['layer4']['protoname'] +            if direction == 'independent': +                conn_id = meta['id'] +                timeout = meta['timeout'] +                orig_src = f'{orig_src}:{orig_sport}' if orig_sport else orig_src +                orig_dst = f'{orig_dst}:{orig_dport}' if orig_dport else orig_dst +                reply_src = f'{reply_src}:{reply_sport}' if reply_sport else reply_src +                reply_dst = f'{reply_dst}:{reply_dport}' if reply_dport else reply_dst +                state = meta['state'] if 'state' in meta else '' +                mark = meta['mark'] +                zone = meta['zone'] if 'zone' in meta else '' +                if nat_direction == 'source': +                    data_entries.append( +                        [orig_src, reply_dst, proto, timeout, mark, zone]) +                elif nat_direction == 'destination': +                    data_entries.append( +                        [orig_dst, reply_src, proto, timeout, mark, zone]) + +    headers = ["Pre-NAT", "Post-NAT", "Proto", "Timeout", "Mark", "Zone"] +    output = tabulate(data_entries, headers, numalign="left") +    return output + +  def show_rules(raw: bool, direction: str, family: str):      nat_rules = _get_raw_data_rules(direction, family)      if raw: @@ -196,6 +288,15 @@ def show_statistics(raw: bool, direction: str, family: str):          return _get_formatted_output_statistics(nat_statistics, direction) +def show_translations(raw: bool, direction: str, family: str): +    family = 'ipv6' if family == 'inet6' else 'ipv4' +    nat_translation = _get_raw_translation(direction, family) +    if raw: +        return nat_translation +    else: +        return _get_formatted_translation(nat_translation, direction, family) + +  if __name__ == '__main__':      try:          res = vyos.opmode.run(sys.modules[__name__]) | 
