Page MenuHomeVyOS Platform

Show nat source rules shows unexpected dictionary
Open, NormalPublicBUG

Description

Show nat source shows unexpected dictionary

set nat source rule 100 destination port '5000-8000'
set nat source rule 100 outbound-interface name 'eth0'
set nat source rule 100 protocol 'tcp'
set nat source rule 100 source address '10.0.0.0/24'
set nat source rule 100 translation address 'masquerade'

show:

vyos@r4# run show nat source rules 
Rule    Source       Destination                    Proto    Out-Int    Translation
------  -----------  -----------------------------  -------  ---------  -------------
100     10.0.0.0/24  0.0.0.0/0                      IP       eth0       masquerade
        sport any    dport {'range': [5000, 8000]}
[edit]
vyos@r4#

Details

Difficulty level
Unknown (require assessment)
Version
VyOS 1.5-rolling-202405140019
Why the issue appeared?
Will be filled on close
Is it a breaking change?
Unspecified (possibly destroys the router)
Issue type
Bug (incorrect behavior)

Event Timeline

Viacheslav triaged this task as Normal priority.

@Viacheslav
Happy to dig into this if can assign it to me.

@Viacheslav, same behaviour exists for epa3, I numbered mine 999 so as not to interfere with existing rules.

The fix can be backported once finished.

$ show version
Version:          VyOS 1.4.0-epa3
Release train:    sagitta

Built by:         Sentrium S.L.
Built on:         Mon 13 May 2024 13:59 UTC
Build UUID:       3533a227-511d-493b-ac9f-18d529653172
Build commit ID:  65282cef38b3ad

Architecture:     x86_64
Boot via:         installed image
System type:      bare metal

Hardware vendor:  Dell Inc.
Hardware model:   OptiPlex 3060
Hardware S/N:     XXXXXX
Hardware UUID:    XXXXXX

Copyright:        VyOS maintainers and contributors

admin@zeus:~$ show nat source rules
Rule    Source        Destination                    Proto    Out-Int    Translation
------  ------------  -----------------------------  -------  ---------  -------------
100     10.0.0.0/16   0.0.0.0/0                      IP       pppoe0     masquerade
        sport any     dport any
200     10.0.20.0/24  192.168.2.1                    IP       eth0       192.168.2.254
        sport any     dport any
999     10.0.0.0/24   0.0.0.0/0                      IP       eth0       masquerade
        sport any     dport {'range': [5000, 8000]}

Follow up, I was able to make nat.py throw the error below.

As the completion helper for port says you can pass a comma-separated list, I tried just that to see how the 'show' output would look but got the error instead.

vyos-A# set nat source rule 100 destination port 
Possible completions:
   <text>               Named port (any name in /etc/services, e.g., http)
   <1-65535>            Numeric IP port
   start-end            Numbered port range (e.g. 1001-1005)
   None                 

Multiple destination ports can be specified as a comma-separated list.
The whole list can also be negated using '!'.
For example: '!22,telnet,http,123,1001-1005'
   !22,22,http,5000-8000

Adjusting the rules you posted above to:

set nat source rule 100 destination port '!22,22,http,5000-8000'
set nat source rule 100 outbound-interface name 'eth0'
set nat source rule 100 protocol 'tcp'
set nat source rule 100 source address '10.0.0.0/24'
set nat source rule 100 translation address 'masquerade'

Yields:

vyos-A:~/vyos-1x/src/op_mode$ show nat source rules
Traceback (most recent call last):
  File "/usr/libexec/vyos/op_mode/nat.py", line 337, in <module>
    res = vyos.opmode.run(sys.modules[__name__])
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3/dist-packages/vyos/opmode.py", line 263, in run
    res = func(**args)
          ^^^^^^^^^^^^
  File "/usr/libexec/vyos/op_mode/nat.py", line 296, in _wrapper
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/usr/libexec/vyos/op_mode/nat.py", line 306, in show_rules
    return _get_formatted_output_rules(nat_rules, direction, family)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/libexec/vyos/op_mode/nat.py", line 139, in _get_formatted_output_rules
    dport = my_dict["set"][0]["range"]
            ~~~~~~~~~~~~~~~~~^^^^^^^^^
TypeError: 'int' object is not sub-scriptable

This issue can be reproduced in both VyOS 1.4.0-epa3 and VyOS 1.5.0

Further digging: the dport output right after line 136 looks like below ... it caught the duplicate port 22 and didn't include it twice. The list of ports and port ranges including the '!' would need handled by the function.

{'protocol': 'tcp', 'field': 'dport', 'set': [22, 80, {'range': [5000, 8000]}], 'op': '!='}

PR opened: https://github.com/vyos/vyos-1x/pull/3532

Test results:

No NAT rules configured:

vyos@vyos-A:~/vyos-1x/src/op_mode$ sudo python3 nat.py show_rules --direction source --family inet
NAT is not configured

vyos@vyos-A:~/vyos-1x/src/op_mode$ sudo python3 nat.py show_rules --direction destination --family inet
NAT is not configured

