diff options
| author | Viacheslav Hletenko <v.gletenko@vyos.io> | 2022-08-13 00:07:41 +0000 | 
|---|---|---|
| committer | Viacheslav Hletenko <v.gletenko@vyos.io> | 2022-08-17 10:53:26 +0000 | 
| commit | 69bcdb9a680b33422d041fd03e70c25094bfa6a2 (patch) | |
| tree | 845bd5bce027e72fd9dfba8f713c1e799bbdf9de | |
| parent | 466e3b192d15563bc21fc308fa7916eb5aae8664 (diff) | |
| download | vyos-1x-69bcdb9a680b33422d041fd03e70c25094bfa6a2.tar.gz vyos-1x-69bcdb9a680b33422d041fd03e70c25094bfa6a2.zip | |
nat: T538: Add static NAT one-to-one
Ability to set static NAT (one-to-one) in one rule
set nat static rule 10 destination address '203.0.113.0/24'
set nat static rule 10 inbound-interface 'eth0'
set nat static rule 10 translation address '192.0.2.0/24'
It will be enough for PREROUTING and POSTROUTING rules
Use a separate table 'vyos_static_nat' as SRC/DST rules and
STATIC rules can have the same rule number
| -rw-r--r-- | data/templates/firewall/nftables-static-nat.j2 | 115 | ||||
| -rw-r--r-- | data/templates/firewall/nftables.j2 | 20 | ||||
| -rw-r--r-- | interface-definitions/include/inbound-interface.xml.i | 11 | ||||
| -rw-r--r-- | interface-definitions/include/ipv4-address-prefix.xml.i | 19 | ||||
| -rw-r--r-- | interface-definitions/nat.xml.in | 53 | ||||
| -rwxr-xr-x | src/conf_mode/nat.py | 18 | 
6 files changed, 226 insertions, 10 deletions
| diff --git a/data/templates/firewall/nftables-static-nat.j2 b/data/templates/firewall/nftables-static-nat.j2 new file mode 100644 index 000000000..d3c43858f --- /dev/null +++ b/data/templates/firewall/nftables-static-nat.j2 @@ -0,0 +1,115 @@ +#!/usr/sbin/nft -f + +{% macro nat_rule(rule, config, chain) %} +{% set comment  = '' %} +{% set base_log = '' %} + +{% if chain is vyos_defined('PREROUTING') %} +{%     set comment   = 'STATIC-NAT-' ~ rule %} +{%     set base_log  = '[NAT-DST-' ~ rule %} +{%     set interface = ' iifname "' ~ config.inbound_interface ~ '"' if config.inbound_interface is vyos_defined and config.inbound_interface is not vyos_defined('any') else '' %} +{%     if config.translation.address is vyos_defined %} +{#         support 1:1 network translation #} +{%         if config.translation.address | is_ip_network %} +{%             set trns_addr = 'dnat ip prefix to ip daddr map { ' ~ config.destination.address ~ ' : ' ~ config.translation.address ~ ' }' %} +{#             we can now clear out the dst_addr part as it's already covered in aboves map #} +{%         else %} +{%             set dst_addr  = 'ip daddr ' ~ config.destination.address if config.destination.address is vyos_defined %} +{%             set trns_addr = 'dnat to ' ~ config.translation.address %} +{%         endif %} +{%     endif %} +{% elif chain is vyos_defined('POSTROUTING') %} +{%     set comment   = 'STATIC-NAT-' ~ rule %} +{%     set base_log  = '[NAT-SRC-' ~ rule %} +{%     set interface = ' oifname "' ~ config.inbound_interface ~ '"' if config.inbound_interface is vyos_defined and config.inbound_interface is not vyos_defined('any') else '' %} +{%     if config.translation.address is vyos_defined %} +{#         support 1:1 network translation #} +{%         if config.translation.address | is_ip_network %} +{%             set trns_addr = 'snat ip prefix to ip saddr map { ' ~ config.translation.address ~ ' : ' ~ config.destination.address ~ ' }' %} +{#             we can now clear out the src_addr part as it's already covered in aboves map #} +{%         else %} +{%             set src_addr  = 'ip saddr ' ~ config.translation.address if config.translation.address is vyos_defined %} +{%             set trns_addr = 'snat to ' ~ config.destination.address %} +{%         endif %} +{%     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 %} + +{% if config.translation.options is vyos_defined %} +{%     if config.translation.options.address_mapping is vyos_defined('persistent') %} +{%         set trns_opts_addr  = 'persistent' %} +{%     endif %} +{%     if config.translation.options.port_mapping is vyos_defined('random') %} +{%         set trns_opts_port  = 'random' %} +{%     elif config.translation.options.port_mapping is vyos_defined('fully-random') %} +{%         set trns_opts_port  = 'fully-random' %} +{%     endif %} +{% endif %} + +{% if trns_opts_addr is vyos_defined and trns_opts_port is vyos_defined %} +{%     set trns_opts  = trns_opts_addr ~ ',' ~ trns_opts_port %} +{% elif trns_opts_addr is vyos_defined %} +{%     set trns_opts  = trns_opts_addr %} +{% elif trns_opts_port is vyos_defined %} +{%     set trns_opts  = trns_opts_port %} +{% endif %} + +{% set output = 'add rule ip vyos_static_nat ' ~ chain ~ interface %} + +{% if dst_addr is vyos_defined %} +{%     set output = output ~ ' ' ~ dst_addr %} +{% endif %} +{% if src_addr is vyos_defined %} +{%     set output = output ~ ' ' ~ src_addr %} +{% endif %} + +{# Count packets #} +{% set output = output ~ ' counter' %} +{# Special handling of log option, we must repeat the entire rule before the #} +{# NAT translation options are added, this is essential                      #} +{% if log is vyos_defined %} +{%     set log_output = output ~ ' log prefix "' ~ log ~ '" comment "' ~ comment ~ '"' %} +{% endif %} +{% if trns_addr is vyos_defined %} +{%     set output = output ~ ' ' ~ trns_addr %} +{% endif %} + +{% if trns_opts is vyos_defined %} +{%     set output = output ~ ' ' ~ trns_opts %} +{% endif %} +{% if comment is vyos_defined %} +{%     set output = output ~ ' comment "' ~ comment ~ '"' %} +{% endif %} +{{ log_output if log_output is vyos_defined }} +{{ output }} +{% endmacro %} + +# Start with clean STATIC NAT chains +flush chain ip vyos_static_nat PREROUTING +flush chain ip vyos_static_nat POSTROUTING + +{# NAT if enabled - add targets to nftables #} + +# +# Destination NAT rules build up here +# +add rule ip vyos_static_nat PREROUTING counter jump VYOS_PRE_DNAT_HOOK +{% if static.rule is vyos_defined %} +{%     for rule, config in static.rule.items() if config.disable is not vyos_defined %} +{{ nat_rule(rule, config, 'PREROUTING') }} +{%     endfor %} +{% endif %} +# +# Source NAT rules build up here +# +add rule ip vyos_static_nat POSTROUTING counter jump VYOS_PRE_SNAT_HOOK +{% if static.rule is vyos_defined %} +{%     for rule, config in static.rule.items() if config.disable is not vyos_defined %} +{{ nat_rule(rule, config, 'POSTROUTING') }} +{%     endfor %} +{% endif %} diff --git a/data/templates/firewall/nftables.j2 b/data/templates/firewall/nftables.j2 index b91fed615..5971e1bbc 100644 --- a/data/templates/firewall/nftables.j2 +++ b/data/templates/firewall/nftables.j2 @@ -181,6 +181,26 @@ table ip nat {      }  } +table ip vyos_static_nat { +    chain PREROUTING { +        type nat hook prerouting priority -100; policy accept; +        counter jump VYOS_PRE_DNAT_HOOK +    } + +    chain POSTROUTING { +        type nat hook postrouting priority 100; policy accept; +        counter jump VYOS_PRE_SNAT_HOOK +    } + +    chain VYOS_PRE_DNAT_HOOK { +        return +    } + +    chain VYOS_PRE_SNAT_HOOK { +        return +    } +} +  table ip6 nat {      chain PREROUTING {          type nat hook prerouting priority -100; policy accept; diff --git a/interface-definitions/include/inbound-interface.xml.i b/interface-definitions/include/inbound-interface.xml.i new file mode 100644 index 000000000..3289bbf8f --- /dev/null +++ b/interface-definitions/include/inbound-interface.xml.i @@ -0,0 +1,11 @@ +<!-- include start from inbound-interface.xml.i --> +<leafNode name="inbound-interface"> +  <properties> +    <help>Inbound interface of NAT traffic</help> +    <completionHelp> +      <list>any</list> +      <script>${vyos_completion_dir}/list_interfaces.py</script> +    </completionHelp> +  </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/ipv4-address-prefix.xml.i b/interface-definitions/include/ipv4-address-prefix.xml.i new file mode 100644 index 000000000..f5be6f1fe --- /dev/null +++ b/interface-definitions/include/ipv4-address-prefix.xml.i @@ -0,0 +1,19 @@ +<!-- include start from ipv4-address-prefix.xml.i --> +<leafNode name="address"> +  <properties> +    <help>IP address, prefix</help> +    <valueHelp> +      <format>ipv4</format> +      <description>IPv4 address to match</description> +    </valueHelp> +    <valueHelp> +      <format>ipv4net</format> +      <description>IPv4 prefix to match</description> +    </valueHelp> +    <constraint> +      <validator name="ipv4-address"/> +      <validator name="ipv4-prefix"/> +    </constraint> +  </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/nat.xml.in b/interface-definitions/nat.xml.in index 9295b631f..501ff05d3 100644 --- a/interface-definitions/nat.xml.in +++ b/interface-definitions/nat.xml.in @@ -14,15 +14,7 @@            #include <include/nat-rule.xml.i>            <tagNode name="rule">              <children> -              <leafNode name="inbound-interface"> -                <properties> -                  <help>Inbound interface of NAT traffic</help> -                  <completionHelp> -                    <list>any</list> -                    <script>${vyos_completion_dir}/list_interfaces.py</script> -                  </completionHelp> -                </properties> -              </leafNode> +              #include <include/inbound-interface.xml.i>                <node name="translation">                  <properties>                    <help>Inside NAT IP (destination NAT only)</help> @@ -65,6 +57,17 @@          <children>            #include <include/nat-rule.xml.i>            <tagNode name="rule"> +            <properties> +              <help>Rule number for NAT</help> +              <valueHelp> +                <format>u32:1-999999</format> +                <description>Number of NAT rule</description> +              </valueHelp> +              <constraint> +                <validator name="numeric" argument="--range 1-999999"/> +              </constraint> +              <constraintErrorMessage>NAT rule number must be between 1 and 999999</constraintErrorMessage> +            </properties>              <children>                #include <include/nat-interface.xml.i>                <node name="translation"> @@ -110,6 +113,38 @@            </tagNode>          </children>        </node> +      <node name="static"> +        <properties> +          <help>Static NAT (one-to-one)</help> +        </properties> +        <children> +          <tagNode name="rule"> +            <properties> +              <help>Rule number for NAT</help> +            </properties> +            <children> +              #include <include/generic-description.xml.i> +              <node name="destination"> +                <properties> +                  <help>NAT destination parameters</help> +                </properties> +                <children> +                  #include <include/ipv4-address-prefix.xml.i> +                </children> +              </node> +              #include <include/inbound-interface.xml.i> +              <node name="translation"> +                <properties> +                  <help>Translation address or prefix</help> +                </properties> +                <children> +                  #include <include/ipv4-address-prefix.xml.i> +                </children> +              </node> +            </children> +          </tagNode> +        </children> +      </node>      </children>    </node>  </interfaceDefinition> diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index 85819a77e..b76ea9f9e 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -45,6 +45,7 @@ else:      k_mod = ['nft_nat', 'nft_chain_nat_ipv4']  nftables_nat_config = '/tmp/vyos-nat-rules.nft' +nftables_static_nat_conf = '/tmp/vyos-static-nat-rules.nft'  def get_handler(json, chain, target):      """ Get nftable rule handler number of given chain/target combination. @@ -88,7 +89,7 @@ def get_config(config=None):      # T2665: we must add the tagNode defaults individually until this is      # moved to the base class -    for direction in ['source', 'destination']: +    for direction in ['source', 'destination', 'static']:          if direction in nat:              default_values = defaults(base + [direction, 'rule'])              for rule in dict_search(f'{direction}.rule', nat) or []: @@ -178,10 +179,22 @@ def verify(nat):              # common rule verification              verify_rule(config, err_msg) +    if dict_search('static.rule', nat): +        for rule, config in dict_search('static.rule', nat).items(): +            err_msg = f'Static NAT configuration error in rule {rule}:' + +            if 'inbound_interface' not in config: +                raise ConfigError(f'{err_msg}\n' \ +                                  'inbound-interface not specified') + +            # common rule verification +            verify_rule(config, err_msg) +      return None  def generate(nat):      render(nftables_nat_config, 'firewall/nftables-nat.j2', nat) +    render(nftables_static_nat_conf, 'firewall/nftables-static-nat.j2', nat)      # dry-run newly generated configuration      tmp = run(f'nft -c -f {nftables_nat_config}') @@ -190,10 +203,13 @@ def generate(nat):              os.unlink(nftables_nat_config)          raise ConfigError('Configuration file errors encountered!') +    tmp = run(f'nft -c -f {nftables_nat_config}') +      return None  def apply(nat):      cmd(f'nft -f {nftables_nat_config}') +    cmd(f'nft -f {nftables_static_nat_conf}')      if os.path.isfile(nftables_nat_config):          os.unlink(nftables_nat_config) | 