Test NAT rules:

set nat destination rule 100 inbound-interface name 'eth1'
set nat destination rule 100 protocol 'tcp'
set nat destination rule 100 source port '!22,22,ssh,http,5000-8000,9000-9500,2222,3999'
set nat destination rule 100 translation address '192.0.2.222'
set nat destination rule 100 destination port '22,22,ssh,http,5000-8000,9000-9500,2222,3999'
set nat destination rule 200 inbound-interface name 'eth1'
set nat destination rule 200 protocol 'udp'
set nat destination rule 200 source port '53'
set nat destination rule 200 translation address '192.0.100.100'
set nat destination rule 200 destination port '53'
set nat source rule 100 outbound-interface name 'eth1'
set nat source rule 100 protocol 'tcp'
set nat source rule 100 source address '10.0.0.0/24'
set nat source rule 100 source port '!ftp,8080,9999-19999'
set nat source rule 100 translation address 'masquerade'

Results after test NAT rules committed:

vyos@vyos-A:~/vyos-1x/src/op_mode$ sudo python3 nat.py show_rules --direction destination --family inet

Rule    Source                                      Destination                                Proto    In-Int    Translation

------  ------------------------------------------  -----------------------------------------  -------  --------  -------------

100     0.0.0.0/0                                   0.0.0.0/0                                  TCP      eth1      192.0.2.222

        sport !22,80,2222,3999,5000-8000,9000-9500  dport 22,80,2222,3999,5000-8000,9000-9500

200     0.0.0.0/0                                   0.0.0.0/0                                  any      eth1      192.0.100.100

        sport 53                                    dport 53

vyos@vyos-A:~/vyos-1x/src/op_mode$ sudo python3 nat.py show_rules --direction source --family inet

Rule    Source                     Destination    Proto    Out-Int    Translation

------  -------------------------  -------------  -------  ---------  -------------

100     10.0.0.0/24                0.0.0.0/0      TCP      eth1       masquerade

        sport !21,8080,9999-19999  dport any

Still bug, the original config in the top of the task

vyos@r4# run show conf com | match "nat "
set nat source rule 100 destination port '5000-8000'
set nat source rule 100 outbound-interface name 'eth0'
set nat source rule 100 protocol 'tcp'
set nat source rule 100 source address '10.0.0.0/24'
set nat source rule 100 translation address 'masquerade'
[edit]
vyos@r4# 
[edit]
vyos@r4# run show nat source rules 
Rule    Source       Destination                    Proto    Out-Int    Translation
------  -----------  -----------------------------  -------  ---------  -------------
100     10.0.0.0/24  0.0.0.0/0                      IP       eth0       masquerade
        sport any    dport {'range': [5000, 8000]}
[edit]
vyos@r4# 
[edit]
vyos@r4# 
[edit]
vyos@r4# run show ver
Version:          VyOS 1.5-rolling-202406260020
Release train:    current
Release flavor:   generic

Built by:         [email protected]
Built on:         Wed 26 Jun 2024 03:19 UTC
Build UUID:       d6745d1f-dcde-48a4-99b7-01d3ad55f0ca
Build commit ID:  4e9f1b4a219bd3

@Viacheslav found the issue which I've documented below.

The JSON output of these rules:

set nat source rule 100 destination port '5000-8000'
set nat source rule 100 outbound-interface name 'eth0'
set nat source rule 100 protocol 'tcp'
set nat source rule 100 source address '10.0.0.0/24'
set nat source rule 100 translation address 'masquerade'

Looks like:

{'nftables': [{'metainfo': {'version': '1.0.9', 'release_name': 'Old Doc Yak #3', 'json_schema_version': 1}}, {'chain': {'family': 'ip', 'table': 'vyos_nat', 'name': 'POSTROUTING', 'handle': 2, 'type': 'nat', 'hook': 'postrouting', 'prio': 100, 'policy': 'accept'}}, {'rule': {'family': 'ip', 'table': 'vyos_nat', 'chain': 'POSTROUTING', 'handle': 6, 'expr': [{'counter': {'packets': 7, 'bytes': 532}}, {'jump': {'target': 'VYOS_PRE_SNAT_HOOK'}}]}}, {'rule': {'family': 'ip', 'table': 'vyos_nat', 'chain': 'POSTROUTING', 'handle': 7, 'comment': 'SRC-NAT-100', 'expr': [{'match': {'op': '==', 'left': {'meta': {'key': 'oifname'}}, 'right': 'eth0'}}, {'match': {'op': '==', 'left': {'payload': {'protocol': 'ip', 'field': 'saddr'}}, 'right': {'prefix': {'addr': '10.0.0.0', 'len': 24}}}}, {'match': {'op': '==', 'left': {'payload': {'protocol': 'tcp', 'field': 'dport'}}, 'right': {'range': [5000, 8000]}}}, {'counter': {'packets': 0, 'bytes': 0}}, {'masquerade': None}]}}]}

Interestingly when the config consists of a single port range there is no "set" inserted into the JSON above, so at this point in the code: https://github.com/vyos/vyos-1x/blob/current/src/op_mode/nat.py#L135, the merging of src/dst l3_l4 parameters doesn't happen. I've reworked the script to make it more robust and will submit a PR soon.

Below is a series of tests I've run to show that the correct ports are being printed to the users.

Rules used to test robustness:

vyos@vyos:~/vyos-1x/src/op_mode$ show conf com | match "nat"
set nat destination rule 100 destination port '5000-8000'
set nat destination rule 100 inbound-interface name 'eth0'
set nat destination rule 100 protocol 'tcp'
set nat destination rule 100 source address '10.0.100.0/24'
set nat destination rule 100 translation address '10.100.100.1'
set nat destination rule 120 destination port '!120'
set nat destination rule 120 inbound-interface name 'eth0'
set nat destination rule 120 protocol 'tcp'
set nat destination rule 120 source address '10.0.120.0/24'
set nat destination rule 120 translation address '10.120.120.1'
set nat destination rule 130 destination port 'ftp,130,9000-9999'
set nat destination rule 130 inbound-interface name 'eth0'
set nat destination rule 130 protocol 'tcp'
set nat destination rule 130 source address '10.0.130.0/24'
set nat destination rule 130 translation address '10.130.130.1'
set nat destination rule 140 destination port '!ftp,130,999'
set nat destination rule 140 inbound-interface name 'eth0'
set nat destination rule 140 protocol 'udp'
set nat destination rule 140 source address '10.0.140.0/24'
set nat destination rule 140 translation address '10.140.140.1'
set nat destination rule 150 destination port '10000-12000,30000-33000'
set nat destination rule 150 inbound-interface name 'eth0'
set nat destination rule 150 protocol 'udp'
set nat destination rule 150 source address '10.0.150.0/24'
set nat destination rule 150 translation address '10.150.150.1'
set nat source rule 100 destination port '5000-8000'
set nat source rule 100 outbound-interface name 'eth0'
set nat source rule 100 protocol 'tcp'
set nat source rule 100 source address '10.0.0.0/24'
set nat source rule 100 translation address 'masquerade'
set nat source rule 120 destination port '120'
set nat source rule 120 outbound-interface name 'eth0'
set nat source rule 120 protocol 'tcp'
set nat source rule 120 source address '10.0.120.0/24'
set nat source rule 120 translation address 'masquerade'
set nat source rule 130 destination port 'ftp,130,9000-9999'
set nat source rule 130 outbound-interface name 'eth0'
set nat source rule 130 protocol 'tcp'
set nat source rule 130 source address '10.0.130.0/24'
set nat source rule 130 translation address 'masquerade'
set nat source rule 140 destination port '!ftp,130,999'
set nat source rule 140 outbound-interface name 'eth0'
set nat source rule 140 protocol 'udp'
set nat source rule 140 source address '10.0.140.0/24'
set nat source rule 140 translation address 'masquerade'
set nat source rule 150 destination port '10000-12000,30000-33000'
set nat source rule 150 outbound-interface name 'eth0'
set nat source rule 150 protocol 'udp'
set nat source rule 150 source address '10.0.150.0/24'
set nat source rule 150 translation address 'masquerade'

Output:

vyos@vyos:~/vyos-1x/src/op_mode$ sudo python3 nat.py show_rules --direction source --family inet
Rule    Source         Destination                    Proto    Out-Int    Translation
------  -------------  -----------------------------  -------  ---------  -------------
100     10.0.0.0/24    0.0.0.0/0                      TCP      eth0       masquerade
        sport any      dport 5000-8000
120     10.0.120.0/24  0.0.0.0/0                      IP       eth0       masquerade
        sport any      dport 120
130     10.0.130.0/24  0.0.0.0/0                      TCP      eth0       masquerade
        sport any      dport 21,130,9000-9999
140     10.0.140.0/24  0.0.0.0/0                      UDP      eth0       masquerade
        sport any      dport !21,130,999
150     10.0.150.0/24  0.0.0.0/0                      UDP      eth0       masquerade
        sport any      dport 10000-12000,30000-33000

vyos@vyos:~/vyos-1x/src/op_mode$ sudo python3 nat.py show_rules --direction destination --family inet
Rule    Source         Destination                    Proto    In-Int    Translation
------  -------------  -----------------------------  -------  --------  -------------
100     10.0.100.0/24  0.0.0.0/0                      TCP      eth0      10.100.100.1
        sport any      dport 5000-8000
120     10.0.120.0/24  0.0.0.0/0                      IP       eth0      10.120.120.1
        sport any      dport !120
130     10.0.130.0/24  0.0.0.0/0                      TCP      eth0      10.130.130.1
        sport any      dport 21,130,9000-9999
140     10.0.140.0/24  0.0.0.0/0                      UDP      eth0      10.140.140.1
        sport any      dport !21,130,999
150     10.0.150.0/24  0.0.0.0/0                      UDP      eth0      10.150.150.1
        sport any      dport 10000-12000,30000-33000